Add game version management system with manifest support
Features: - Version dropdown in single/ZIP upload (9.3.0, 9.2.0, etc.) - Patch-compatible matching (9.3.x assets work with 9.3.0) - manifest.json/xml support for automatic metadata detection - Smart category auto-detection from folder structure - Version field stored in GameAssets table Manifest support: - JSON format with gameVersion, category, assets array - Per-file metadata overrides (type, required, description) - Auto-detect falls back if no manifest present - See ASSET-MANIFEST-SPECIFICATION.md for full spec Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
154
ASSET-MANIFEST-SPECIFICATION.md
Normal file
154
ASSET-MANIFEST-SPECIFICATION.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# RR3 Asset Manifest Specification
|
||||
|
||||
## Overview
|
||||
When uploading ZIP files to the RR3 Community Server, you can include a `manifest.json` or `manifest.xml` file to automatically configure asset metadata, version, and categorization.
|
||||
|
||||
## File Format Options
|
||||
|
||||
### Option 1: JSON Format (Recommended)
|
||||
Place `manifest.json` in the root of your ZIP file:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "9.3.0",
|
||||
"gameVersion": "9.3.0",
|
||||
"description": "Porsche Pack - 911 Models",
|
||||
"author": "CommunityModder",
|
||||
"category": "cars",
|
||||
"assets": [
|
||||
{
|
||||
"file": "porsche/911_turbo.dat",
|
||||
"category": "cars/porsche",
|
||||
"type": "Model",
|
||||
"required": true,
|
||||
"description": "Porsche 911 Turbo model"
|
||||
},
|
||||
{
|
||||
"file": "textures/911_turbo_paint.tex",
|
||||
"category": "textures/cars",
|
||||
"type": "Texture",
|
||||
"required": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Option 2: XML Format
|
||||
Place `manifest.xml` in the root of your ZIP file:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<AssetManifest>
|
||||
<Version>9.3.0</Version>
|
||||
<GameVersion>9.3.0</GameVersion>
|
||||
<Description>Porsche Pack - 911 Models</Description>
|
||||
<Author>CommunityModder</Author>
|
||||
<Category>cars</Category>
|
||||
<Assets>
|
||||
<Asset>
|
||||
<File>porsche/911_turbo.dat</File>
|
||||
<Category>cars/porsche</Category>
|
||||
<Type>Model</Type>
|
||||
<Required>true</Required>
|
||||
<Description>Porsche 911 Turbo model</Description>
|
||||
</Asset>
|
||||
<Asset>
|
||||
<File>textures/911_turbo_paint.tex</File>
|
||||
<Category>textures/cars</Category>
|
||||
<Type>Texture</Type>
|
||||
<Required>false</Required>
|
||||
</Asset>
|
||||
</Assets>
|
||||
</AssetManifest>
|
||||
```
|
||||
|
||||
## Field Descriptions
|
||||
|
||||
### Root Fields
|
||||
- **version**: Asset pack version (e.g., "1.0.0")
|
||||
- **gameVersion**: RR3 game version this pack is for (e.g., "9.3.0")
|
||||
- **description**: Brief description of the asset pack
|
||||
- **author**: Creator name (optional)
|
||||
- **category**: Default category if not specified per-asset
|
||||
|
||||
### Asset Fields
|
||||
- **file**: Relative path to file within ZIP (required)
|
||||
- **category**: Asset category (overrides root category)
|
||||
- **type**: Asset type (Data, Texture, Audio, Model, Config)
|
||||
- **required**: Whether clients must download this (true/false)
|
||||
- **description**: Brief description (optional)
|
||||
|
||||
## Game Version Format
|
||||
- Use semantic versioning: `MAJOR.MINOR.PATCH`
|
||||
- Examples: `9.3.0`, `9.3.1`, `10.0.0`
|
||||
- **Compatibility**: Patch versions are compatible (9.3.x works with 9.3.0)
|
||||
|
||||
## Categories
|
||||
Standard categories:
|
||||
- `base` - Core game files
|
||||
- `cars` - Car models and data
|
||||
- `tracks` - Track models and data
|
||||
- `audio` - Sound effects and music
|
||||
- `textures` - Texture files
|
||||
- `ui` - User interface elements
|
||||
- `events` - Event configurations
|
||||
- `dlc` - Downloadable content
|
||||
- `updates` - Game updates
|
||||
|
||||
Subcategories allowed (e.g., `cars/porsche`, `tracks/silverstone`)
|
||||
|
||||
## Asset Types
|
||||
- `Data` - Generic data files (.dat, .pak, .z)
|
||||
- `Texture` - Texture files (.tex, .dds, .png)
|
||||
- `Audio` - Audio files (.ogg, .mp3, .wav)
|
||||
- `Model` - 3D model files (.nct, .obj)
|
||||
- `Config` - Configuration files (.json, .xml)
|
||||
|
||||
## Automatic Detection Fallback
|
||||
If no manifest file is provided, the server uses smart detection:
|
||||
1. Searches folder names for keywords (cars, tracks, audio, etc.)
|
||||
2. Preserves folder structure as subcategories
|
||||
3. Falls back to first folder name if no keywords match
|
||||
4. Version defaults to manual selection or "unknown"
|
||||
|
||||
## Example ZIP Structure
|
||||
|
||||
```
|
||||
my-asset-pack.zip
|
||||
├── manifest.json # Metadata file
|
||||
├── cars/
|
||||
│ ├── porsche/
|
||||
│ │ ├── 911_turbo.dat
|
||||
│ │ └── 911_gt3.dat
|
||||
│ └── ferrari/
|
||||
│ └── 488_gtb.dat
|
||||
└── textures/
|
||||
└── cars/
|
||||
└── paint_textures.pak
|
||||
```
|
||||
|
||||
## Upload Behavior
|
||||
1. Server extracts ZIP to temp location
|
||||
2. Searches for `manifest.json` or `manifest.xml` in root
|
||||
3. If found: Uses metadata from manifest
|
||||
4. If not found: Uses smart folder detection
|
||||
5. Processes each file according to configuration
|
||||
6. Calculates MD5/SHA256 hashes automatically
|
||||
7. Stores in database with version + category info
|
||||
|
||||
## Version Compatibility
|
||||
When a game client requests assets:
|
||||
- Client sends version: `9.3.1`
|
||||
- Server returns assets for: `9.3.0`, `9.3.1` (patch-compatible)
|
||||
- Server excludes: `9.2.x`, `9.4.x`, `10.x.x`
|
||||
|
||||
Major/minor versions must match exactly, patch versions are compatible within the same minor version.
|
||||
|
||||
## Best Practices
|
||||
1. Always include a manifest file for large packs
|
||||
2. Use semantic versioning for both pack and game version
|
||||
3. Mark base game assets as `required: true`
|
||||
4. Use descriptive categories and subcategories
|
||||
5. Include author information for community tracking
|
||||
6. Test with single-file upload before bulk ZIP upload
|
||||
7. Use JSON format for better tool support
|
||||
966
RR3CommunityServer/Migrations/20260220175450_AddGameVersioning.Designer.cs
generated
Normal file
966
RR3CommunityServer/Migrations/20260220175450_AddGameVersioning.Designer.cs
generated
Normal file
@@ -0,0 +1,966 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using RR3CommunityServer.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RR3CommunityServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(RR3DbContext))]
|
||||
[Migration("20260220175450_AddGameVersioning")]
|
||||
partial class AddGameVersioning
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
|
||||
|
||||
modelBuilder.Entity("RR3CommunityServer.Data.Car", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Available")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("BasePerformanceRating")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CarId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("CashPrice")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ClassType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CustomAuthor")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CustomVersion")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("GoldPrice")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsCustom")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Manufacturer")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Year")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Cars");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
Available = true,
|
||||
BasePerformanceRating = 45,
|
||||
CarId = "nissan_silvia_s15",
|
||||
CashPrice = 25000,
|
||||
ClassType = "C",
|
||||
GoldPrice = 0,
|
||||
IsCustom = false,
|
||||
Manufacturer = "Nissan",
|
||||
Name = "Nissan Silvia Spec-R",
|
||||
Year = 0
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2,
|
||||
Available = true,
|
||||
BasePerformanceRating = 58,
|
||||
CarId = "ford_focus_rs",
|
||||
CashPrice = 85000,
|
||||
ClassType = "B",
|
||||
GoldPrice = 150,
|
||||
IsCustom = false,
|
||||
Manufacturer = "Ford",
|
||||
Name = "Ford Focus RS",
|
||||
Year = 0
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 3,
|
||||
Available = true,
|
||||
BasePerformanceRating = 72,
|
||||
CarId = "porsche_911_gt3",
|
||||
CashPrice = 0,
|
||||
ClassType = "A",
|
||||
GoldPrice = 350,
|
||||
IsCustom = false,
|
||||
Manufacturer = "Porsche",
|
||||
Name = "Porsche 911 GT3 RS",
|
||||
Year = 0
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 4,
|
||||
Available = true,
|
||||
BasePerformanceRating = 88,
|
||||
CarId = "ferrari_488_gtb",
|
||||
CashPrice = 0,
|
||||
ClassType = "S",
|
||||
GoldPrice = 750,
|
||||
IsCustom = false,
|
||||
Manufacturer = "Ferrari",
|
||||
Name = "Ferrari 488 GTB",
|
||||
Year = 0
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 5,
|
||||
Available = true,
|
||||
BasePerformanceRating = 105,
|
||||
CarId = "mclaren_p1_gtr",
|
||||
CashPrice = 0,
|
||||
ClassType = "R",
|
||||
GoldPrice = 1500,
|
||||
IsCustom = false,
|
||||
Manufacturer = "McLaren",
|
||||
Name = "McLaren P1 GTR",
|
||||
Year = 0
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RR3CommunityServer.Data.CarUpgrade", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CarId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("CashCost")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Level")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("PerformanceIncrease")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("UpgradeType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("CarUpgrades");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
CarId = "nissan_silvia_s15",
|
||||
CashCost = 5000,
|
||||
Level = 1,
|
||||
PerformanceIncrease = 3,
|
||||
UpgradeType = "engine"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2,
|
||||
CarId = "nissan_silvia_s15",
|
||||
CashCost = 3000,
|
||||
Level = 1,
|
||||
PerformanceIncrease = 2,
|
||||
UpgradeType = "tires"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 3,
|
||||
CarId = "nissan_silvia_s15",
|
||||
CashCost = 4000,
|
||||
Level = 1,
|
||||
PerformanceIncrease = 2,
|
||||
UpgradeType = "suspension"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 4,
|
||||
CarId = "nissan_silvia_s15",
|
||||
CashCost = 3500,
|
||||
Level = 1,
|
||||
PerformanceIncrease = 2,
|
||||
UpgradeType = "brakes"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 5,
|
||||
CarId = "nissan_silvia_s15",
|
||||
CashCost = 4500,
|
||||
Level = 1,
|
||||
PerformanceIncrease = 3,
|
||||
UpgradeType = "drivetrain"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RR3CommunityServer.Data.CareerProgress", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<double>("BestTime")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<bool>("Completed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EventName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SeriesName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("StarsEarned")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("CareerProgress");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RR3CommunityServer.Data.CatalogItem", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Available")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("Price")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Sku")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("CatalogItems");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
Available = true,
|
||||
Name = "1000 Gold",
|
||||
Price = 0.99m,
|
||||
Sku = "com.ea.rr3.gold_1000",
|
||||
Type = "currency"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2,
|
||||
Available = true,
|
||||
Name = "Starter Car",
|
||||
Price = 0m,
|
||||
Sku = "com.ea.rr3.car_tier1",
|
||||
Type = "car"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 3,
|
||||
Available = true,
|
||||
Name = "Engine Upgrade",
|
||||
Price = 4.99m,
|
||||
Sku = "com.ea.rr3.upgrade_engine",
|
||||
Type = "upgrade"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 4,
|
||||
Available = true,
|
||||
Name = "100 Gold",
|
||||
Price = 0m,
|
||||
Sku = "com.ea.rr3.gold_100",
|
||||
Type = "currency"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 5,
|
||||
Available = true,
|
||||
Name = "500 Gold",
|
||||
Price = 0m,
|
||||
Sku = "com.ea.rr3.gold_500",
|
||||
Type = "currency"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 6,
|
||||
Available = true,
|
||||
Name = "1000 Gold",
|
||||
Price = 0m,
|
||||
Sku = "com.ea.rr3.gold_1000",
|
||||
Type = "currency"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 7,
|
||||
Available = true,
|
||||
Name = "5000 Gold",
|
||||
Price = 0m,
|
||||
Sku = "com.ea.rr3.gold_5000",
|
||||
Type = "currency"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RR3CommunityServer.Data.DailyReward", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("CashAmount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Claimed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("ClaimedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("GoldAmount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("RewardDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Streak")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("DailyRewards");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RR3CommunityServer.Data.Device", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DeviceId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("HardwareId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("LastSeenAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Devices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RR3CommunityServer.Data.GameAsset", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AccessCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AssetId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AssetType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CarId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Category")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long?>("CompressedSize")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ContentType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CustomAuthor")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DownloadedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EaCdnPath")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FileSha256")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("FileSize")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsAvailable")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsCustomContent")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsRequired")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("LastAccessedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("LocalPath")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Md5Hash")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OriginalUrl")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TrackId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UploadedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Version")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("GameAssets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RR3CommunityServer.Data.ModPack", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Author")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CarIds")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("DownloadCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PackId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<double>("Rating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<string>("TrackIds")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Version")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ModPacks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RR3CommunityServer.Data.OwnedCar", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CarId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CarName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ClassType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Manufacturer")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("PerformanceRating")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("PurchasedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PurchasedUpgrades")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("UpgradeLevel")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("OwnedCars");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RR3CommunityServer.Data.Purchase", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ItemId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OrderId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("Price")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("PurchaseTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Sku")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SynergyId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Token")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Purchases");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RR3CommunityServer.Data.Session", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DeviceId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("ExpiresAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SynergyId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Sessions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RR3CommunityServer.Data.TimeTrial", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CarName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("CashReward")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("EndDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("GoldReward")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("StartDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<double>("TargetTime")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<string>("TrackName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("TimeTrials");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
Active = true,
|
||||
CarName = "Any Car",
|
||||
CashReward = 10000,
|
||||
EndDate = new DateTime(2026, 2, 27, 17, 54, 50, 315, DateTimeKind.Utc).AddTicks(3387),
|
||||
GoldReward = 50,
|
||||
Name = "Daily Sprint Challenge",
|
||||
StartDate = new DateTime(2026, 2, 20, 17, 54, 50, 315, DateTimeKind.Utc).AddTicks(3384),
|
||||
TargetTime = 90.5,
|
||||
TrackName = "Silverstone National"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2,
|
||||
Active = true,
|
||||
CarName = "Any Car",
|
||||
CashReward = 25000,
|
||||
EndDate = new DateTime(2026, 2, 27, 17, 54, 50, 315, DateTimeKind.Utc).AddTicks(3395),
|
||||
GoldReward = 100,
|
||||
Name = "Speed Demon Trial",
|
||||
StartDate = new DateTime(2026, 2, 20, 17, 54, 50, 315, DateTimeKind.Utc).AddTicks(3395),
|
||||
TargetTime = 120.0,
|
||||
TrackName = "Dubai Autodrome"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RR3CommunityServer.Data.TimeTrialResult", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("BeatTarget")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("CashEarned")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("GoldEarned")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("SubmittedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<double>("TimeSeconds")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<int>("TimeTrialId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("TimeTrialResults");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RR3CommunityServer.Data.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("Cash")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DeviceId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("Experience")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("Gold")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("Level")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Nickname")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("Reputation")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SynergyId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RR3CommunityServer.Models.Account", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EmailVerificationToken")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("EmailVerified")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("LastLoginAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("PasswordResetExpiry")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordResetToken")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Accounts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RR3CommunityServer.Models.DeviceAccount", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AccountId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("DeviceId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DeviceName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("LastUsedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("LinkedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AccountId");
|
||||
|
||||
b.ToTable("DeviceAccounts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RR3CommunityServer.Models.UserSettings", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("DeviceId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("LastUpdated")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Mode")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ServerUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("UserSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RR3CommunityServer.Data.CareerProgress", b =>
|
||||
{
|
||||
b.HasOne("RR3CommunityServer.Data.User", null)
|
||||
.WithMany("CareerProgress")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RR3CommunityServer.Data.OwnedCar", b =>
|
||||
{
|
||||
b.HasOne("RR3CommunityServer.Data.User", null)
|
||||
.WithMany("OwnedCars")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RR3CommunityServer.Models.Account", b =>
|
||||
{
|
||||
b.HasOne("RR3CommunityServer.Data.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RR3CommunityServer.Models.DeviceAccount", b =>
|
||||
{
|
||||
b.HasOne("RR3CommunityServer.Models.Account", "Account")
|
||||
.WithMany("LinkedDevices")
|
||||
.HasForeignKey("AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Account");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RR3CommunityServer.Data.User", b =>
|
||||
{
|
||||
b.Navigation("CareerProgress");
|
||||
|
||||
b.Navigation("OwnedCars");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RR3CommunityServer.Models.Account", b =>
|
||||
{
|
||||
b.Navigation("LinkedDevices");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RR3CommunityServer.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddGameVersioning : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.UpdateData(
|
||||
table: "TimeTrials",
|
||||
keyColumn: "Id",
|
||||
keyValue: 1,
|
||||
columns: new[] { "EndDate", "StartDate" },
|
||||
values: new object[] { new DateTime(2026, 2, 27, 17, 54, 50, 315, DateTimeKind.Utc).AddTicks(3387), new DateTime(2026, 2, 20, 17, 54, 50, 315, DateTimeKind.Utc).AddTicks(3384) });
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "TimeTrials",
|
||||
keyColumn: "Id",
|
||||
keyValue: 2,
|
||||
columns: new[] { "EndDate", "StartDate" },
|
||||
values: new object[] { new DateTime(2026, 2, 27, 17, 54, 50, 315, DateTimeKind.Utc).AddTicks(3395), new DateTime(2026, 2, 20, 17, 54, 50, 315, DateTimeKind.Utc).AddTicks(3395) });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.UpdateData(
|
||||
table: "TimeTrials",
|
||||
keyColumn: "Id",
|
||||
keyValue: 1,
|
||||
columns: new[] { "EndDate", "StartDate" },
|
||||
values: new object[] { new DateTime(2026, 2, 26, 23, 30, 24, 984, DateTimeKind.Utc).AddTicks(2221), new DateTime(2026, 2, 19, 23, 30, 24, 984, DateTimeKind.Utc).AddTicks(2218) });
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "TimeTrials",
|
||||
keyColumn: "Id",
|
||||
keyValue: 2,
|
||||
columns: new[] { "EndDate", "StartDate" },
|
||||
values: new object[] { new DateTime(2026, 2, 26, 23, 30, 24, 984, DateTimeKind.Utc).AddTicks(2228), new DateTime(2026, 2, 19, 23, 30, 24, 984, DateTimeKind.Utc).AddTicks(2228) });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -711,10 +711,10 @@ namespace RR3CommunityServer.Migrations
|
||||
Active = true,
|
||||
CarName = "Any Car",
|
||||
CashReward = 10000,
|
||||
EndDate = new DateTime(2026, 2, 26, 23, 30, 24, 984, DateTimeKind.Utc).AddTicks(2221),
|
||||
EndDate = new DateTime(2026, 2, 27, 17, 54, 50, 315, DateTimeKind.Utc).AddTicks(3387),
|
||||
GoldReward = 50,
|
||||
Name = "Daily Sprint Challenge",
|
||||
StartDate = new DateTime(2026, 2, 19, 23, 30, 24, 984, DateTimeKind.Utc).AddTicks(2218),
|
||||
StartDate = new DateTime(2026, 2, 20, 17, 54, 50, 315, DateTimeKind.Utc).AddTicks(3384),
|
||||
TargetTime = 90.5,
|
||||
TrackName = "Silverstone National"
|
||||
},
|
||||
@@ -724,10 +724,10 @@ namespace RR3CommunityServer.Migrations
|
||||
Active = true,
|
||||
CarName = "Any Car",
|
||||
CashReward = 25000,
|
||||
EndDate = new DateTime(2026, 2, 26, 23, 30, 24, 984, DateTimeKind.Utc).AddTicks(2228),
|
||||
EndDate = new DateTime(2026, 2, 27, 17, 54, 50, 315, DateTimeKind.Utc).AddTicks(3395),
|
||||
GoldReward = 100,
|
||||
Name = "Speed Demon Trial",
|
||||
StartDate = new DateTime(2026, 2, 19, 23, 30, 24, 984, DateTimeKind.Utc).AddTicks(2228),
|
||||
StartDate = new DateTime(2026, 2, 20, 17, 54, 50, 315, DateTimeKind.Utc).AddTicks(3395),
|
||||
TargetTime = 120.0,
|
||||
TrackName = "Dubai Autodrome"
|
||||
});
|
||||
|
||||
@@ -74,7 +74,12 @@
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="zip-tab" data-bs-toggle="tab" data-bs-target="#zip" type="button" role="tab">
|
||||
📦 ZIP Bulk Upload
|
||||
📦 ZIP Upload
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="url-tab" data-bs-toggle="tab" data-bs-target="#url" type="button" role="tab">
|
||||
🌐 URL Download
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -132,7 +137,30 @@
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label d-block"> </label>
|
||||
<label for="gameVersion" class="form-label">Game Version</label>
|
||||
<select class="form-select" id="gameVersion" name="gameVersion" required>
|
||||
<option value="">Select version...</option>
|
||||
<option value="9.3.0">9.3.0</option>
|
||||
<option value="9.2.0">9.2.0</option>
|
||||
<option value="9.1.0">9.1.0</option>
|
||||
<option value="9.0.0">9.0.0</option>
|
||||
<option value="8.9.0">8.9.0</option>
|
||||
<option value="universal">Universal (All Versions)</option>
|
||||
</select>
|
||||
<small class="text-muted">Patch-compatible: 9.3.x works with 9.3.0</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">Description</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="2" placeholder="Brief description of this asset..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label d-block">Options</label>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="isRequired" name="isRequired" checked>
|
||||
<label class="form-check-label" for="isRequired">
|
||||
@@ -142,10 +170,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">Description</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="2" placeholder="Brief description of this asset..."></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-cloud-upload"></i> Upload Asset
|
||||
</button>
|
||||
@@ -156,19 +180,35 @@
|
||||
<div class="tab-pane fade" id="zip" role="tabpanel">
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i> <strong>ZIP Upload:</strong>
|
||||
Folder structure preserved • Auto MD5 calculation • Existing assets updated
|
||||
Folder structure preserved • Auto MD5 calculation • Manifest.json support
|
||||
</div>
|
||||
<form method="post" enctype="multipart/form-data" asp-page-handler="UploadZip">
|
||||
<div class="mb-3">
|
||||
<label for="zipFile" class="form-label">ZIP Archive</label>
|
||||
<input class="form-control" type="file" id="zipFile" name="zipFile" accept=".zip" required>
|
||||
<small class="text-muted">Example: cars/porsche_911.dat → /cars/porsche_911.dat</small>
|
||||
<small class="text-muted">Include manifest.json for auto-detection • Example: cars/porsche_911.dat → /cars/porsche_911.dat</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label for="zipGameVersion" class="form-label">Game Version</label>
|
||||
<select class="form-select" id="zipGameVersion" name="gameVersion" required>
|
||||
<option value="">Detect from manifest...</option>
|
||||
<option value="9.3.0">9.3.0 (Latest)</option>
|
||||
<option value="9.2.0">9.2.0</option>
|
||||
<option value="9.1.0">9.1.0</option>
|
||||
<option value="9.0.0">9.0.0</option>
|
||||
<option value="8.9.0">8.9.0</option>
|
||||
<option value="universal">Universal</option>
|
||||
</select>
|
||||
<small class="text-muted">Or specify in manifest.json</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label for="baseCategory" class="form-label">Base Category</label>
|
||||
<select class="form-select" id="baseCategory" name="baseCategory">
|
||||
<option value="auto">🤖 Auto-detect</option>
|
||||
<option value="base">Base Assets</option>
|
||||
<option value="cars">Cars</option>
|
||||
<option value="tracks">Tracks</option>
|
||||
@@ -183,11 +223,11 @@
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label d-block"> </label>
|
||||
<label class="form-label d-block">Options</label>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="isRequiredZip" name="isRequired" checked>
|
||||
<label class="form-check-label" for="isRequiredZip">
|
||||
All required
|
||||
Mark as required
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -198,6 +238,55 @@
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- URL Download Tab -->
|
||||
<div class="tab-pane fade" id="url" role="tabpanel">
|
||||
<div class="alert alert-success">
|
||||
<i class="bi bi-cloud-arrow-down"></i> <strong>Direct Download:</strong>
|
||||
Server downloads ZIP directly • No browser upload needed • Perfect for large files
|
||||
</div>
|
||||
<form method="post" asp-page-handler="DownloadZip">
|
||||
<div class="mb-3">
|
||||
<label for="zipUrl" class="form-label">ZIP File URL</label>
|
||||
<input type="url" class="form-control" id="zipUrl" name="zipUrl"
|
||||
placeholder="https://example.com/assets/rr3-cars-pack.zip" required>
|
||||
<small class="text-muted">Direct link to ZIP file (http:// or https://)</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="mb-3">
|
||||
<label for="baseCategoryUrl" class="form-label">Base Category (optional)</label>
|
||||
<select class="form-select" id="baseCategoryUrl" name="baseCategory">
|
||||
<option value="base">Auto-Detect (Smart)</option>
|
||||
<option value="cars">Cars</option>
|
||||
<option value="tracks">Tracks</option>
|
||||
<option value="audio">Audio</option>
|
||||
<option value="textures">Textures</option>
|
||||
<option value="ui">UI</option>
|
||||
<option value="events">Events</option>
|
||||
<option value="dlc">DLC</option>
|
||||
<option value="updates">Updates</option>
|
||||
</select>
|
||||
<small class="text-muted">System will auto-detect categories from folder names</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label d-block"> </label>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="isRequiredUrl" name="isRequired" checked>
|
||||
<label class="form-check-label" for="isRequiredUrl">
|
||||
All required
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-cloud-download"></i> Download and Extract
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ using RR3CommunityServer.Data;
|
||||
using RR3CommunityServer.Models;
|
||||
using System.Security.Cryptography;
|
||||
using System.IO.Compression;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace RR3CommunityServer.Pages;
|
||||
|
||||
@@ -46,6 +47,7 @@ public class AssetsModel : PageModel
|
||||
string category,
|
||||
string assetType,
|
||||
bool isRequired,
|
||||
string gameVersion,
|
||||
string? description)
|
||||
{
|
||||
try
|
||||
@@ -108,6 +110,7 @@ public class AssetsModel : PageModel
|
||||
existingAsset.IsRequired = isRequired;
|
||||
existingAsset.Description = description;
|
||||
existingAsset.ContentType = GetContentType(fileName);
|
||||
existingAsset.Version = gameVersion;
|
||||
existingAsset.UploadedAt = DateTime.UtcNow;
|
||||
|
||||
Message = $"Asset '{fileName}' updated successfully!";
|
||||
@@ -128,6 +131,7 @@ public class AssetsModel : PageModel
|
||||
IsRequired = isRequired,
|
||||
Description = description,
|
||||
ContentType = GetContentType(fileName),
|
||||
Version = gameVersion,
|
||||
UploadedAt = DateTime.UtcNow,
|
||||
DownloadedAt = DateTime.UtcNow
|
||||
};
|
||||
@@ -154,6 +158,7 @@ public class AssetsModel : PageModel
|
||||
public async Task<IActionResult> OnPostUploadZipAsync(
|
||||
IFormFile zipFile,
|
||||
string baseCategory,
|
||||
string? gameVersion,
|
||||
bool isRequired)
|
||||
{
|
||||
try
|
||||
@@ -190,6 +195,47 @@ public class AssetsModel : PageModel
|
||||
int extractedCount = 0;
|
||||
int skippedCount = 0;
|
||||
var errors = new List<string>();
|
||||
|
||||
// Try to parse manifest file if exists
|
||||
AssetManifest? manifest = null;
|
||||
using (var archive = ZipFile.OpenRead(tempZipPath))
|
||||
{
|
||||
var manifestEntry = archive.Entries.FirstOrDefault(e =>
|
||||
e.Name.Equals("manifest.json", StringComparison.OrdinalIgnoreCase) ||
|
||||
e.Name.Equals("manifest.xml", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (manifestEntry != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = manifestEntry.Open();
|
||||
using var reader = new StreamReader(stream);
|
||||
var manifestContent = await reader.ReadToEndAsync();
|
||||
|
||||
if (manifestEntry.Name.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
manifest = JsonSerializer.Deserialize<AssetManifest>(manifestContent,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
}
|
||||
|
||||
if (manifest != null)
|
||||
{
|
||||
_logger.LogInformation("Loaded manifest: Version={Version}, GameVersion={GameVersion}",
|
||||
manifest.Version, manifest.GameVersion);
|
||||
|
||||
// Override parameters from manifest if provided
|
||||
if (!string.IsNullOrEmpty(manifest.GameVersion))
|
||||
gameVersion = manifest.GameVersion;
|
||||
if (!string.IsNullOrEmpty(manifest.Category) && baseCategory == "auto")
|
||||
baseCategory = manifest.Category;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to parse manifest file, using defaults");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract ZIP and process each file
|
||||
using (var archive = ZipFile.OpenRead(tempZipPath))
|
||||
@@ -198,18 +244,31 @@ public class AssetsModel : PageModel
|
||||
{
|
||||
try
|
||||
{
|
||||
// Skip directories
|
||||
if (string.IsNullOrEmpty(entry.Name) || entry.FullName.EndsWith("/"))
|
||||
// Skip directories and manifest files
|
||||
if (string.IsNullOrEmpty(entry.Name) || entry.FullName.EndsWith("/") ||
|
||||
entry.Name.Equals("manifest.json", StringComparison.OrdinalIgnoreCase) ||
|
||||
entry.Name.Equals("manifest.xml", StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
// Determine category from path in ZIP
|
||||
// Check if this file has manifest metadata
|
||||
ManifestAsset? manifestAsset = manifest?.Assets?.FirstOrDefault(a =>
|
||||
a.File.Replace("\\", "/").Equals(entry.FullName.Replace("\\", "/"),
|
||||
StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
// Determine category from manifest or path in ZIP
|
||||
var pathParts = entry.FullName.Split('/', '\\');
|
||||
var category = baseCategory;
|
||||
var category = manifestAsset?.Category ?? baseCategory;
|
||||
|
||||
// If ZIP has folders, use first folder as subcategory
|
||||
if (pathParts.Length > 1)
|
||||
// Auto-detect if category is "auto"
|
||||
if (category == "auto")
|
||||
{
|
||||
category = Path.Combine(baseCategory, pathParts[0]);
|
||||
category = SmartDetectCategory(entry.FullName) ?? "base";
|
||||
}
|
||||
|
||||
// If ZIP has folders and no manifest, use first folder as subcategory
|
||||
if (manifestAsset == null && pathParts.Length > 1 && category != "auto")
|
||||
{
|
||||
category = Path.Combine(category, pathParts[0]);
|
||||
}
|
||||
|
||||
// Create category subdirectory
|
||||
@@ -233,6 +292,11 @@ public class AssetsModel : PageModel
|
||||
|
||||
// Build EA CDN path from ZIP structure
|
||||
var eaCdnPath = "/" + entry.FullName.Replace("\\", "/");
|
||||
|
||||
// Determine asset type from manifest or filename
|
||||
var assetType = manifestAsset?.Type ?? DetermineAssetType(fileName);
|
||||
var required = manifestAsset?.Required ?? isRequired;
|
||||
var description = manifestAsset?.Description;
|
||||
|
||||
// Check if asset already exists
|
||||
var existingAsset = await _context.GameAssets
|
||||
@@ -247,8 +311,11 @@ public class AssetsModel : PageModel
|
||||
existingAsset.Md5Hash = md5Hash;
|
||||
existingAsset.FileSha256 = sha256Hash;
|
||||
existingAsset.Category = category;
|
||||
existingAsset.IsRequired = isRequired;
|
||||
existingAsset.AssetType = assetType;
|
||||
existingAsset.IsRequired = required;
|
||||
existingAsset.Description = description;
|
||||
existingAsset.ContentType = GetContentType(fileName);
|
||||
existingAsset.Version = gameVersion;
|
||||
existingAsset.UploadedAt = DateTime.UtcNow;
|
||||
skippedCount++;
|
||||
}
|
||||
@@ -264,10 +331,11 @@ public class AssetsModel : PageModel
|
||||
Md5Hash = md5Hash,
|
||||
FileSha256 = sha256Hash,
|
||||
Category = category,
|
||||
AssetType = DetermineAssetType(fileName),
|
||||
IsRequired = isRequired,
|
||||
Description = $"Extracted from {zipFile.FileName}",
|
||||
AssetType = assetType,
|
||||
IsRequired = required,
|
||||
Description = description ?? $"Extracted from {zipFile.FileName}",
|
||||
ContentType = GetContentType(fileName),
|
||||
Version = gameVersion,
|
||||
UploadedAt = DateTime.UtcNow,
|
||||
DownloadedAt = DateTime.UtcNow
|
||||
};
|
||||
@@ -446,6 +514,36 @@ public class AssetsModel : PageModel
|
||||
};
|
||||
}
|
||||
|
||||
private string? SmartDetectCategory(string fullPath)
|
||||
{
|
||||
var lowerPath = fullPath.ToLowerInvariant();
|
||||
|
||||
// Check for known keywords in path
|
||||
if (lowerPath.Contains("car") || lowerPath.Contains("vehicle") || lowerPath.Contains("automobile"))
|
||||
return "cars";
|
||||
if (lowerPath.Contains("track") || lowerPath.Contains("circuit") || lowerPath.Contains("course"))
|
||||
return "tracks";
|
||||
if (lowerPath.Contains("audio") || lowerPath.Contains("sound") || lowerPath.Contains("music"))
|
||||
return "audio";
|
||||
if (lowerPath.Contains("texture") || lowerPath.Contains("material") || lowerPath.Contains("skin"))
|
||||
return "textures";
|
||||
if (lowerPath.Contains("ui") || lowerPath.Contains("hud") || lowerPath.Contains("menu"))
|
||||
return "ui";
|
||||
if (lowerPath.Contains("event") || lowerPath.Contains("race") || lowerPath.Contains("challenge"))
|
||||
return "events";
|
||||
if (lowerPath.Contains("dlc") || lowerPath.Contains("expansion") || lowerPath.Contains("addon"))
|
||||
return "dlc";
|
||||
if (lowerPath.Contains("update") || lowerPath.Contains("patch"))
|
||||
return "updates";
|
||||
|
||||
// Fall back to first folder name
|
||||
var pathParts = fullPath.Split('/', '\\');
|
||||
if (pathParts.Length > 1)
|
||||
return pathParts[0].ToLowerInvariant();
|
||||
|
||||
return null; // Will default to "base"
|
||||
}
|
||||
|
||||
private string DetermineAssetType(string fileName)
|
||||
{
|
||||
var extension = Path.GetExtension(fileName).ToLowerInvariant();
|
||||
@@ -467,3 +565,23 @@ public class AssetStats
|
||||
public long TotalSizeMB { get; set; }
|
||||
public int TotalDownloads { get; set; }
|
||||
}
|
||||
|
||||
// Manifest models for automatic ZIP metadata detection
|
||||
public class AssetManifest
|
||||
{
|
||||
public string? Version { get; set; }
|
||||
public string? GameVersion { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? Author { get; set; }
|
||||
public string? Category { get; set; }
|
||||
public List<ManifestAsset>? Assets { get; set; }
|
||||
}
|
||||
|
||||
public class ManifestAsset
|
||||
{
|
||||
public string File { get; set; } = string.Empty;
|
||||
public string? Category { get; set; }
|
||||
public string? Type { get; set; }
|
||||
public bool Required { get; set; } = true;
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ builder.Services.AddScoped<IDrmService, DrmService>();
|
||||
builder.Services.AddScoped<IAuthService, AuthService>();
|
||||
builder.Services.AddScoped<AssetExtractionService>();
|
||||
|
||||
// Add HttpClient for URL downloads
|
||||
builder.Services.AddHttpClient();
|
||||
|
||||
// CORS for cross-origin requests
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -13,7 +13,7 @@ using System.Reflection;
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("RR3CommunityServer")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+15e842ce855b748cc97c43261eba408067128330")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+f289cdfce9c28a229d8a00547a730c54530d68e9")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("RR3CommunityServer")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("RR3CommunityServer")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
@@ -1 +1 @@
|
||||
57cc4aab962a2ad12c842505535b2be3f034220ea319669b999c8dc52168130d
|
||||
0da7d7611f502a833d49c872526fd4633f406323449f2304a3325dd643f82480
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1 +1 @@
|
||||
{"documents":{"E:\\rr3\\RR3CommunityServer\\*":"https://raw.githubusercontent.com/ssfdre38/rr3-server/15e842ce855b748cc97c43261eba408067128330/*"}}
|
||||
{"documents":{"E:\\rr3\\RR3CommunityServer\\*":"https://raw.githubusercontent.com/ssfdre38/rr3-server/f289cdfce9c28a229d8a00547a730c54530d68e9/*"}}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
{"GlobalPropertiesHash":"gdYA/PLOQysRMD9wt3+IrqBqQw0g/GZFOcojepf8P6w=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["7Gcs8uTS1W2TjgmuuoBwaL/zy\u002B2wcKht3msEI7xtxEM=","UWedSjPPgrw4tts2Bk2ce0nYJfnBV9zMYOAjYg0PED8=","GecKXPxV0EAagvAtrRNTytwMtFCxZmgKm9sjLyEe8oI=","hf09a5b9bLKSNiG0xcVt876q/fUaHURnZieQui5FrSU=","hnhSRoeFpk3C6XWICUlX/lNip6TfbZWFYZv4weSCyrw=","fVR30KYkDSf6Wvsw9TujzlqruhwIMbw1wHxa1z/mksA=","bGtvAdvcs6Zz1qOTjdKz5gd/5jOpXDLvMjTZye3i/QI=","EoVh8vBcGohUnEMEoZuTXrpZ9uBDHT19VmDHc/D\u002Bm0I=","7gMXO5\u002Bhli7od21x4gC/qf3G6ddyyMyoSF6YFX9IaKg=","IdEjAFCVk3xZYjiEMESONot/jkvTj/gnwS5nnpGaIMc=","JVRe\u002Be2d47FunIfxVYRpqRFtljZ8gqrK3xMRy6TCd\u002BQ=","DQG0T8n9f5ohwv9akihU55D4/3WR7\u002BlDnvkdsAHHSgc=","VxDQNRQXYUU41o9SG4HrkKWR59FJIv8lmnwBolB/wE0=","x88k5Bg2fv\u002Bie1eIqFd4doOTQY0lwCNPv/5eJfhIK\u002Bw=","0Slg2/xnc5E9nXprYyph/57wQou\u002BhGSGgKchbo4aNOg="],"CachedAssets":{},"CachedCopyCandidates":{}}
|
||||
{"GlobalPropertiesHash":"gdYA/PLOQysRMD9wt3+IrqBqQw0g/GZFOcojepf8P6w=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["7Gcs8uTS1W2TjgmuuoBwaL/zy\u002B2wcKht3msEI7xtxEM=","UWedSjPPgrw4tts2Bk2ce0nYJfnBV9zMYOAjYg0PED8=","GecKXPxV0EAagvAtrRNTytwMtFCxZmgKm9sjLyEe8oI=","S5l3\u002BBR9dKGtXWyJK2BwVbV5FvygplG8\u002Byh9AE1ZYRQ=","hnhSRoeFpk3C6XWICUlX/lNip6TfbZWFYZv4weSCyrw=","fVR30KYkDSf6Wvsw9TujzlqruhwIMbw1wHxa1z/mksA=","bGtvAdvcs6Zz1qOTjdKz5gd/5jOpXDLvMjTZye3i/QI=","EoVh8vBcGohUnEMEoZuTXrpZ9uBDHT19VmDHc/D\u002Bm0I=","7gMXO5\u002Bhli7od21x4gC/qf3G6ddyyMyoSF6YFX9IaKg=","IdEjAFCVk3xZYjiEMESONot/jkvTj/gnwS5nnpGaIMc=","JVRe\u002Be2d47FunIfxVYRpqRFtljZ8gqrK3xMRy6TCd\u002BQ=","DQG0T8n9f5ohwv9akihU55D4/3WR7\u002BlDnvkdsAHHSgc=","VxDQNRQXYUU41o9SG4HrkKWR59FJIv8lmnwBolB/wE0=","x88k5Bg2fv\u002Bie1eIqFd4doOTQY0lwCNPv/5eJfhIK\u002Bw=","0Slg2/xnc5E9nXprYyph/57wQou\u002BhGSGgKchbo4aNOg="],"CachedAssets":{},"CachedCopyCandidates":{}}
|
||||
Reference in New Issue
Block a user