Files
rr3-server/RR3CommunityServer/Pages/Assets.cshtml
Daniel Elliott 5d2c3bf880 Add asset management system
- Created Assets.cshtml and Assets.cshtml.cs for admin panel
- Upload assets with MD5/SHA256 hash calculation
- Generate asset manifests in RR3 format (tab-separated)
- Integrated with Nimble SDK asset download system
- Updated GameAsset model with IsRequired, UploadedAt, Description
- Added navigation link in _Layout.cshtml
- Supports categories: base, cars, tracks, audio, textures, UI, DLC
- Asset download endpoint at /content/api/{assetPath}
- Manifest endpoint at /content/api/manifest

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-19 15:16:43 -08:00

322 lines
16 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">
<h5 class="mb-0">⬆️ Upload New Asset</h5>
</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">&nbsp;</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>
</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>
</form>
</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]}";
}
}