ReboundEmulator/Controllers/ConfigAndAssetsController.cs
Moonbase 2631d3199c Implement Multiplayer Support
Implement Custom Screen Name Support
2026-10-06 19:00:15 -07:00

178 lines
7.1 KiB
C#

using Microsoft.AspNetCore.Mvc;
using NotPogo.Core;
using NotPogo.Model;
using System.IO.Compression;
using System.Net;
using System.Reflection.Metadata.Ecma335;
using System.Text;
namespace NotPogo.Controllers
{
[Route("/")]
public class ConfigAndAssetsController(ILogger<ConfigAndAssetsController> logger) : Controller
{
private readonly ILogger<ConfigAndAssetsController> _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 hostedGameClass,
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 <applet> 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 },
{ "hosted.game.class", hostedGameClass ?? "" },
{ "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<AppletDefinition> { 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<IActionResult> 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<string> BuildMergedPropertiesAsync(string file)
{
var props = new Dictionary<string, string>();
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<string, string> 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)..];
}
}
}
}