211 lines
13 KiB
Go
211 lines
13 KiB
Go
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:
|
||
// <code>public class DoorClientInfo // TypeDefIndex: 33146 { public float OpenAngle; // 0x10 }</code>
|
||
// 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.
|
||
//
|