package methods // // Gadget (互动物件) contracts, copied verbatim from the build-4229938 IL2CPP interop assembly // (ux_game_all.dump.cs -> UX.Game.IGameSceneToClient). // A Gadget is the game's "placed interactive prop": chairs the player can sit on, vending // machines, arcade cabinets, computers, drill shelves, doors. The client owns every prefab — // the server only has to say *which prefab, where*, and the client loads and spawns it: // SyncGadgetAdd(68843189) GadgetEntityInfo one gadget // SyncGadgetAddList(68925813) List<GadgetEntityInfo> batch // SyncGadgetRefresh(68765074) GadgetEntityInfo restate one // SyncGadgetAOIAddAndRemove(68169646) add/remove/active round-trip // SyncGadgetRemove(...) / SyncGadgetRemoveList(...) ulong id + GadgetDisposeReason // Field order matters: the wire layout is the declaration order below, which matches the // IL2CPP field offsets in the dump. Every nested object except Pack is nullable and is // sent as null on first contact — the client's reader treats a null as "not present" and leaves // the subsystem uninitialised, which is exactly what a freshly spawned prop should look like. // // // PackedGadgetSpecialParam (struct). The spoon parameter the client needs on some placer instances // to arm their behaviour — e.g. markId 31511937 = "True" on MiniGameMachine_Bee (223752202). // ⛑️ Inline = true because dump.cs:1749913 declares it a **struct** (typedef 33134, fields // `int markId` / `string value`), exactly like GridIndex: value types carry no null/type-mark peek // byte, so switching this back to a Complex contract would shift every field after it. // // // PackedGadgetInfo — the placement payload: where the prop stands, which prefab it uses, and the // instance id the client will later report back when the player interacts with it. // // ReplaceGadgetInfo — swap-in animation for loot containers. None = 0, Loot = 1. // SceneDevicePersonalValueInfo (struct) — per-player state bag on a device. // SceneDeviceOccupantInfo (struct) — who is currently using a seat/device slot. // // DoorModuleSyncInfo — powered doors. Null unless the prefab has a door module. // ⛑️ Wire law (handoff 16G, disasm @0x189EFB727 / @0x189EFB757): LockState and // ProximityState are read with movzx = ONE WIRE BYTE each, not int32. Writing four // bytes each shifts ProximityStartTime into ClientInfos' peek byte and the client throws // `deserialize failed methodId=68765074 parameter=DoorModuleSyncInfo.ClientInfos`. // ⛑️ ClientInfos carries the LEAF RECORDS (handoff 16I-4/5, disasm SyncDoorClientInfo // @0x1927FD7D0 writes OpenAngle into the module's pre-existing per-panel list). With the list // EMPTY there is no leaf record and the panel hides the leaf on apply — that is the observed // "walk up and the door disappears" symptom. Body sizes, both donor-live-verified: // 130 bytes = ClientInfos empty // 135 bytes = ClientInfos carries one OpenAngle (+5 = element peek + f32) // OpenAngle semantics are panel-owned (hinge or slider); 0 = closed, 90 = open is safe for // sliders too, per round 16I-5. // // DoorClientInfo — ONE leaf record, and it is nothing but the angle. // ⛑️ Verbatim from dump.cs:1750082: // public class DoorClientInfo // TypeDefIndex: 33146 { public float OpenAngle; // 0x10 } // An earlier version of this contract carried (ulong Pid, int State); the real TypeDef has no // such fields, so those twelve bytes were pure invention and would have misaligned everything // read after them. Degrees: 0 = shut, 90 = open. // // // SyncDoorProximityState(68908833) — THE official door channel. // ⛑️ Found 2026-09-22 by reading GameSceneToClientImpl in dump.cs (~line 171535): the override is // `public override void SyncDoorProximityState(ulong gadgetId, DoorProximityState state, // uint startTime)` with RVA 0x127D4690 — a REAL implementation, not the 0x9148360 stub. The // donor's round 16C census ("zero door args classes among 2754") missed all three door S2Cs // because they carry bare scalars and therefore have NO args class to scan for. // Wire shape quoted from the client's own deserializer, RPCDeserializeAuto.lua:7150: // `gadgetId = ReadUInt64 | state = ReadByte | startTime = ReadUInt32` = 13 bytes. // The single state byte is the same handoff-16G movzx law: enums here are ONE wire byte. // // // SyncDoorLockState(68611285) — (ulong gadgetId, DoorLockState state), override RVA 0x127D4500. // Reader RPCDeserializeAuto.lua:6403: UInt64 + Byte = 9 bytes. 0 = Unlocked, 1 = Locked. // // // SyncDoorClientInfo(68622771) — (ulong gadgetId, uint index, DoorClientInfo clientInfo), // override RVA 0x127D4840. Reader RPCDeserializeAuto.lua:6435: UInt64 + UInt32 + ReadComplex. // That uint index is why the module's ClientInfos is a per-panel list: this sets ONE leaf by // index, far more precise than restating an entire GadgetEntityInfo. // // DrillShelfSyncInfo — the drilling-shelf minigame grid. // DrillShelfCell — one cell of the drilling-shelf grid. // SceneDeviceHangingInfo — props that hang off another entity (wall signs, lanterns). // // LiftInteractBanInfo — elevator interaction ban window (dump.cs:1750024, TypeDefIndex 33141). // ⛑️ Real fields are LiftInteractBanType Type + List<int> SpecificLevelList // (dump.cs:1750037: None=0 / All=1 / SpecificLevel=2). An earlier draft invented // (bool BanInteract, uint BanEndTime); those five bytes never existed and would misalign // everything after them. We always send null, which costs a single 0x00 peek byte. // // // MobilePlatformSyncInfo — moving platforms / lifts. // ⛑️⛑️ BIASED COUNT (UxCountEncoding.Int7, empty = 01 01) — same law as the lists. // Authority is the client's own deserializer, RPCDeserializeAuto.lua: // • Auto.Reader[452] reads Players = Base.ReadDict7Bit(...), and DICTIONARIES go // through the very same reader:ReadInt7Bit() as ReadList7Bit — one shared // native reader, so one shared bias. `SerializeObjectMarkNull` is 0 and the count byte is // read only when the presence byte is non-zero, hence an empty dictionary is `01 01`. // • The 2026-09-22 22:23 run that "proved" dicts are raw was misread: the push was rejected // because SyncMobilePlatformInfoArgs4229938.mobilePlatformInfo was shipped as a // STRUCT (no presence byte) while the client reads it with Base.ReadComplex. The // missing byte shifted everything, and the field-level error happened to be pinned on // MobilePlatformSyncInfo.Players. Fixing the missing marker fixed the count too. // • FF 01 is wrong here: 0xFF decodes to 255 and the bias leaves 254 phantom players // (handoff 16B). `01 00` is wrong too: the bias turns 0 into −1. // // // SyncLiftDoorMode(68799106): (ulong mobilePlatformId, LiftDoorMode doorMode) — // UXRPCMethodArgs68799106 (dump.cs:1835026). // ⛑️⛑️ THIS is how a lift door actually opens. There is no "open the door" C2S: the F-press only // produces AskMoveMobilePlatform, and LiftMgr keeps its own per-lift door mode, so parking // the cab with MobilePlatformSyncInfo.DoorMode = Normal leaves the doors shut and the // player can never step in. Client-side proof it is real: // • dump.cs:171154 public override void SyncLiftDoorMode(...) at RVA 0x127C5160 // (real implementation — 0x9148360 is the empty-stub RVA); // • RPCDeserializeAuto.lua self.midToReader[68799106] = ReadUInt64 + ReadByte. // LiftDoorMode (dump.cs:1750057): Normal = 0, StayOpen = 1, Closed = 2. // Body = 9 bytes, no root marker (the reader goes straight into the fields). // // // SyncMobilePlatformDoorClose(68424923): (ulong mobilePlatformId, int level) — 12 bytes. // The counterpart of SyncLiftDoorModeArgs4229938: shuts the cab door at a level. // Lua: self.midToReader[68424923] = ReadUInt64 + ReadInt32. Kept for the close half of the // ride; we currently never need to slam the door shut on the player. // // // AdhereMovingPlatformInfo (Gadget copy). // NpcSpawnMethods.cs already carries an empty class with this name (the NPC spawn path never // fills it), so this one is named apart on purpose — reusing that empty definition here would // silently serialise nothing for a gadget that really does ride a platform. // // // GadgetEntityInfo — the full entity record SyncGadgetAdd carries. Only Pack is // mandatory; every other nested module is authored per prefab and stays null here. // // // SyncGadgetRefresh(68765074) — restate ONE gadget entity. This is the only wire the client // listens to for door state (handoff 16C: census of all 2754 UXRPCMethodArgs classes found zero // dedicated door-proximity args classes). // ⛑️ Args shape (handoff 16D): ONE nullable class field and nothing else — a single present // peek then the entity's fields. NO count, NO second marker. Inline root + a complex // GadgetEntityInfo field reproduces exactly that: one 0xFF peek followed by the entity. // The 16C writer emitted an extra leading marker ("double-peek") and every field after it was // misaligned → `deserialize failed ... parameter=GadgetEntityInfo.Pack`. // // // SyncMobilePlatformInfo(68705244): (ulong mobilePlatformId, MobilePlatformSyncInfo // mobilePlatformInfo, bool force) — UXRPCMethodArgs68705244 (dump.cs TypeDefIndex 35613). // This is the ONLY thing that moves a lift: the client's AskMoveMobilePlatform is a request, and // the cab "arrives" when the server echoes this push back (handoff 16B/16F: with the body fixed, // the Bassline 808 elevator did a full GetOn/Move/GetOff round trip, error=0). // ⛑⛑ Inline = true — the args carry NO root presence byte. Proven live against our own client // (2026-09-22 21:33, Ananta_mainleakflow_260805/log/Player.log): with a leading 0xFF the client // read the marker into the id and reported // [error][电梯]LiftInfo pid 2358241240919044863 没有数据 // while the lift's real id is 11034023767650314226. 0x20BA27EA03A7F2FF is exactly // (0x9920BA27EA03A7F2 << 8) | 0xFF — our eight id bytes shifted by the marker. LiftMgr.RefreshLiftState // then looked up a pid that does not exist in its PCGLiftInfo table and dropped the push without // any `deserialize failed` (all trailing fields are fixed-width, so the extra byte is simply // ignored). Same law as SyncGadgetGridAOIDecrease(68956797): if the reader goes straight into the // fields, never write the envelope. // ⛑️ But the args are NOT one flat struct: `mobilePlatformInfo` is read with // Base.ReadComplex, so it keeps its own presence byte. Body is 50 bytes: // 8 id + 1 info marker + 40 MobilePlatformSyncInfo + 1 force. // // // AskMoveMobilePlatform(63185921): (ulong mobilePlatformId, int targetIndex) — // UXRPCMethodArgs63185921. Bare scalars, so inline (no 0xFF/0x00 envelope). // // // AskGetOnMobilePlatform(63687984): (ulong mobilePlatformId) — UXRPCMethodArgs63687984. // AskGetOffMobilePlatform(63974264) carries no fields at all and needs no contract. // // GadgetPackSyncInfo — id + placement only, used by the AOI add/remove batch. // MetroCarriageGadgetInfos — answer to AskMetroGadgetIds(67809129). // // AskMetroGadgetIds(67809129): UXRPCTask<List<MetroCarriageGadgetInfos>> AskMetroGadgetIds(int metroId). // One bare int32 ⇒ inline (no 0xFF/0x00 presence byte) — the captured request body is exactly 4 bytes. // metroId is the same id we hand out in SyncRunningMetroInfos (lineId * 100 + trainIndex, e.g. 405). // // // Client invoke AskActiveDynamicGo(67339530): AskActiveDynamicGo(int dyId, bool active, // DynamicGoChangeReason reason). // Three bare scalars, so this is an inline (struct) contract with no 0xFF/0x00 prefix. // reason is a byte despite the dump typing the enum as int: the captured payload is // 4+1+1 bytes (e.g. DCFB5E01 00 05 = id 22930780, active false, reason 5/Shop). Writing it as // an int would shift everything after it and the client would read garbage. // // // SyncDynamicGoChange(68106491): (int dynamicGoId, bool active, DynamicGoChangeReason reason). // Mirror image of the client's report, inline for the same reason. // // // SyncGadgetRemoveList(68199571): (List<ulong> ids, GadgetDisposeReason reason). // Both args are bare, so inline. // // // SyncGadgetAddList(68925813): (List<GadgetEntityInfo> infos) — the channel // AnantaGymLoader.exe uses (it calls this il2cpp method directly by frida). // Args class (dump.cs TypeDefIndex 35751) has exactly ONE field, infos. Same law as // SyncGadgetAdd(68843189): a single-parameter RPC is read as the bare parameter, with no // envelope around the args object — so inline, then the Int7-biased list count. // ⚠️ Wire shape here is an inference from that law, not yet proven live. If a push on this // channel produces nothing at all, try a leading presence byte instead (see // GadgetSendChannel.AddList and flip world.content.gymEquipment.channel back to 0). // // // SyncGadgetAOIAddAndRemove(68169646): the world-streaming channel for props. // Client signature (dump.cs, GameSceneToClientImpl): // SyncGadgetAOIAddAndRemove(List<GadgetEntityInfo> addInfos, // List<GadgetPackSyncInfo> addPackSyncInfos, List<ulong> removeIds, // List<ulong> activeIds, List<ulong> inacticeIds, AoiAddAndRemoveReason reason) // Six bare args, so inline. reason values: Init=0, Loaded=1, AOIMove=2, // SpecialCondition=3, TargetCondition=4, SectorChange=5, BuildingChange=6, Quality=7, Other=8. //