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
|
FodyWeavers.xsd
|
||||||
|
|
||||||
# Game Assets (copyrighted stuff lol)
|
# Game Assets (copyrighted stuff lol)
|
||||||
StaticFiles/
|
StaticFiles/
|
||||||
/appsettings.Development.json
|
|
||||||
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.UseAuthorization();
|
||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
app.MapFlashGateway();
|
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
},
|
},
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"applicationUrl": ""
|
"applicationUrl": "http://localhost:8080"
|
||||||
},
|
},
|
||||||
"https": {
|
"https": {
|
||||||
"commandName": "Project",
|
"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": "*",
|
"AllowedHosts": "*",
|
||||||
"GameServerPort": 11200,
|
"GameServerPort": 11200
|
||||||
"Kestrel": {
|
|
||||||
"Endpoints": {
|
|
||||||
"Http8080Local": {
|
|
||||||
"Url": "http://localhost:8080"
|
|
||||||
},
|
|
||||||
"Http80": {
|
|
||||||
"Url": "http://*:80"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user