This repository has been archived on 2026-10-31. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Ananta-ps/pkg/protocol/methods/gadget_methods4229938.go
T

210 lines
13 KiB
Go
Raw Normal View History

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&lt;int&gt; 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 &lt;&lt; 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&lt;List&lt;MetroCarriageGadgetInfos&gt;&gt; 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&lt;ulong&gt; ids, GadgetDisposeReason reason).
// Both args are bare, so inline.
//
//
// SyncGadgetAddList(68925813): (List&lt;GadgetEntityInfo&gt; 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&lt;GadgetEntityInfo&gt; addInfos,
// List&lt;GadgetPackSyncInfo&gt; addPackSyncInfos, List&lt;ulong&gt; removeIds,
// List&lt;ulong&gt; activeIds, List&lt;ulong&gt; 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.
//