Initial commit: Ananta CBT 4229938 private server with all fixes
This commit is contained in:
commit
c84135c357
286 files changed
+2214315
No files matched your search
@@ -0,0 +1,179 @@
|
||||
// rpc_serializer.js
|
||||
// Schema-driven serializer that mirrors the client's RPCSerializeBase.lua
|
||||
// exactly. The offline server fills plain JS objects; this turns them into
|
||||
// the wire bytes the client expects. No hand-built byte buffers.
|
||||
//
|
||||
// Marker convention (from RPCSerializeBase.lua):
|
||||
// SerializeObjectMarkNull = 0x00
|
||||
// SerializeObjectMarkCommon = 0xFF
|
||||
// WriteComplex(null,nullable) -> writes 0x00 ; present -> 0xFF + fields
|
||||
// WriteList(null,nullable) -> 0x00 ; present -> 0xFF + int32 count + items
|
||||
// WriteStruct -> NO marker, fields written directly
|
||||
// WriteBuffer(null,nullable) -> 0x00 ; present -> 0xFF + buffer
|
||||
//
|
||||
// A "schema" is the field list extracted from the client's Reader[N].
|
||||
// Each field: {name, kind, op?, ref?, item?}
|
||||
// kind: prim | struct | complex | buffer | list | dict
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
class Writer {
|
||||
constructor() { this.chunks = []; }
|
||||
_push(b) { this.chunks.push(b); }
|
||||
WriteByte(v) { const b = Buffer.alloc(1); b.writeUInt8(v & 0xff, 0); this._push(b); }
|
||||
WriteBoolean(v) { this.WriteByte(v ? 1 : 0); }
|
||||
WriteInt16(v) { const b = Buffer.alloc(2); b.writeInt16LE(v | 0, 0); this._push(b); }
|
||||
WriteUInt16(v) { const b = Buffer.alloc(2); b.writeUInt16LE(v & 0xffff, 0); this._push(b); }
|
||||
WriteInt32(v) { const b = Buffer.alloc(4); b.writeInt32LE(v | 0, 0); this._push(b); }
|
||||
WriteUInt32(v) { const b = Buffer.alloc(4); b.writeUInt32LE(v >>> 0, 0); this._push(b); }
|
||||
WriteInt64(v) { const b = Buffer.alloc(8); b.writeBigInt64LE(BigInt(v || 0), 0); this._push(b); }
|
||||
WriteUInt64(v) { const b = Buffer.alloc(8); b.writeBigUInt64LE(BigInt(v || 0), 0); this._push(b); }
|
||||
WriteSingle(v) { const b = Buffer.alloc(4); b.writeFloatLE(v || 0, 0); this._push(b); }
|
||||
WriteDouble(v) { const b = Buffer.alloc(8); b.writeDoubleLE(v || 0, 0); this._push(b); }
|
||||
WriteString(v) {
|
||||
// client string format: byte present-marker + int32 length + utf8 bytes
|
||||
if (v === null || v === undefined) { this.WriteByte(0x00); return; }
|
||||
const s = Buffer.from(String(v), "utf8");
|
||||
this.WriteByte(0xFF);
|
||||
this.WriteInt32(s.length);
|
||||
this._push(s);
|
||||
}
|
||||
WriteRawBuffer(buf) { this._push(Buffer.from(buf)); }
|
||||
toBuffer() { return Buffer.concat(this.chunks); }
|
||||
}
|
||||
|
||||
const PRIM = {
|
||||
ReadByte: "WriteByte", ReadBoolean: "WriteBoolean",
|
||||
ReadInt16: "WriteInt16", ReadUInt16: "WriteUInt16",
|
||||
ReadInt32: "WriteInt32", ReadUInt32: "WriteUInt32",
|
||||
ReadInt64: "WriteInt64", ReadUInt64: "WriteUInt64",
|
||||
ReadSingle: "WriteSingle", ReadDouble: "WriteDouble",
|
||||
ReadString: "WriteString",
|
||||
};
|
||||
|
||||
const PRIM_DEFAULT = {
|
||||
WriteByte: 0, WriteBoolean: false, WriteInt16: 0, WriteUInt16: 0,
|
||||
WriteInt32: 0, WriteUInt32: 0, WriteInt64: 0, WriteUInt64: 0,
|
||||
WriteSingle: 0, WriteDouble: 0, WriteString: null,
|
||||
};
|
||||
|
||||
class RpcSerializer {
|
||||
constructor(schemaPath) {
|
||||
const j = JSON.parse(fs.readFileSync(schemaPath, "utf8"));
|
||||
this.schemas = j.schemas;
|
||||
this.midToReader = j.midToReader;
|
||||
this.midToName = j.midToName;
|
||||
}
|
||||
|
||||
// write one primitive
|
||||
_writePrim(w, op, val) {
|
||||
const fn = PRIM[op];
|
||||
if (!fn) throw new Error("unknown prim op " + op);
|
||||
if (val === undefined) val = PRIM_DEFAULT[fn];
|
||||
w[fn](val);
|
||||
}
|
||||
|
||||
// write a struct (NO marker) by schema ref
|
||||
_writeStruct(w, ref, val) {
|
||||
const fields = this.schemas[ref] || [];
|
||||
const obj = val || {};
|
||||
for (const f of fields) this._writeField(w, f, obj[f.name]);
|
||||
}
|
||||
|
||||
// write a complex (nullable, has marker) by schema ref
|
||||
_writeComplex(w, ref, val) {
|
||||
if (val === null || val === undefined) { w.WriteByte(0x00); return; }
|
||||
w.WriteByte(0xFF);
|
||||
const fields = this.schemas[ref] || [];
|
||||
for (const f of fields) this._writeField(w, f, val[f.name]);
|
||||
}
|
||||
|
||||
_writeList(w, item, val) {
|
||||
if (val === null || val === undefined) { w.WriteByte(0x00); return; }
|
||||
w.WriteByte(0xFF);
|
||||
const arr = Array.isArray(val) ? val : [];
|
||||
w.WriteInt32(arr.length);
|
||||
for (const el of arr) this._writeItem(w, item, el);
|
||||
}
|
||||
|
||||
_writeItem(w, item, val) {
|
||||
if (item.kind === "prim") this._writePrim(w, item.op, val);
|
||||
else if (item.kind === "struct") this._writeStruct(w, item.ref, val);
|
||||
else if (item.kind === "complex") this._writeComplex(w, item.ref, val);
|
||||
else throw new Error("unknown list item kind " + item.kind);
|
||||
}
|
||||
|
||||
_writeField(w, f, val) {
|
||||
switch (f.kind) {
|
||||
case "prim": this._writePrim(w, f.op, val); break;
|
||||
case "struct": this._writeStruct(w, f.ref, val); break;
|
||||
case "complex": this._writeComplex(w, f.ref, val); break;
|
||||
case "buffer":
|
||||
if (val === null || val === undefined) { w.WriteByte(0x00); }
|
||||
else { w.WriteByte(0xFF); w.WriteInt32(val.length); w.WriteRawBuffer(val); }
|
||||
break;
|
||||
case "list": this._writeList(w, f.item, val); break;
|
||||
case "dict":
|
||||
// dict serialized like list: null=0x00 ; present=0xFF + int32 count + pairs
|
||||
if (val === null || val === undefined) { w.WriteByte(0x00); }
|
||||
else { w.WriteByte(0xFF); w.WriteInt32(0); } // empty dict baseline
|
||||
break;
|
||||
default: throw new Error("unknown field kind " + f.kind);
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize a top-level struct (by reader ref) into a complex payload
|
||||
// (with the leading present marker), e.g. EnterSceneInfo = ref "53".
|
||||
serializeComplex(ref, obj) {
|
||||
const w = new Writer();
|
||||
this._writeComplex(w, ref, obj);
|
||||
return w.toBuffer();
|
||||
}
|
||||
|
||||
// Build a fully-PRESENT default object for a complex schema ref. Every field
|
||||
// is materialised so the client never dereferences a nil:
|
||||
// prim -> 0 / false / "" default
|
||||
// struct -> recurse
|
||||
// complex-> recurse (present, never null)
|
||||
// list -> [] (present empty list, NOT null)
|
||||
// dict -> {} (present empty dict, NOT null)
|
||||
// buffer -> null (ReadBuffer tolerates absent)
|
||||
// A real server sends a fresh player exactly like this - no null sub-objects.
|
||||
// `overrides` lets callers set real values on specific top-level fields.
|
||||
// `_depth` guards against cyclic schemas.
|
||||
autoDefault(ref, overrides, _depth) {
|
||||
_depth = _depth || 0;
|
||||
const fields = this.schemas[ref];
|
||||
if (!fields || _depth > 24) return {};
|
||||
const out = {};
|
||||
for (const f of fields) {
|
||||
if (overrides && Object.prototype.hasOwnProperty.call(overrides, f.name)) {
|
||||
out[f.name] = overrides[f.name];
|
||||
continue;
|
||||
}
|
||||
switch (f.kind) {
|
||||
case "prim":
|
||||
out[f.name] = (f.op === "ReadBoolean") ? false
|
||||
: (f.op === "ReadString") ? null
|
||||
: 0;
|
||||
break;
|
||||
case "struct": out[f.name] = this.autoDefault(f.ref, null, _depth + 1); break;
|
||||
case "complex": out[f.name] = this.autoDefault(f.ref, null, _depth + 1); break;
|
||||
case "list": out[f.name] = []; break;
|
||||
case "dict": out[f.name] = {}; break;
|
||||
case "buffer": out[f.name] = null; break;
|
||||
default: out[f.name] = null;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Serialize a struct without the outer marker.
|
||||
serializeStruct(ref, obj) {
|
||||
const w = new Writer();
|
||||
this._writeStruct(w, ref, obj);
|
||||
return w.toBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { RpcSerializer, Writer };
|
||||
Reference in new issue
Block a user