Benefits: - More flexible - can enter any version (14.0.2, 13.5.1, etc.) - Future-proof - not limited to predefined versions - Supports auto-detection in ZIP upload (leave blank) - Regex validation: MAJOR.MINOR.PATCH or 'universal' Single upload: Required field with placeholder examples ZIP upload: Optional field (detects from manifest.json if blank) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
464 lines
26 KiB
Plaintext
464 lines
26 KiB
Plaintext
@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>
|
||
<input type="text" class="form-control" id="gameVersion" name="gameVersion"
|
||
placeholder="e.g., 15.0.0, 14.0.1, universal" required
|
||
pattern="^(\d+\.\d+\.\d+|universal)$"
|
||
title="Use format: MAJOR.MINOR.PATCH (e.g., 14.0.1) or 'universal'">
|
||
<small class="text-muted">15.0.0 (Community), 14.0.1 (EA Latest), or universal</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>
|
||
<input type="text" class="form-control" id="zipGameVersion" name="gameVersion"
|
||
placeholder="auto-detect or type version..."
|
||
pattern="^(\d+\.\d+\.\d+|universal)?$"
|
||
title="Use format: MAJOR.MINOR.PATCH (e.g., 14.0.1) or 'universal'">
|
||
<small class="text-muted">Leave blank to detect from 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"> </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]}";
|
||
}
|
||
}
|