ReboundEmulator/Controllers/ConfigAndAssetsController.cs
Moonbase 84587f6ce4 Implement Multi Applet Support To Game Page
Implement Experimental Pogo Bowl Support (and in turn Leaderboard games in general)
Remove "StaticFiles" In Favor Of App Base Directory Storage
New `logn` Sequence
Fixed Incorrect Jackpot Result Response (although it did uncover lost media with spin.rslt=1)
2026-09-28 19:39:01 -07:00

189 lines
7.6 KiB
C#

using Microsoft.AspNetCore.Mvc;
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,
bool isSingleApplet = true)
{
string baseUrl = $"{Request.Scheme}://{Request.Host.Value}";
string jarUrl = $"{baseUrl}/get-jar?game={game}&patchName=" + (!string.IsNullOrEmpty(patchName) ? patchName : "");
// 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;
var gameApplet = new AppletDefinition
{
Name = "game",
Code = appletClass,
Codebase = baseUrl,
Archive = jarUrl,
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", "NotPogoUser" },
{ "sourceid", "" },
{ "launchPage", "" },
{ "lkey", "" },
{ "cnid", "" },
{ "isSingleApplet", isSingleApplet ? "true" : "false" },
{ "browserInfo", "" },
{ "debug", "true" },
{ "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 = jarUrl,
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("/get-jar")]
public IActionResult GetAppletCode(string game, string patchName)
{
string jarName = game + "-en_US" + ((!string.IsNullOrEmpty(patchName)) ? "-" + patchName : "") + ".jar";
_logger.LogInformation("Serving Jar - {JarName}", jarName);
var path = Path.Combine(AppContext.BaseDirectory, "StaticFiles", "JARs", jarName);
if (!System.IO.File.Exists(path))
return NotFound("Applet Not Found");
else
return File(System.IO.File.ReadAllBytes(path), "application/x-java-archive", fileDownloadName: $"{game}.jar");
}
[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)..];
}
}
}
}