using Ananta.Server.RpcTypes.Client4229938; using SceneMethods = Ananta.Server.RpcTypes.Client4229938.Methods.GameScene; namespace Ananta.Server.Protocol.Client4229938; /// /// Typed factories for world/game-scene messages. The wire layout itself lives in Ananta.RpcTypes. /// Large world/profile packets are built as RpcTypes DTOs by RuntimePayloadFactory and serialized on the fly. /// internal static class WorldCodec { internal static SceneMethods.SyncLogicAgentEnter LogicAgentEnter(ulong agentId) => new() { agentId = agentId }; internal static SceneMethods.SyncLogicAgentLeave LogicAgentLeave(ulong agentId) => new() { agentId = agentId }; internal static SceneMethods.SyncManagedLogicAgent ManagedLogicAgent(ulong agentId, ulong pid, byte moveId = 0) => new() { agentId = agentId, pid = pid, moveId = moveId }; /// /// S2C 68838628 SyncLogicAgentSyncData —— 给「服务端驱动」单位回正位姿。 /// /// ⛑⛑ 它**不能驱动步态**(donor1 第 39 轮 + 我们 09-20 实测已证伪:10Hz 推了 6280 条, /// 字节布局与客户端上报逐字节对齐,人依然纹丝不动),但正是这个「纹丝不动」 /// 才是我们要的:留在大世界里的旧角色一旦没人管就会自由落体掉出世界 ///(2026-09-24 18:1x 实测 Y 从 -0.13 → -293 → -3000,约 60~90 m/s), /// 因为客户端停掉自己的 AI 后只做物理模拟,而这些 actor 没有地面吸附。 /// ⇒ 用这个包把记录里的站立位姿回给它,它就不会掉。 /// internal static SceneMethods.SyncLogicAgentSyncData4229938 LogicAgentSyncData( ulong agentId, Vec3 position, float facing, byte moveId = 1) => new() { AgentId = agentId, Position = new SceneMethods.UxVector3(position.X, position.Y, position.Z), Rotation = new SceneMethods.UxVector3(0f, facing, 0f), MoveId = moveId, }; /// /// Round 39 (donor1): SyncLogicAgentCommand (mid 68050574) carrying a movement task - /// TaskWayPointMoveCommandData (wire tag 39). It carries speed, move action and animation /// group with the waypoints, so the client needs no navigation data at all to run the walk. /// ★ donor1 round-51 用户亲测的"完整路人"驱动协议(一直有走路动画、会移动、到目的地停下)。 /// internal static SceneMethods.SyncLogicAgentCommandWayPoint LogicAgentCommandWayPoint( ulong agentId, int commandId, byte specificMethod, float speed, uint moveActionId, uint moveActionGroup, bool keepMovingAction, IReadOnlyList points, bool noRootMotion = true) => new() { agentId = agentId, commandId = commandId, data = new SceneMethods.TaskWayPointMoveCommandData { SpecificMethod = specificMethod, KeepMovingAction = keepMovingAction, Speed = speed, MoveActionId = moveActionId, NoRootMotion = noRootMotion, MoveActionGroup = moveActionGroup, WayPoints = points.ToList(), }, }; internal static SceneMethods.SyncPlayerCurrentSpirit CurrentSpirit() => CurrentSpirit(Profile.PlayerPid, Profile.InitialSpiritTemplateId, Profile.InitialUnitId); internal static SceneMethods.SyncPlayerCurrentSpirit CurrentSpirit(ulong playerPid, uint templateId, ulong spiritId, bool isAgentSwitch = false) => new() { playerPid = playerPid, templateId = templateId, spiritId = spiritId, isAgentSwitch = isAgentSwitch }; /// /// 切人演出的收尾握手之一:告诉客户端"新角色就在原地接管"(2 字段版本,见 /// WorldMethods.cs 里 SyncPlayerSwitchSpiritInSitu 的注释)。 /// internal static SceneMethods.SyncPlayerSwitchSpiritInSitu SwitchSpiritInSitu(uint templateId, ulong spiritId) => new() { templateId = templateId, spiritId = spiritId }; /// /// SyncManagedSpirit / SyncManagedAgent 共用的包体构造。 /// CBT2 官方服务端这里传的是 moveId = 0。 /// internal static SceneMethods.SyncManagedUnit4229938 ManagedUnit(ulong pid, ulong id, int moveId = 0) => new() { pid = pid, id = id, moveId = moveId }; internal static SceneMethods.SyncSwitchSpiritConfigId SwitchSpiritConfigId( uint configId, Vec3 position, IReadOnlyCollection? spawnedAgentIds = null) => new() { configId = configId, position = new SceneMethods.UxVector3(position.X, position.Y, position.Z), spawnedAgentIds = spawnedAgentIds?.ToList() ?? [] }; internal static SceneMethods.SyncPreSwitchSpirit PreSwitchSpirit(uint configId, uint newTemplateId, Vec3 position) => new() { configId = configId, newTemplateId = newTemplateId, position = new SceneMethods.UxVector3(position.X, position.Y, position.Z) }; internal static SceneMethods.SyncPlayerLoadRate PlayerLoadRate(double rate = 1.0) => new() { playerPid = Profile.PlayerPid, rate = rate }; internal static SceneMethods.SyncSceneLoadCompleted SceneLoadCompleted(ulong sceneId) => new() { sceneId = sceneId }; /// /// Hard snap: the unit is placed and its locomotion state is explicitly "none" (MoveAction.Unknown). /// Correct for teleports and first-time placement — wrong for anything that is supposed to walk, /// because a unit moved this way slides to the target without ever playing a gait. /// internal static SceneMethods.SyncUnitPositionAndFacing PositionAndFacing(ulong unitId, Vec3 position, float facing, byte setPositionType = SetPositionType.SwitchSpirit) => PositionAndFacing(unitId, position, facing, setPositionType, continueMove: false, moveId: MoveAction.Unknown); /// /// Position push that also tells the client which locomotion state the unit is in. /// /// is a UX.Game.BattleMoveActionType and is the field the client's /// locomotion state machine reads. 0 (Unknown) means "no gait", so an entity moved this way slides /// to the new position without playing a walk/run cycle — the "走路僵硬 / 瞬移" symptom. Every /// overload here used to hardcode moveId = 0, which made any server-driven movement look /// stiff no matter what the caller intended. /// internal static SceneMethods.SyncUnitPositionAndFacing PositionAndFacing( ulong unitId, Vec3 position, float facing, byte setPositionType, bool continueMove, byte moveId) => new() { unitId = unitId, position = new SceneMethods.UxVector3(position.X, position.Y, position.Z), facing = facing, moveId = moveId, continueMove = continueMove, setPositionType = setPositionType, moveGroundInfo = null, loadingInfo = null, }; /// Walking push: gait = WalkFront, GM positioning, keep the locomotion state alive on arrival. internal static SceneMethods.SyncUnitPositionAndFacing Walking( ulong unitId, Vec3 position, float facing, byte moveId = MoveAction.WalkFront, byte setPositionType = SetPositionType.Gm, bool continueMove = true) => PositionAndFacing(unitId, position, facing, setPositionType, continueMove, moveId); internal static SceneMethods.SyncUnitPositionAndFacing PositionAndFacing(Vec3 position, float facing, byte setPositionType = SetPositionType.SwitchSpirit) => PositionAndFacing(Profile.InitialUnitId, position, facing, setPositionType); internal static SceneMethods.SyncUnitPositionP PositionP(ulong unitId, Vec3 position, float facing, byte setPositionType = SetPositionType.SwitchSpirit) => PositionP(unitId, position, facing, setPositionType, MoveAction.Unknown); /// /// carries a moveId too, so it has the same "no gait" trap as /// . /// internal static SceneMethods.SyncUnitPositionP PositionP(ulong unitId, Vec3 position, float facing, byte setPositionType, byte moveId) => new() { unitId = unitId, position = new SceneMethods.UxVector3(position.X, position.Y, position.Z), facing = facing, moveId = moveId, setPositionType = setPositionType }; internal static SceneMethods.SyncUnitPositionP PositionP(Vec3 position, float facing, byte setPositionType = SetPositionType.SwitchSpirit) => PositionP(Profile.InitialUnitId, position, facing, setPositionType); internal static SceneMethods.SyncGamePause GamePause(bool pause) => new() { pause = pause }; internal static SceneMethods.SyncEntityActionGroup EntityActionGroup(ulong unitId, uint actionGroupId = 1u) => new() { unitId = unitId, actionGroupId = actionGroupId }; internal static SceneMethods.SyncAllSpiritCombatPower AllSpiritCombatPower(uint templateId, float combatPower = 1000f) => new() { combatPower = new Dictionary { [templateId] = combatPower } }; internal static SceneMethods.SyncAllSpiritCombatPower AllSpiritCombatPower(params uint[] templateIds) { var body = new SceneMethods.SyncAllSpiritCombatPower(); foreach (var templateId in templateIds) body.combatPower[templateId] = 1000f; return body; } internal static SceneMethods.SyncChangeSkill SkillBinding(ulong spiritId, uint skillId) => new() { spiritId = spiritId, skillId = skillId, duration = skillId == 0 ? 0f : 99999f }; internal static SceneMethods.SyncUnitBuffList UnitBuffList(ulong unitId, IReadOnlyList buffIds, uint firstInstanceId) { var body = new SceneMethods.SyncUnitBuffList { entityId = unitId }; // 4229938 still evaluates ExpireTime inside several client-side BuffActions even when // Permanent=true. ExpireTime=0 becomes a huge negative remaining time and can execute // teardown paths immediately. Use a real far-future unix timestamp for persistent state. var persistentExpiry = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 315_360_000d; var instanceId = firstInstanceId; foreach (var buffId in buffIds) { body.buffs.Add(new SceneMethods.BuffViewData { InstanceId = instanceId++, Id = buffId, ReleaserId = unitId, ExpireTime = persistentExpiry, Tier = 1, Permanent = true, DestructibleId = 0 }); } return body; } internal static SceneMethods.SyncUnitAddBuff UnitAddBuff(ulong unitId, uint buffId, uint instanceId) => new() { entityId = unitId, buff = new SceneMethods.BuffViewData { InstanceId = instanceId, Id = buffId, ReleaserId = unitId, ExpireTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 315_360_000d, Tier = 1, Permanent = true, DestructibleId = 0 } }; internal static SceneMethods.SyncUnitRemoveBuff UnitRemoveBuff(ulong unitId, uint buffInstanceId) => new() { entityId = unitId, buffInstanceId = buffInstanceId }; internal static SceneMethods.SyncUnitStates UnitStates(ulong unitId, params uint[] states) => new() { unitId = unitId, states = states?.ToList(), effectFreezeState = 0 }; internal static SceneMethods.SyncUnitStates UnitStates(params uint[] states) => UnitStates(Profile.InitialUnitId, states); internal static SceneMethods.SyncRemoveUnitState RemoveUnitState(ulong unitId, uint state) => new() { unitId = unitId, state = state, reason = 1, sourceId = 0 }; internal static SceneMethods.SyncRemoveUnitState RemoveUnitState(uint state) => RemoveUnitState(Profile.InitialUnitId, state); /// /// UX.Game.BattleMoveActionType — authoritative values from /// ux_rpc/ux_game_all.dump.cs:21040 (IL2CPP TypeDefIndex 32492). /// /// This is the moveId byte of SyncUnitPositionAndFacing, SyncUnitPositionP and /// SyncManagedLogicAgent, and of every BattleMoveDataBase subclass. The enum itself is /// an int in the client, but the wire field is a single byte. /// /// Unknown (0) means "no locomotion state" — a unit pushed with it slides instead of walking, which /// is why server-driven movement used to look stiff. /// internal static class MoveAction { internal const byte Unknown = 0; internal const byte Idle = 1; internal const byte WalkFront = 2; internal const byte WalkBack = 3; internal const byte WalkLeft = 4; internal const byte WalkRight = 5; internal const byte Run = 6; internal const byte Min = Unknown; internal const byte Max = Run; internal static readonly (byte Id, string Name)[] All = [ (Unknown, "Unknown 空"), (Idle, "Idle 待机"), (WalkFront, "WalkFront 前进"), (WalkBack, "WalkBack 后退"), (WalkLeft, "WalkLeft 左移"), (WalkRight, "WalkRight 右移"), (Run, "Run 奔跑"), ]; internal static string NameOf(byte moveId) { foreach (var entry in All) { if (entry.Id == moveId) return entry.Name; } return $"MoveId {moveId}"; } } /// /// UX.Game.SetPositionTypeForClient — authoritative values from /// ux_rpc/ux_game_all.dump.cs:15656 (IL2CPP TypeDefIndex 32205). /// /// ⚠️ Gm is NOT a prerequisite for moving the main character: the value that matters less /// than the packet. Moving the local avatar via SyncUnitPosition_P fails no matter which /// type is used (NetworkGhostModule demands NetworkRoleType.Proxy); use /// SyncUnitPositionAndFacing with the default SwitchSpirit instead — that is what /// the donor server's own teleport API does and it lands the player. /// internal static class SetPositionType { internal const byte Force = 0; internal const byte RejectSync = 1; internal const byte Revive = 2; internal const byte SwitchSpirit = 3; internal const byte SpoonNoLoading = 4; internal const byte FallGround = 5; internal const byte Teleport = 6; internal const byte Gm = 7; internal const byte Portal = 8; internal const byte OutOfStuck = 9; } }