diff --git a/DCGEngine.sln b/DCGEngine.sln index 06ec6344..23f6ffc5 100644 --- a/DCGEngine.sln +++ b/DCGEngine.sln @@ -15,6 +15,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVSim.BattleEngine", "SVSim EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVSim.BattleEngine.Tests", "SVSim.BattleEngine.Tests\SVSim.BattleEngine.Tests.csproj", "{68F3F596-CAD5-4326-8779-AD8C7BD20CDA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVSim.ContentServer", "SVSim.ContentServer\SVSim.ContentServer.csproj", "{AC3897E1-7745-49D0-BFA1-7D859D317C07}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution 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}.Release|Any CPU.ActiveCfg = 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 EndGlobal diff --git a/SVSim.ContentServer/Program.cs b/SVSim.ContentServer/Program.cs new file mode 100644 index 00000000..cfddfb86 --- /dev/null +++ b/SVSim.ContentServer/Program.cs @@ -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=. +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(); + } +} diff --git a/SVSim.ContentServer/Properties/launchSettings.json b/SVSim.ContentServer/Properties/launchSettings.json new file mode 100644 index 00000000..06ece504 --- /dev/null +++ b/SVSim.ContentServer/Properties/launchSettings.json @@ -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" + } + } + } +} diff --git a/SVSim.ContentServer/SVSim.ContentServer.csproj b/SVSim.ContentServer/SVSim.ContentServer.csproj new file mode 100644 index 00000000..bd956c22 --- /dev/null +++ b/SVSim.ContentServer/SVSim.ContentServer.csproj @@ -0,0 +1,11 @@ + + + + net8.0 + enable + enable + true + SVSim.ContentServer + + + diff --git a/SVSim.ContentServer/appsettings.json b/SVSim.ContentServer/appsettings.json new file mode 100644 index 00000000..193f158e --- /dev/null +++ b/SVSim.ContentServer/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "Content": { + "Root": "" + } +}