Files
rr3-server/RR3CommunityServer/Pages/Assets.cshtml
Daniel Elliott dd2c23000f 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>
2026-02-20 09:55:05 -08:00

474 lines
27 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@page
@model RR3CommunityServer.Pages.AssetsModel
@{
ViewData["Title"] = "Asset Management";
}
<div class="container-fluid mt-4">
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1>📦 Asset Management</h1>
<p class="text-muted">Upload and manage game assets for client downloads</p>
</div>
<a href="/admin" class="btn btn-outline-secondary">← Back to Dashboard</a>
</div>
</div>
</div>
@if (!string.IsNullOrEmpty(Model.Message))
{
<div class="alert alert-@(Model.IsError ? "danger" : "success") alert-dismissible fade show" role="alert">
@Model.Message
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
}
<!-- Asset Statistics -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card border-primary">
<div class="card-body text-center">
<h3 class="text-primary">@Model.Stats.TotalAssets</h3>
<p class="mb-0 text-muted">Total Assets</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-success">
<div class="card-body text-center">
<h3 class="text-success">@Model.Stats.AvailableAssets</h3>
<p class="mb-0 text-muted">Available</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-info">
<div class="card-body text-center">
<h3 class="text-info">@Model.Stats.TotalSizeMB MB</h3>
<p class="mb-0 text-muted">Total Size</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-warning">
<div class="card-body text-center">
<h3 class="text-warning">@Model.Stats.TotalDownloads</h3>
<p class="mb-0 text-muted">Downloads</p>
</div>
</div>
</div>
</div>
<!-- Upload Asset Section -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header bg-primary text-white">
<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">
<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>
<!-- 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">&nbsp;</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>
</div>
</div>
<!-- Asset List -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
<h5 class="mb-0">📋 Asset Inventory</h5>
<div>
<button class="btn btn-sm btn-outline-light" onclick="refreshAssets()">
<i class="bi bi-arrow-clockwise"></i> Refresh
</button>
<form method="post" asp-page-handler="GenerateManifest" class="d-inline">
<button type="submit" class="btn btn-sm btn-success">
<i class="bi bi-file-text"></i> Generate Manifest
</button>
</form>
</div>
</div>
<div class="card-body">
@if (!Model.Assets.Any())
{
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> No assets uploaded yet. Use the form above to upload your first asset.
</div>
}
else
{
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>File Name</th>
<th>EA CDN Path</th>
<th>Category</th>
<th>Type</th>
<th>Size</th>
<th>MD5</th>
<th>Downloads</th>
<th>Required</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var asset in Model.Assets)
{
<tr>
<td>
<strong>@asset.FileName</strong>
@if (!string.IsNullOrEmpty(asset.Description))
{
<br><small class="text-muted">@asset.Description</small>
}
</td>
<td><code>@asset.EaCdnPath</code></td>
<td><span class="badge bg-secondary">@asset.Category</span></td>
<td><span class="badge bg-info">@asset.AssetType</span></td>
<td>@FormatFileSize(asset.FileSize)</td>
<td>
<code class="small">@(asset.Md5Hash?.Substring(0, 8) ?? "N/A")...</code>
@if (!string.IsNullOrEmpty(asset.Md5Hash))
{
<button class="btn btn-sm btn-outline-secondary" onclick="copyToClipboard('@asset.Md5Hash')">
<i class="bi bi-clipboard"></i>
</button>
}
</td>
<td>@asset.AccessCount</td>
<td>
@if (asset.IsRequired)
{
<span class="badge bg-danger">Required</span>
}
else
{
<span class="badge bg-secondary">Optional</span>
}
</td>
<td>
<div class="btn-group btn-group-sm">
<a href="/content/api@asset.EaCdnPath" class="btn btn-outline-primary" target="_blank" title="Download">
<i class="bi bi-download"></i>
</a>
<form method="post" asp-page-handler="Delete" asp-route-id="@asset.Id" class="d-inline"
onsubmit="return confirm('Delete @asset.FileName?')">
<button type="submit" class="btn btn-outline-danger" title="Delete">
<i class="bi bi-trash"></i>
</button>
</form>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>
</div>
</div>
</div>
<!-- How Nimble SDK Downloads Assets -->
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header bg-info text-white">
<h5 class="mb-0"> Nimble SDK Asset Download System</h5>
</div>
<div class="card-body">
<h6>How RR3 Downloads Assets:</h6>
<ol>
<li><strong>Game Startup:</strong> UnpackAssetsActivity extracts bundled APK assets</li>
<li><strong>Manifest Request:</strong> Game calls <code>GET /content/api/manifest</code></li>
<li><strong>Verification:</strong> Compares local assets with manifest (MD5 checksums)</li>
<li><strong>Download Missing:</strong> Calls <code>GET /content/api/[asset-path]</code> for missing files</li>
<li><strong>Storage:</strong> Saves to <code>/external/storage/apk/</code> directory</li>
<li><strong>Launch Game:</strong> All required assets present, game starts</li>
</ol>
<h6 class="mt-3">Asset Manifest Format:</h6>
<pre><code>{
"resultCode": 0,
"message": "Success",
"data": [
{
"path": "/rr3/base/game_data.pak",
"md5": "a1b2c3d4e5f6...",
"compressedSize": 1048576,
"uncompressedSize": 2097152,
"category": "base"
}
]
}</code></pre>
<h6 class="mt-3">Nimble SDK Authentication Headers:</h6>
<ul>
<li><code>EAM-SESSION</code> - Session UUID</li>
<li><code>EAM-USER-ID</code> - User identifier</li>
<li><code>EA-SELL-ID</code> - Marketplace (e.g., GOOGLE_PLAY)</li>
<li><code>SDK-VERSION</code> - Nimble SDK version</li>
</ul>
<div class="alert alert-warning mt-3">
<strong>Important:</strong> Assets must have correct MD5 hashes or the game will reject them and re-download.
</div>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
<script>
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
alert('Copied to clipboard: ' + text);
});
}
function refreshAssets() {
location.reload();
}
</script>
}
@functions {
private string FormatFileSize(long bytes)
{
string[] sizes = { "B", "KB", "MB", "GB" };
double len = bytes;
int order = 0;
while (len >= 1024 && order < sizes.Length - 1)
{
order++;
len = len / 1024;
}
return $"{len:0.##} {sizes[order]}";
}
}