Compare commits
No commits in common. "f6e304929fe03fbec7074521da617b784d0f068a" and "574bceafecc4f929b7fa99a2f26c63ead4121298" have entirely different histories.
f6e304929f
...
574bceafec
3
.gitignore
vendored
3
.gitignore
vendored
@ -363,5 +363,4 @@ MigrationBackup/
|
||||
FodyWeavers.xsd
|
||||
|
||||
# Game Assets (copyrighted stuff lol)
|
||||
StaticFiles/
|
||||
/appsettings.Development.json
|
||||
StaticFiles/
|
||||
221
Amf/Amf0.cs
221
Amf/Amf0.cs
@ -1,221 +0,0 @@
|
||||
// Minimal AMF0 codec + Flash Remoting packet format (what mx.remoting / NetServices speaks).
|
||||
// Reference: Adobe "AMF0 File Format Specification" + the AMF remoting packet envelope.
|
||||
//
|
||||
// .NET mapping:
|
||||
// number (0x00) <-> double (any .NET numeric type is written as a double)
|
||||
// boolean (0x01) <-> bool
|
||||
// string (0x02 / 0x0C long) <-> string
|
||||
// object (0x03) <-> Dictionary<string, object?> (writer also accepts POCOs / anonymous objects)
|
||||
// null (0x05), undefined (0x06) -> null
|
||||
// reference(0x07) -> the previously read object/array
|
||||
// ecma arr (0x08) -> Dictionary<string, object?>
|
||||
// strict (0x0A) <-> List<object?> (writer accepts any IEnumerable that isn't a string)
|
||||
// date (0x0B) <-> DateTime (UTC)
|
||||
// xml (0x0F) -> string
|
||||
// typed obj(0x10) -> Dictionary<string, object?> with the class name under "__className"
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace NotPogo.Amf;
|
||||
|
||||
public sealed record AmfHeader(string Name, bool MustUnderstand, object? Value);
|
||||
public sealed record AmfMessage(string Target, string Response, object? Body);
|
||||
|
||||
public sealed class AmfPacket
|
||||
{
|
||||
public ushort Version { get; set; } // Flash 8 / AS2 remoting sends 0
|
||||
public List<AmfHeader> Headers { get; } = new();
|
||||
public List<AmfMessage> Messages { get; } = new();
|
||||
|
||||
public static AmfPacket Read(byte[] data)
|
||||
{
|
||||
var r = new Amf0Reader(data);
|
||||
var p = new AmfPacket { Version = r.U16() };
|
||||
|
||||
int headerCount = r.U16();
|
||||
for (int i = 0; i < headerCount; i++)
|
||||
{
|
||||
string name = r.Utf8(r.U16());
|
||||
bool must = r.U8() != 0;
|
||||
r.U32(); // byte length (may be 0xFFFFFFFF = unknown); not needed
|
||||
r.ResetReferences(); // each header/body has its own reference table
|
||||
p.Headers.Add(new AmfHeader(name, must, r.ReadValue()));
|
||||
}
|
||||
|
||||
int messageCount = r.U16();
|
||||
for (int i = 0; i < messageCount; i++)
|
||||
{
|
||||
string target = r.Utf8(r.U16()); // e.g. "com.pogo.flash.FlashRoomsFeed.generateFeed"
|
||||
string response = r.Utf8(r.U16()); // e.g. "/1"
|
||||
r.U32();
|
||||
r.ResetReferences();
|
||||
p.Messages.Add(new AmfMessage(target, response, r.ReadValue()));
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
public byte[] Write()
|
||||
{
|
||||
var w = new Amf0Writer();
|
||||
w.U16(Version);
|
||||
|
||||
w.U16((ushort)Headers.Count);
|
||||
foreach (var h in Headers)
|
||||
{
|
||||
w.ShortString(h.Name);
|
||||
w.U8((byte)(h.MustUnderstand ? 1 : 0));
|
||||
WriteSized(w, h.Value);
|
||||
}
|
||||
|
||||
w.U16((ushort)Messages.Count);
|
||||
foreach (var m in Messages)
|
||||
{
|
||||
w.ShortString(m.Target); // e.g. "/1/onResult"
|
||||
w.ShortString(m.Response); // "null" for replies
|
||||
WriteSized(w, m.Body);
|
||||
}
|
||||
return w.ToArray();
|
||||
}
|
||||
|
||||
// Values are prefixed with their byte length, so encode separately first.
|
||||
static void WriteSized(Amf0Writer w, object? value)
|
||||
{
|
||||
var inner = new Amf0Writer();
|
||||
inner.WriteValue(value);
|
||||
var bytes = inner.ToArray();
|
||||
w.U32((uint)bytes.Length);
|
||||
w.Raw(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class Amf0Reader
|
||||
{
|
||||
readonly byte[] _d;
|
||||
int _p;
|
||||
readonly List<object> _refs = new();
|
||||
|
||||
public Amf0Reader(byte[] data) => _d = data;
|
||||
|
||||
public void ResetReferences() => _refs.Clear();
|
||||
|
||||
public byte U8() => _d[_p++];
|
||||
public ushort U16() { var v = BinaryPrimitives.ReadUInt16BigEndian(_d.AsSpan(_p)); _p += 2; return v; }
|
||||
public uint U32() { var v = BinaryPrimitives.ReadUInt32BigEndian(_d.AsSpan(_p)); _p += 4; return v; }
|
||||
double F64() { var v = BinaryPrimitives.ReadDoubleBigEndian(_d.AsSpan(_p)); _p += 8; return v; }
|
||||
public string Utf8(int len) { var s = Encoding.UTF8.GetString(_d, _p, len); _p += len; return s; }
|
||||
|
||||
public object? ReadValue()
|
||||
{
|
||||
byte marker = U8();
|
||||
switch (marker)
|
||||
{
|
||||
case 0x00: return F64();
|
||||
case 0x01: return U8() != 0;
|
||||
case 0x02: return Utf8(U16());
|
||||
case 0x03: { var o = new Dictionary<string, object?>(); _refs.Add(o); ReadProps(o); return o; }
|
||||
case 0x05: return null; // null
|
||||
case 0x06: return null; // undefined
|
||||
case 0x07: return _refs[U16()]; // reference
|
||||
case 0x08: // ECMA (associative) array
|
||||
{
|
||||
U32(); // approximate count; ignore
|
||||
var o = new Dictionary<string, object?>(); _refs.Add(o); ReadProps(o); return o;
|
||||
}
|
||||
case 0x0A: // strict array
|
||||
{
|
||||
uint n = U32();
|
||||
var list = new List<object?>((int)Math.Min(n, 4096)); _refs.Add(list);
|
||||
for (uint i = 0; i < n; i++) list.Add(ReadValue());
|
||||
return list;
|
||||
}
|
||||
case 0x0B: // date: ms since epoch + tz (ignored)
|
||||
{
|
||||
double ms = F64(); U16();
|
||||
return DateTime.UnixEpoch.AddMilliseconds(ms);
|
||||
}
|
||||
case 0x0C: return Utf8((int)U32()); // long string
|
||||
case 0x0F: return Utf8((int)U32()); // XML document (as text)
|
||||
case 0x10: // typed object
|
||||
{
|
||||
string cls = Utf8(U16());
|
||||
var o = new Dictionary<string, object?> { ["__className"] = cls }; _refs.Add(o); ReadProps(o); return o;
|
||||
}
|
||||
case 0x11: throw new NotSupportedException("AMF3 value inside AMF0 (Flash 9+ only; AS2 never sends this)");
|
||||
default: throw new FormatException($"Unknown AMF0 marker 0x{marker:X2} at offset {_p - 1}");
|
||||
}
|
||||
}
|
||||
|
||||
// key/value pairs until an empty key followed by the object-end marker 0x09
|
||||
void ReadProps(Dictionary<string, object?> o)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
string key = Utf8(U16());
|
||||
if (key.Length == 0 && _d[_p] == 0x09) { _p++; return; }
|
||||
o[key] = ReadValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class Amf0Writer
|
||||
{
|
||||
readonly MemoryStream _s = new();
|
||||
|
||||
public byte[] ToArray() => _s.ToArray();
|
||||
public void Raw(byte[] b) => _s.Write(b);
|
||||
public void U8(byte v) => _s.WriteByte(v);
|
||||
public void U16(ushort v) { Span<byte> b = stackalloc byte[2]; BinaryPrimitives.WriteUInt16BigEndian(b, v); _s.Write(b); }
|
||||
public void U32(uint v) { Span<byte> b = stackalloc byte[4]; BinaryPrimitives.WriteUInt32BigEndian(b, v); _s.Write(b); }
|
||||
void F64(double v) { Span<byte> b = stackalloc byte[8]; BinaryPrimitives.WriteDoubleBigEndian(b, v); _s.Write(b); }
|
||||
|
||||
public void ShortString(string s)
|
||||
{
|
||||
var b = Encoding.UTF8.GetBytes(s);
|
||||
if (b.Length > ushort.MaxValue) throw new ArgumentException("string too long for a short string");
|
||||
U16((ushort)b.Length); _s.Write(b);
|
||||
}
|
||||
|
||||
public void WriteValue(object? v)
|
||||
{
|
||||
switch (v)
|
||||
{
|
||||
case null: U8(0x05); return;
|
||||
case bool b: U8(0x01); U8((byte)(b ? 1 : 0)); return;
|
||||
case string s:
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(s);
|
||||
if (bytes.Length <= ushort.MaxValue) { U8(0x02); U16((ushort)bytes.Length); }
|
||||
else { U8(0x0C); U32((uint)bytes.Length); }
|
||||
_s.Write(bytes); return;
|
||||
}
|
||||
case byte or sbyte or short or ushort or int or uint or long or ulong or float or double or decimal:
|
||||
U8(0x00); F64(Convert.ToDouble(v)); return;
|
||||
case Enum e: U8(0x00); F64(Convert.ToDouble(e)); return;
|
||||
case DateTime dt:
|
||||
U8(0x0B); F64((dt.ToUniversalTime() - DateTime.UnixEpoch).TotalMilliseconds); U16(0); return;
|
||||
case IDictionary dict:
|
||||
U8(0x03);
|
||||
foreach (DictionaryEntry kv in dict) { ShortString(kv.Key.ToString()!); WriteValue(kv.Value); }
|
||||
EndObject(); return;
|
||||
case IEnumerable list:
|
||||
{
|
||||
var items = list.Cast<object?>().ToList();
|
||||
U8(0x0A); U32((uint)items.Count);
|
||||
foreach (var item in items) WriteValue(item);
|
||||
return;
|
||||
}
|
||||
default: // POCO / anonymous object -> anonymous AMF object
|
||||
U8(0x03);
|
||||
foreach (var prop in v.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
if (prop.GetIndexParameters().Length != 0) continue;
|
||||
ShortString(prop.Name); WriteValue(prop.GetValue(v));
|
||||
}
|
||||
EndObject(); return;
|
||||
}
|
||||
}
|
||||
|
||||
void EndObject() { U16(0); U8(0x09); }
|
||||
}
|
||||
@ -1,98 +0,0 @@
|
||||
// /flashservices/gateway endpoint for roomsel.swf (com.pogo.roomsel.RoomTabs).
|
||||
// Register in Program.cs: app.MapFlashGateway();
|
||||
|
||||
namespace NotPogo.Amf;
|
||||
|
||||
public static class FlashGateway
|
||||
{
|
||||
// "Service.method" -> handler(args). Args arrive as AMF0 values: strings, doubles, etc.
|
||||
static readonly Dictionary<string, Func<List<object?>, object?>> Handlers = new()
|
||||
{
|
||||
// RoomTabs.getRoomFeed -> generateFeed(gameCode, siteCode)
|
||||
["com.pogo.flash.FlashRoomsFeed.generateFeed"] = args =>
|
||||
{
|
||||
string gameCode = Str(args, 0), siteCode = Str(args, 1);
|
||||
return new
|
||||
{
|
||||
errorMsg = (string?)null, // non-null = SWF shows the error
|
||||
tabs = new[] { new { tabId = 1, index = 0 } },
|
||||
rooms = new[]
|
||||
{
|
||||
// shown only if tabId == selected tab AND ruleId == gRuleId (omit ruleId if noruleid)
|
||||
// clickable only if numPeople < softCapacity && live
|
||||
new { key = "wr_classic_1", humanName = "NotPogo WR Classic", numPeople = 0, softCapacity = 50, live = true, tabId = 1, ruleId = "classic" },
|
||||
new { key = "wr_rally_1", humanName = "NotPogo WR Rally", numPeople = 0, softCapacity = 50, live = true, tabId = 1, ruleId = "rally" },
|
||||
new { key = "wr_advanced_1", humanName = "NotPogo WR Expert", numPeople = 0, softCapacity = 50, live = true, tabId = 1, ruleId = "advanced" },
|
||||
},
|
||||
rules = Array.Empty<object>(), // stored, never read by the SWF
|
||||
ruleTabs = new[]
|
||||
{
|
||||
new { id = "classic", index = 0 },
|
||||
new { id = "rally", index = 1 },
|
||||
new { id = "advanced", index = 2 },
|
||||
}, // [{ id = <ruleId>, index = n }] -> ruleIdx for Play Now
|
||||
};
|
||||
},
|
||||
|
||||
// RoomTabs.goRoom -> connectRoom(roomKey, gameCode, siteCode, lkey)
|
||||
["com.pogo.flash.FlashRoomConnect.connectRoom"] = args =>
|
||||
RoomInfo(Str(args, 0)),
|
||||
|
||||
// RoomTabs.playNow -> playNow(gameCode, siteCode, lkey, tabIdx, ruleIdx)
|
||||
["com.pogo.flash.FlashRoomConnect.playNow"] = args =>
|
||||
RoomInfo("wr_room_1"), // pick a room server-side
|
||||
};
|
||||
|
||||
// roomInfo becomes FSCommand goRoom "humanName '..' key '..' host '..' port '..'"; the exe connects TCP to host:port
|
||||
static object RoomInfo(string roomKey) => new
|
||||
{
|
||||
errorMsg = (string?)null, // null or "NO ERROR" = success
|
||||
roomInfo = new { humanName = "NotPogo Room", key = roomKey, host = "downld.pogo.com", port = 11200 },
|
||||
};
|
||||
|
||||
public static void MapFlashGateway(this IEndpointRouteBuilder app)
|
||||
{
|
||||
app.MapPost("/flashservices/gateway", async (HttpContext ctx, ILoggerFactory lf) =>
|
||||
{
|
||||
var log = lf.CreateLogger("FlashGateway");
|
||||
using var ms = new MemoryStream();
|
||||
await ctx.Request.Body.CopyToAsync(ms);
|
||||
|
||||
var request = AmfPacket.Read(ms.ToArray());
|
||||
var reply = new AmfPacket { Version = request.Version };
|
||||
|
||||
// Flash may batch several calls into one POST; answer each, echoing its response URI.
|
||||
foreach (var msg in request.Messages)
|
||||
{
|
||||
var args = msg.Body as List<object?> ?? new List<object?>();
|
||||
log.LogInformation("AMF call {Target}({Args}) -> {Response}", msg.Target, string.Join(", ", args), msg.Response);
|
||||
try
|
||||
{
|
||||
if (!Handlers.TryGetValue(msg.Target, out var handler))
|
||||
throw new InvalidOperationException($"No such service method: {msg.Target}");
|
||||
reply.Messages.Add(new AmfMessage(msg.Response + "/onResult", "null", handler(args)));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.LogWarning(ex, "AMF call {Target} failed", msg.Target);
|
||||
// mx.remoting turns this into a Fault; "description" becomes faultstring
|
||||
reply.Messages.Add(new AmfMessage(msg.Response + "/onStatus", "null", new
|
||||
{
|
||||
level = "error",
|
||||
code = "SERVER.PROCESSING",
|
||||
description = ex.Message,
|
||||
details = "",
|
||||
type = ex.GetType().Name,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
var bytes = reply.Write();
|
||||
ctx.Response.ContentType = "application/x-amf";
|
||||
ctx.Response.ContentLength = bytes.Length;
|
||||
await ctx.Response.Body.WriteAsync(bytes);
|
||||
});
|
||||
}
|
||||
|
||||
static string Str(List<object?> a, int i) => i < a.Count ? a[i]?.ToString() ?? "" : "";
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace NotPogo.Controllers
|
||||
{
|
||||
public class CDLController : Controller
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("/cdl/cdl-info.jsp")]
|
||||
public IResult CDLInfo(string lkey)
|
||||
{
|
||||
// this is just to satisfy download clients
|
||||
if (string.IsNullOrEmpty(lkey))
|
||||
return Results.Text("");
|
||||
else
|
||||
return Results.Text("error none screenname 'NotPogoUser' site 'pogo' tokens 9999 enti 'oava 1 obad 1'");
|
||||
}
|
||||
|
||||
[HttpGet("/cdl/1/login")]
|
||||
public IResult CDLLogin()
|
||||
{
|
||||
// more satisfying
|
||||
return Results.Text($"<LoginResponse error=\"none\">\n<LoginSessionKey>{Guid.NewGuid()}</LoginSessionKey></LoginResponse>", "text/xml");
|
||||
}
|
||||
|
||||
[HttpGet("/cdl/1/cdl-clientprops")]
|
||||
public IActionResult CDLClientProps(string game)
|
||||
{
|
||||
string pathToProps = Path.Combine(AppContext.BaseDirectory, "StaticFiles", "Configs", "CDL", $"{game}.properties");
|
||||
|
||||
if (System.IO.File.Exists(pathToProps))
|
||||
return Ok(System.IO.File.ReadAllText(pathToProps));
|
||||
else
|
||||
return Ok(); // don't send anything since we don't have the config for it on the server yet
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -45,7 +45,6 @@ namespace NotPogo
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
app.MapFlashGateway();
|
||||
|
||||
app.Run();
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": ""
|
||||
"applicationUrl": "http://localhost:8080"
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
|
||||
10
appsettings.Development.json
Normal file
10
appsettings.Development.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"GameServerPort": 11200
|
||||
}
|
||||
@ -6,15 +6,5 @@
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"GameServerPort": 11200,
|
||||
"Kestrel": {
|
||||
"Endpoints": {
|
||||
"Http8080Local": {
|
||||
"Url": "http://localhost:8080"
|
||||
},
|
||||
"Http80": {
|
||||
"Url": "http://*:80"
|
||||
}
|
||||
}
|
||||
}
|
||||
"GameServerPort": 11200
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user