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=. 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=."); 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>(); // 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(); } }