Compare commits
2 Commits
15e842ce85
...
dd2c23000f
| Author | SHA1 | Date | |
|---|---|---|---|
| dd2c23000f | |||
| f289cdfce9 |
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"
|
||||
});
|
||||
|
||||
@@ -66,76 +66,228 @@
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0">⬆️ Upload New Asset</h5>
|
||||
<ul class="nav nav-tabs card-header-tabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="single-tab" data-bs-toggle="tab" data-bs-target="#single" type="button" role="tab">
|
||||
📄 Single File
|
||||
</button>
|
||||
</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 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>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" enctype="multipart/form-data" asp-page-handler="Upload">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="assetFile" class="form-label">Asset File</label>
|
||||
<input type="file" class="form-control" id="assetFile" name="assetFile" required>
|
||||
<small class="text-muted">Supported: .pak, .z, .dat, .nct, .json, .xml, images, audio</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="eaCdnPath" class="form-label">EA CDN Path</label>
|
||||
<input type="text" class="form-control" id="eaCdnPath" name="eaCdnPath" placeholder="/rr3/assets/file.pak" required>
|
||||
<small class="text-muted">Path format: /rr3/category/filename.ext</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label for="category" class="form-label">Category</label>
|
||||
<select class="form-select" id="category" name="category" required>
|
||||
<option value="">Select category...</option>
|
||||
<option value="base">Base Assets</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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label for="assetType" class="form-label">Asset Type</label>
|
||||
<select class="form-select" id="assetType" name="assetType">
|
||||
<option value="Data">Data File</option>
|
||||
<option value="Texture">Texture</option>
|
||||
<option value="Audio">Audio</option>
|
||||
<option value="Model">3D Model</option>
|
||||
<option value="Config">Configuration</option>
|
||||
</select>
|
||||
</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="isRequired" name="isRequired" checked>
|
||||
<label class="form-check-label" for="isRequired">
|
||||
Required Asset (mandatory download)
|
||||
</label>
|
||||
<div class="tab-content">
|
||||
<!-- Single File Upload -->
|
||||
<div class="tab-pane fade show active" id="single" role="tabpanel">
|
||||
<form method="post" enctype="multipart/form-data" asp-page-handler="Upload">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="assetFile" class="form-label">Asset File</label>
|
||||
<input type="file" class="form-control" id="assetFile" name="assetFile" required>
|
||||
<small class="text-muted">Supported: .pak, .z, .dat, .nct, .json, .xml, images, audio</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="eaCdnPath" class="form-label">EA CDN Path</label>
|
||||
<input type="text" class="form-control" id="eaCdnPath" name="eaCdnPath" placeholder="/rr3/assets/file.pak" required>
|
||||
<small class="text-muted">Path format: /rr3/category/filename.ext</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label for="category" class="form-label">Category</label>
|
||||
<select class="form-select" id="category" name="category" required>
|
||||
<option value="">Select category...</option>
|
||||
<option value="base">Base Assets</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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label for="assetType" class="form-label">Asset Type</label>
|
||||
<select class="form-select" id="assetType" name="assetType">
|
||||
<option value="Data">Data File</option>
|
||||
<option value="Texture">Texture</option>
|
||||
<option value="Audio">Audio</option>
|
||||
<option value="Model">3D Model</option>
|
||||
<option value="Config">Configuration</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<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">
|
||||
Required Asset
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-cloud-upload"></i> Upload Asset
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- ZIP Bulk Upload -->
|
||||
<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 • 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">Include manifest.json for auto-detection • Example: cars/porsche_911.dat → /cars/porsche_911.dat</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<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>
|
||||
<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>
|
||||
</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="isRequiredZip" name="isRequired" checked>
|
||||
<label class="form-check-label" for="isRequiredZip">
|
||||
Mark as required
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="bi bi-file-zip"></i> Extract and Upload
|
||||
</button>
|
||||
</form>
|
||||
</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>
|
||||
|
||||
<!-- 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>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-cloud-upload"></i> Upload Asset
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,8 @@ using Microsoft.EntityFrameworkCore;
|
||||
using RR3CommunityServer.Data;
|
||||
using RR3CommunityServer.Models;
|
||||
using System.Security.Cryptography;
|
||||
using System.IO.Compression;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace RR3CommunityServer.Pages;
|
||||
|
||||
@@ -45,6 +47,7 @@ public class AssetsModel : PageModel
|
||||
string category,
|
||||
string assetType,
|
||||
bool isRequired,
|
||||
string gameVersion,
|
||||
string? description)
|
||||
{
|
||||
try
|
||||
@@ -107,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!";
|
||||
@@ -127,6 +131,7 @@ public class AssetsModel : PageModel
|
||||
IsRequired = isRequired,
|
||||
Description = description,
|
||||
ContentType = GetContentType(fileName),
|
||||
Version = gameVersion,
|
||||
UploadedAt = DateTime.UtcNow,
|
||||
DownloadedAt = DateTime.UtcNow
|
||||
};
|
||||
@@ -150,6 +155,236 @@ public class AssetsModel : PageModel
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostUploadZipAsync(
|
||||
IFormFile zipFile,
|
||||
string baseCategory,
|
||||
string? gameVersion,
|
||||
bool isRequired)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (zipFile == null || zipFile.Length == 0)
|
||||
{
|
||||
Message = "No ZIP file selected.";
|
||||
IsError = true;
|
||||
await OnGetAsync();
|
||||
return Page();
|
||||
}
|
||||
|
||||
if (!zipFile.FileName.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Message = "Please upload a ZIP file.";
|
||||
IsError = true;
|
||||
await OnGetAsync();
|
||||
return Page();
|
||||
}
|
||||
|
||||
// Ensure assets directory exists
|
||||
if (!Directory.Exists(_assetsBasePath))
|
||||
{
|
||||
Directory.CreateDirectory(_assetsBasePath);
|
||||
}
|
||||
|
||||
// Save ZIP to temp location
|
||||
var tempZipPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".zip");
|
||||
using (var stream = new FileStream(tempZipPath, FileMode.Create))
|
||||
{
|
||||
await zipFile.CopyToAsync(stream);
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 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;
|
||||
|
||||
// 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 = manifestAsset?.Category ?? baseCategory;
|
||||
|
||||
// Auto-detect if category is "auto"
|
||||
if (category == "auto")
|
||||
{
|
||||
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
|
||||
var categoryPath = Path.Combine(_assetsBasePath, category);
|
||||
if (!Directory.Exists(categoryPath))
|
||||
{
|
||||
Directory.CreateDirectory(categoryPath);
|
||||
}
|
||||
|
||||
// Extract file
|
||||
var fileName = entry.Name;
|
||||
var localPath = Path.Combine(categoryPath, fileName);
|
||||
|
||||
// Extract to disk
|
||||
entry.ExtractToFile(localPath, overwrite: true);
|
||||
|
||||
// Calculate hashes
|
||||
var md5Hash = await CalculateMd5Async(localPath);
|
||||
var sha256Hash = await CalculateSha256Async(localPath);
|
||||
var fileInfo = new FileInfo(localPath);
|
||||
|
||||
// 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
|
||||
.FirstOrDefaultAsync(a => a.EaCdnPath == eaCdnPath);
|
||||
|
||||
if (existingAsset != null)
|
||||
{
|
||||
// Update existing
|
||||
existingAsset.FileName = fileName;
|
||||
existingAsset.LocalPath = localPath;
|
||||
existingAsset.FileSize = fileInfo.Length;
|
||||
existingAsset.Md5Hash = md5Hash;
|
||||
existingAsset.FileSha256 = sha256Hash;
|
||||
existingAsset.Category = category;
|
||||
existingAsset.AssetType = assetType;
|
||||
existingAsset.IsRequired = required;
|
||||
existingAsset.Description = description;
|
||||
existingAsset.ContentType = GetContentType(fileName);
|
||||
existingAsset.Version = gameVersion;
|
||||
existingAsset.UploadedAt = DateTime.UtcNow;
|
||||
skippedCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create new asset
|
||||
var asset = new GameAsset
|
||||
{
|
||||
FileName = fileName,
|
||||
EaCdnPath = eaCdnPath,
|
||||
LocalPath = localPath,
|
||||
FileSize = fileInfo.Length,
|
||||
Md5Hash = md5Hash,
|
||||
FileSha256 = sha256Hash,
|
||||
Category = category,
|
||||
AssetType = assetType,
|
||||
IsRequired = required,
|
||||
Description = description ?? $"Extracted from {zipFile.FileName}",
|
||||
ContentType = GetContentType(fileName),
|
||||
Version = gameVersion,
|
||||
UploadedAt = DateTime.UtcNow,
|
||||
DownloadedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_context.GameAssets.Add(asset);
|
||||
extractedCount++;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errors.Add($"{entry.FullName}: {ex.Message}");
|
||||
_logger.LogError(ex, "Error extracting file from ZIP: {FileName}", entry.FullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save changes
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Clean up temp ZIP
|
||||
if (System.IO.File.Exists(tempZipPath))
|
||||
{
|
||||
System.IO.File.Delete(tempZipPath);
|
||||
}
|
||||
|
||||
// Build message
|
||||
if (errors.Any())
|
||||
{
|
||||
Message = $"ZIP processed: {extractedCount} new, {skippedCount} updated. Errors: {errors.Count}";
|
||||
IsError = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Message = $"ZIP extracted successfully! {extractedCount} new files, {skippedCount} updated.";
|
||||
}
|
||||
|
||||
_logger.LogInformation("ZIP uploaded: {FileName} -> {ExtractedCount} files", zipFile.FileName, extractedCount);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error processing ZIP file");
|
||||
Message = $"Error processing ZIP: {ex.Message}";
|
||||
IsError = true;
|
||||
}
|
||||
|
||||
await OnGetAsync();
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostDeleteAsync(int id)
|
||||
{
|
||||
try
|
||||
@@ -278,6 +513,49 @@ public class AssetsModel : PageModel
|
||||
_ => "application/octet-stream"
|
||||
};
|
||||
}
|
||||
|
||||
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();
|
||||
return extension switch
|
||||
{
|
||||
".png" or ".jpg" or ".jpeg" or ".pvr" or ".atlas" => "Texture",
|
||||
".mp3" or ".ogg" or ".wav" => "Audio",
|
||||
".json" or ".xml" => "Config",
|
||||
".pak" or ".dat" or ".nct" => "Data",
|
||||
_ => "Data"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class AssetStats
|
||||
@@ -287,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+f1d0d43cb70abf0fc44598a01a6250f3b7b73922")]
|
||||
[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 @@
|
||||
42ee391f7017f761577d9436f4160570943e6c116049654eb80a0c5412651e8a
|
||||
0da7d7611f502a833d49c872526fd4633f406323449f2304a3325dd643f82480
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1 +1 @@
|
||||
{"documents":{"E:\\rr3\\RR3CommunityServer\\*":"https://raw.githubusercontent.com/ssfdre38/rr3-server/f1d0d43cb70abf0fc44598a01a6250f3b7b73922/*"}}
|
||||
{"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=","AD8WKv0o3OeySN/Mlu5s1a4y3Dt/ik0jFKKHCrGjFtA=","hnhSRoeFpk3C6XWICUlX/lNip6TfbZWFYZv4weSCyrw=","fVR30KYkDSf6Wvsw9TujzlqruhwIMbw1wHxa1z/mksA=","VpFNnyDFqynPhhZPcyqeWcncA9QmAv\u002BG3ez5PxfzaTQ=","EoVh8vBcGohUnEMEoZuTXrpZ9uBDHT19VmDHc/D\u002Bm0I=","eB7z8UswjcYO/RErEzjxxHwVLVba/7iPOUH17NS53Fw=","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":{}}
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user