using Microsoft.AspNetCore.Mvc; using ReboundEmulator.Core; using ReboundEmulator.Model; using System.IO.Compression; using System.Net; using System.Reflection.Metadata.Ecma335; using System.Text; namespace ReboundEmulator.Controllers { [Route("/")] public class ConfigAndAssetsController(ILogger logger) : Controller { private readonly ILogger _logger = logger; private static readonly string ConfigsRoot = Path.Combine(AppContext.BaseDirectory, "StaticFiles", "Configs"); private static readonly string MasterPropsPath = Path.Combine(ConfigsRoot, "master.properties"); public IActionResult Index() => Ok("Not Pogo Is Running!"); // might aswell provide a minimal way to play the games supported [HttpGet] [Route("/play-game")] public IActionResult GamePage(string? name, string game, string patchName, string appletClass, string site, string locale, string scrn = null!, // user will get a username assigned if screenName is not set bool isSingleApplet = true) { string baseUrl = $"{Request.Scheme}://{Request.Host.Value}"; // Games like waterwheel/bowling ship with isSingleApplet=true: the compiled game // code draws its own chat panel as a CHILD COMPONENT of GameHostApplet -- confirmed // via a real log: "GameHostApplet-ChatPanel,584,0,192x408", a subcomponent positioned // at x=584 inside one 776-wide canvas, not a second applet instance. Giving that // applet anything narrower than 776 clips its self-drawn chat panel off-screen, // which is exactly the "chat doesn't show up" bug -- so single-applet mode has to // stay one 776-wide tag with no second one alongside it. int gameWidth = isSingleApplet ? 776 : 564; string lkey = Sessions.Issue(scrn); // since there isn't a database var gameApplet = new AppletDefinition { Name = "game", Code = appletClass, Codebase = baseUrl, Archive = $"{baseUrl}/v/19.1.0.25/applet/{game}/{game}-en_US.jar", Width = gameWidth, Height = 506, Parameters = new() { { "name", game }, { "standalone", "false" }, { "type", game }, { "config", $"client-props.jsp?game={game}" }, { "locale", locale }, { "site", site }, { "rkey", "" }, { "unid", "" }, { "scrn", scrn! }, { "sourceid", "" }, { "launchPage", "" }, { "lkey", lkey }, { "cnid", "" }, { "isSingleApplet", isSingleApplet ? "true" : "false" }, { "browserInfo", "" }, { "debug", "false" }, { "fullscreen.enabled", "false" }, { "fullscreen.supported", "false" } } }; var applets = new List { gameApplet }; if (!isSingleApplet) { // Only for games actually captured running the split layout (e.g. Lottso // Bingo's combined-game-chat.html) -- a genuinely separate ShellApplet instance, // packaged in the same jar, params matching that capture exactly: name, ugifts, // applet-version, and notably NO "config" param. How it gets its properties // without one is still an open question carried over from earlier research // (possibly LiveConnect/JS, possibly it reads what the game applet already // loaded) -- untested here. applets.Add(new AppletDefinition { Name = "chat", Code = "com.pogo.game.client2.shell.ShellApplet", Codebase = baseUrl, Archive = $"{baseUrl}/v/19.1.0.25/applet/{game}/{game}-en_US.jar", Width = 212, Height = 506, Parameters = new() { { "name", "chat" }, { "ugifts", "0" }, { "applet-version", "19.1.0.25" } } }); } var gamePage = new GamePage { GameName = name ?? game, InternalName = game, PatchName = patchName, Applets = applets }; return View(gamePage); } [HttpGet] [Route("/blank.html")] public IActionResult GetEmptyPage() => Ok(); [HttpGet] [Route("/client-props.jsp")] public async Task GetConfigAsync(string game) { string merged = await BuildMergedPropertiesAsync(game); // client props needs to be gzip compressed for whatever reason (probably to throw off dumpers or something idk) byte[] compressed; using (var ms = new MemoryStream()) { using (var gzip = new GZipStream(ms, CompressionLevel.Fastest, leaveOpen: true)) { await gzip.WriteAsync(Encoding.UTF8.GetBytes(merged)); } compressed = ms.ToArray(); } return File(compressed, "text/plain"); } private async Task BuildMergedPropertiesAsync(string file) { var props = new Dictionary(); if (System.IO.File.Exists(MasterPropsPath)) await LoadPropertiesIntoAsync(MasterPropsPath, props); else _logger.LogWarning("Master properties file missing at {Path}", MasterPropsPath); var gamePropsPath = Path.Combine(ConfigsRoot, file + ".properties"); if (System.IO.File.Exists(gamePropsPath)) await LoadPropertiesIntoAsync(gamePropsPath, props); else _logger.LogWarning("Game-specific properties file missing at {Path}", gamePropsPath); var sb = new StringBuilder(); foreach (var (key, value) in props) sb.Append(key).Append('=').Append(value).Append('\n'); return sb.ToString(); } private static async Task LoadPropertiesIntoAsync(string path, Dictionary into) { foreach (var rawLine in await System.IO.File.ReadAllLinesAsync(path)) { var line = rawLine.Trim(); if (line.Length == 0 || line.StartsWith('#')) continue; int idx = line.IndexOf('='); if (idx <= 0) continue; into[line[..idx].Trim()] = line[(idx + 1)..]; } } } }