Files
rr3-server/RR3CommunityServer/Pages/Assets.cshtml
Daniel Elliott f289cdfce9 Add ZIP bulk upload to asset manager
Features:
- Upload ZIP files with folder structure
- Automatic extraction and MD5/SHA256 calculation
- Preserve folder paths as EA CDN paths
- Auto-categorize based on file extensions
- Update existing assets automatically
- Bootstrap tabs for single/bulk upload UI
- Progress feedback (X new, Y updated)

Example: cars/porsche.dat → /cars/porsche.dat

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-20 09:42:52 -08:00

385 lines
20 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 Bulk Upload
</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 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
</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>
<!-- 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 • Existing assets updated
</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>
</div>
<div class="row">
<div class="col-md-8">
<div class="mb-3">
<label for="baseCategory" class="form-label">Base Category</label>
<select class="form-select" id="baseCategory" name="baseCategory">
<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">&nbsp;</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
</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>
</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]}";
}
}