Files
SVSimServer/SVSim.ContentServer/Program.cs
gamer147 4acc38b4af 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>
2026-06-11 10:29:37 -04:00

88 lines
3.5 KiB
C#

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();
}
}