feat(content-server): scaffold SVSim.ContentServer
New ASP.NET Core 8 web project, standalone (no project references). Serves the CDN-mirror tree from a configured root via UseStaticFiles mounted at /dl/, with ServeUnknownFileTypes=true + DefaultContentType=octet-stream since blob filenames are bare MD5 hashes with no extension and asset extensions (.unity3d/.acb/.usm) aren't in the default MIME map. Configuration: SVSIM_CONTENT_ROOT env var (or Content:Root in appsettings, or --Content:Root cli arg) points at the asset root containing the dl/ subdirectory. The asset root is populated by data_dumps/scripts/ content_cdn_mirror.py in the outer repo (~22 GB Eng+Jpn). Listens on port 5149 (distinct from EmulatedEntrypoint's 5148). Lightweight request logging via inline middleware emits status/method/path/ms for every request. /health returns "ok". End-to-end smoke verified against the populated working copy at 4670rPsPMVlRTd2: - Both lang tier-1s served (Eng 1013b / Jpn 955b), MD5s match live CDN. - A category manifest (bg_assetmanifest Eng) served + verified. - A 4.5 MB font asset bundle served + MD5-verified. - A Jpn voice from Sound/Jpn/ served + MD5-verified. - Hardlinked content (master_ai_ally_common.unity3d, hash shared across langs) served identically from both Resource/Eng/ and Resource/Jpn/. - 404 for missing blobs (correct). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVSim.BattleEngine", "SVSim
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVSim.BattleEngine.Tests", "SVSim.BattleEngine.Tests\SVSim.BattleEngine.Tests.csproj", "{68F3F596-CAD5-4326-8779-AD8C7BD20CDA}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVSim.BattleEngine.Tests", "SVSim.BattleEngine.Tests\SVSim.BattleEngine.Tests.csproj", "{68F3F596-CAD5-4326-8779-AD8C7BD20CDA}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVSim.ContentServer", "SVSim.ContentServer\SVSim.ContentServer.csproj", "{AC3897E1-7745-49D0-BFA1-7D859D317C07}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -49,5 +51,9 @@ Global
|
|||||||
{68F3F596-CAD5-4326-8779-AD8C7BD20CDA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{68F3F596-CAD5-4326-8779-AD8C7BD20CDA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{68F3F596-CAD5-4326-8779-AD8C7BD20CDA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{68F3F596-CAD5-4326-8779-AD8C7BD20CDA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{68F3F596-CAD5-4326-8779-AD8C7BD20CDA}.Release|Any CPU.Build.0 = Release|Any CPU
|
{68F3F596-CAD5-4326-8779-AD8C7BD20CDA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{AC3897E1-7745-49D0-BFA1-7D859D317C07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{AC3897E1-7745-49D0-BFA1-7D859D317C07}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{AC3897E1-7745-49D0-BFA1-7D859D317C07}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{AC3897E1-7745-49D0-BFA1-7D859D317C07}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
87
SVSim.ContentServer/Program.cs
Normal file
87
SVSim.ContentServer/Program.cs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
using Microsoft.Extensions.FileProviders;
|
||||||
|
|
||||||
|
namespace SVSim.ContentServer;
|
||||||
|
|
||||||
|
// SVSim.ContentServer — server #3 of the 4-server topology.
|
||||||
|
//
|
||||||
|
// Dumb static-file host serving the Shadowverse asset CDN tree. The CDN is
|
||||||
|
// plain HTTP, no auth, content-addressed by MD5 in the Resource/Sound trees
|
||||||
|
// and name-keyed under Manifest/{resver}/{lang}/{plat}/. We mirror the URL
|
||||||
|
// structure on disk and mount UseStaticFiles directly at /dl/.
|
||||||
|
//
|
||||||
|
// Required runtime config:
|
||||||
|
// SVSIM_CONTENT_ROOT env var pointing at the asset root (must contain a
|
||||||
|
// dl/ subdirectory built by data_dumps/scripts/
|
||||||
|
// content_cdn_mirror.py).
|
||||||
|
//
|
||||||
|
// Alternatively the same path may be set in appsettings.json under
|
||||||
|
// "Content:Root", or passed on the command line as --Content:Root=<path>.
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Env var takes priority — it's the canonical knob per the spec docs.
|
||||||
|
var contentRoot = Environment.GetEnvironmentVariable("SVSIM_CONTENT_ROOT")
|
||||||
|
?? builder.Configuration["Content:Root"];
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(contentRoot))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"SVSim.ContentServer: no content root configured. Set SVSIM_CONTENT_ROOT " +
|
||||||
|
"env var, Content:Root in appsettings, or pass --Content:Root=<path>.");
|
||||||
|
Environment.Exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
contentRoot = Path.GetFullPath(contentRoot);
|
||||||
|
var dlRoot = Path.Combine(contentRoot, "dl");
|
||||||
|
|
||||||
|
if (!Directory.Exists(dlRoot))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"SVSim.ContentServer: content root '{contentRoot}' does not contain a 'dl' " +
|
||||||
|
"subdirectory. Run data_dumps/scripts/content_cdn_mirror.py to populate.");
|
||||||
|
Environment.Exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
var logger = app.Services.GetRequiredService<ILogger<Program>>();
|
||||||
|
|
||||||
|
// Request logging for first-light visibility. Registered before
|
||||||
|
// UseStaticFiles so it observes the static responses too.
|
||||||
|
app.Use(async (context, next) =>
|
||||||
|
{
|
||||||
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
await next();
|
||||||
|
sw.Stop();
|
||||||
|
logger.LogInformation("{Status} {Method} {Path} ({Elapsed}ms)",
|
||||||
|
context.Response.StatusCode,
|
||||||
|
context.Request.Method,
|
||||||
|
context.Request.Path.Value,
|
||||||
|
sw.ElapsedMilliseconds);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ServeUnknownFileTypes=true is REQUIRED. Blob filenames are bare MD5
|
||||||
|
// hashes with no extension; asset/sound/movie extensions (.unity3d,
|
||||||
|
// .acb, .awb, .usm, .lz4) aren't in the default MIME map. The client
|
||||||
|
// doesn't care about Content-Type, but it does care about getting
|
||||||
|
// non-zero bytes.
|
||||||
|
app.UseStaticFiles(new StaticFileOptions
|
||||||
|
{
|
||||||
|
FileProvider = new PhysicalFileProvider(dlRoot),
|
||||||
|
RequestPath = "/dl",
|
||||||
|
ServeUnknownFileTypes = true,
|
||||||
|
DefaultContentType = "application/octet-stream",
|
||||||
|
});
|
||||||
|
|
||||||
|
app.MapGet("/health", () => "ok");
|
||||||
|
|
||||||
|
logger.LogInformation("Content root: {Root}", dlRoot);
|
||||||
|
logger.LogInformation("Serving /dl/* from this root. Manifests are under " +
|
||||||
|
"/dl/Manifest/{{resver}}/{{lang}}/{{plat}}/; blob trees are " +
|
||||||
|
"content-addressed under /dl/Resource/ and /dl/Sound/.");
|
||||||
|
|
||||||
|
app.Run();
|
||||||
|
}
|
||||||
|
}
|
||||||
15
SVSim.ContentServer/Properties/launchSettings.json
Normal file
15
SVSim.ContentServer/Properties/launchSettings.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": false,
|
||||||
|
"applicationUrl": "http://localhost:5149",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
|
"SVSIM_CONTENT_ROOT": "C:\\Users\\m\\Documents\\SVSim\\Shadowverse_Assets_Working"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
SVSim.ContentServer/SVSim.ContentServer.csproj
Normal file
11
SVSim.ContentServer/SVSim.ContentServer.csproj
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<InvariantGlobalization>true</InvariantGlobalization>
|
||||||
|
<RootNamespace>SVSim.ContentServer</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
12
SVSim.ContentServer/appsettings.json
Normal file
12
SVSim.ContentServer/appsettings.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*",
|
||||||
|
"Content": {
|
||||||
|
"Root": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user