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

211 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
//