Files
Ananta-ps/Ananta.Server/Ananta.Handlers/Game/GameRouter.Switching.cs
T

133 lines
6.8 KiB
C#

using Ananta.SDK.Rpc;
using Ananta.Server.Handlers;
using Ananta.Server.Gameplay;
using Ananta.Server.Protocol.Client4229938;
using Ananta.Server.ClientData.Client4229938;
using Ananta.Server.RpcTypes.Client4229938;
namespace Ananta.Server.Handlers.Game;
/// <summary>Generic character switching. No protagonist-specific switch path lives here.</summary>
internal sealed partial class GameRouter
{
async Task OnSwitchSpirit(RpcContext ctx, uint requestedSpiritId)
{
var state = GetWorldState(ctx);
if (!state.Ready)
{
ctx.Session.Log.Warn($"[SWITCH-MIN] ignored before world ready spirit={requestedSpiritId}");
return;
}
if (state.PendingSwitchTemplateId != 0)
{
ctx.Session.Log.Warn($"[SWITCH-MIN] ignored while pending target={state.PendingSwitchTemplateId}");
return;
}
var target = ClientConfigRepository.Characters().FirstOrDefault(x => x.TemplateId == requestedSpiritId);
if (target is null)
{
ctx.Session.Log.Warn($"[SWITCH-MIN] unknown requested spirit={requestedSpiritId}");
return;
}
if (target.TemplateId == state.ActiveSpiritTemplateId)
return;
var hasLiveTransform = state.HasLastReportedPlayerTransform;
var position = hasLiveTransform
? state.LastReportedPlayerPosition
: state.WorldEntryCreateHeroPosition;
var facing = hasLiveTransform && float.IsFinite(state.LastReportedPlayerRotation.Y)
? state.LastReportedPlayerRotation.Y
: state.WorldEntryCreateHeroFacing;
// Freeze the old actor's last authoritative transform before CurrentSpirit changes ownership.
// Delayed movement packets from the old unit are ignored after the handoff, so every target
// starts exactly where the player stood when the switch was requested.
state.LastReportedPlayerPosition = position;
state.LastReportedPlayerRotation = new Vec3(0f, facing, 0f);
state.HasLastReportedPlayerTransform = true;
// Minimal mode never starts SyncPreSwitchSpirit/SyncSwitchSpiritConfigId Timeline flows.
// It performs only the identity/unit handoff needed to keep the requested character controllable.
await SwitchSpiritDirectSameScene4229938(ctx, state, target.TemplateId, target.UnitId, position, facing);
}
async Task SwitchSpiritDirectSameScene4229938(
RpcContext ctx, WorldEntryState state, uint templateId, ulong unitId, Vec3 switchPosition, float switchFacing)
{
var oldUnitId = state.ActiveSpiritUnitId;
state.PendingSwitchTemplateId = templateId;
state.PendingSwitchUnitId = unitId;
state.PendingSwitchOldUnitId = oldUnitId;
state.PendingSwitchShowId = 0;
state.PendingSwitchPosition = switchPosition;
state.PendingSwitchFacing = switchFacing;
state.PendingSwitchControlTransferred = false;
state.PendingSwitchLandingStarted = false;
// Materialize exactly one target character. No authored switch Timeline, weapons, combat graph,
// fashion inventory replay, vehicle cleanup or old-build gameplay hydration is published.
await ctx.NotifyAsync(MethodId.SyncLogicAgentEnter, WorldCodec.LogicAgentEnter(unitId));
await ctx.NotifyAsync(MethodId.SyncManagedLogicAgent, WorldCodec.ManagedLogicAgent(unitId, Profile.PlayerPid, 0));
await ctx.NotifyAsync(MethodId.SyncRaidBattleUnitSpirit,
RuntimePayloadFactory.CharacterUnitProjection(templateId, switchPosition, switchFacing));
await ctx.NotifyAsync(MethodId.SyncUnitPositionAndFacing,
WorldCodec.PositionAndFacing(unitId, switchPosition, switchFacing));
await ctx.NotifyAsync(MethodId.SyncPlayerCurrentSpirit,
WorldCodec.CurrentSpirit(Profile.PlayerPid, templateId, unitId, isAgentSwitch: true));
// CurrentSpirit can rebuild the controlled actor on the client. Re-assert the frozen transform
// after ownership transfer so the new character cannot fall back to its template/default spawn.
await ctx.NotifyAsync(MethodId.SyncUnitPositionAndFacing,
WorldCodec.PositionAndFacing(unitId, switchPosition, switchFacing));
state.LastReportedPlayerPosition = switchPosition;
state.LastReportedPlayerRotation = new Vec3(0f, switchFacing, 0f);
state.HasLastReportedPlayerTransform = true;
state.ActiveSpiritTemplateId = templateId;
state.ActiveSpiritUnitId = unitId;
state.PendingSwitchControlTransferred = true;
state.ActiveSkillId = 0;
state.RestoreResourcesAfterActiveSkill = false;
state.ActiveClientSkillInstanceId = 0;
state.ActiveSkillStartedTicks = 0;
// Direct switch still needs a complete actor presentation even though combat gameplay is
// disabled. Bind the stock fashion/wheel/current weapon so the target does not become a
// half-initialized BaseUnit after CurrentSpirit changes ownership.
await PublishMinimalActorPresentation4229938(ctx, unitId, templateId, "direct-switch");
if (state.ActiveFeiSuoBuffInstanceId != 0)
{
var transientUnitId = state.ActiveFeiSuoBuffUnitId != 0 ? state.ActiveFeiSuoBuffUnitId : oldUnitId;
await ctx.NotifyAsync(MethodId.SyncUnitRemoveBuff,
WorldCodec.UnitRemoveBuff(transientUnitId, state.ActiveFeiSuoBuffInstanceId));
state.ActiveFeiSuoBuffInstanceId = 0;
state.ActiveFeiSuoBuffUnitId = 0;
}
await ClearClientWebLifecycleBuffs(ctx, oldUnitId, "minimal-direct-switch");
await PublishSafeRuntimeBuffSnapshot4229938(ctx, unitId, templateId, "minimal-direct-switch");
state.AllBuildBuffsPublished = true;
// Keep the previous character in the world scene so the player can interact with / carry them
// if (oldUnitId != 0 && oldUnitId != unitId)
// await ctx.NotifyAsync(MethodId.SyncLogicAgentLeave, WorldCodec.LogicAgentLeave(oldUnitId));
await ctx.NotifyAsync(MethodId.SyncGamePause, WorldCodec.GamePause(false));
state.SwitchCount++;
state.LastSwitchShowId = 0;
state.PendingSwitchTemplateId = 0;
state.PendingSwitchUnitId = 0;
state.PendingSwitchOldUnitId = 0;
state.PendingSwitchShowId = 0;
state.PendingSwitchFacing = state.WorldEntryCreateHeroFacing;
state.PendingSwitchControlTransferred = false;
state.PendingSwitchLandingStarted = false;
ctx.Session.Log.Info($"[SWITCH-MIN] direct count={state.SwitchCount} oldUnit={oldUnitId} target={templateId}/{unitId} authoredTimeline=false combatModules=false actorPresentation=true vehicles=false buffs=safe-web-only pos=({switchPosition.X:0.##},{switchPosition.Y:0.##},{switchPosition.Z:0.##}) facing={switchFacing:0.##}");
}
}