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/Ananta.Server/Ananta.Core/Protocol/Client4229938/WorldCodec.cs
T

338 lines
15 KiB
C#
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.
using Ananta.Server.RpcTypes.Client4229938;
using SceneMethods = Ananta.Server.RpcTypes.Client4229938.Methods.GameScene;
namespace Ananta.Server.Protocol.Client4229938;
/// <summary>
/// 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.
/// </summary>
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 };
/// <summary>
/// S2C 68838628 <c>SyncLogicAgentSyncData</c> —— 给「服务端驱动」单位回正位姿。
///
/// ⛑⛑ 它**不能驱动步态**(donor1 第 39 轮 + 我们 09-20 实测已证伪:10Hz 推了 6280 条,
/// 字节布局与客户端上报逐字节对齐,人依然纹丝不动),但正是这个「纹丝不动」
/// 才是我们要的:留在大世界里的旧角色一旦没人管就会自由落体掉出世界
///(2026-09-24 18:1x 实测 Y 从 -0.13 → -293 → -3000,约 60~90 m/s),
/// 因为客户端停掉自己的 AI 后只做物理模拟,而这些 actor 没有地面吸附。
/// ⇒ 用这个包把记录里的站立位姿回给它,它就不会掉。
/// </summary>
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,
};
/// <summary>
/// Round 39 (donor1): <c>SyncLogicAgentCommand</c> (mid 68050574) carrying a <b>movement task</b> -
/// <c>TaskWayPointMoveCommandData</c> (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 用户亲测的"完整路人"驱动协议(一直有走路动画、会移动、到目的地停下)。
/// </summary>
internal static SceneMethods.SyncLogicAgentCommandWayPoint LogicAgentCommandWayPoint(
ulong agentId, int commandId, byte specificMethod, float speed, uint moveActionId, uint moveActionGroup,
bool keepMovingAction, IReadOnlyList<SceneMethods.UxVector3> 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
};
/// <summary>
/// 切人演出的收尾握手之一:告诉客户端"新角色就在原地接管"(2 字段版本,见
/// <c>WorldMethods.cs</c> 里 <c>SyncPlayerSwitchSpiritInSitu</c> 的注释)。
/// </summary>
internal static SceneMethods.SyncPlayerSwitchSpiritInSitu SwitchSpiritInSitu(uint templateId, ulong spiritId)
=> new()
{
templateId = templateId,
spiritId = spiritId
};
/// <summary>
/// <c>SyncManagedSpirit</c> / <c>SyncManagedAgent</c> 共用的包体构造。
/// CBT2 官方服务端这里传的是 <c>moveId = 0</c>。
/// </summary>
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<ulong>? 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 };
/// <summary>
/// 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.
/// </summary>
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);
/// <summary>
/// Position push that also tells the client <b>which locomotion state the unit is in</b>.
///
/// <paramref name="moveId"/> is a <c>UX.Game.BattleMoveActionType</c> 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 <c>moveId = 0</c>, which made any server-driven movement look
/// stiff no matter what the caller intended.
/// </summary>
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,
};
/// <summary>Walking push: gait = WalkFront, GM positioning, keep the locomotion state alive on arrival.</summary>
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);
/// <summary>
/// <see cref="SyncUnitPositionP"/> carries a moveId too, so it has the same "no gait" trap as
/// <see cref="PositionAndFacing(ulong, Vec3, float, byte)"/>.
/// </summary>
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<uint, float> { [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<uint> 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);
/// <summary>
/// <c>UX.Game.BattleMoveActionType</c> — authoritative values from
/// <c>ux_rpc/ux_game_all.dump.cs:21040</c> (IL2CPP TypeDefIndex 32492).
///
/// This is the <c>moveId</c> byte of <c>SyncUnitPositionAndFacing</c>, <c>SyncUnitPositionP</c> and
/// <c>SyncManagedLogicAgent</c>, and of every <c>BattleMoveDataBase</c> subclass. The enum itself is
/// an <c>int</c> 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.
/// </summary>
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}";
}
}
/// <summary>
/// <c>UX.Game.SetPositionTypeForClient</c> — authoritative values from
/// <c>ux_rpc/ux_game_all.dump.cs:15656</c> (IL2CPP TypeDefIndex 32205).
///
/// ⚠️ <c>Gm</c> is NOT a prerequisite for moving the main character: the value that matters less
/// than the packet. Moving the local avatar via <c>SyncUnitPosition_P</c> fails no matter which
/// type is used (NetworkGhostModule demands NetworkRoleType.Proxy); use
/// <c>SyncUnitPositionAndFacing</c> with the default <c>SwitchSpirit</c> instead — that is what
/// the donor server's own teleport API does and it lands the player.
/// </summary>
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;
}
}