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

181 lines
8.1 KiB
C#

using Ananta.SDK.Rpc;
using Ananta.Server.Handlers;
using Ananta.Server.Gameplay;
using Ananta.Server.Protocol.Client4229938;
namespace Ananta.Server.Handlers.Game;
/// <summary>Protagonist traversal/web buff handling. Buff ids come from private-server.json.</summary>
internal sealed partial class GameRouter
{
static bool IsActiveProtagonist(WorldEntryState state, ulong unitId = 0)
=> WebTraversal.IsProtagonist(state.ActiveSpiritTemplateId)
&& (unitId == 0 || unitId == state.ActiveSpiritUnitId);
async Task EnsureFeiSuoActionBuff(RpcContext ctx)
{
var state = GetWorldState(ctx);
if (!IsActiveProtagonist(state))
return;
if (state.ActiveFeiSuoBuffInstanceId != 0)
return;
var instanceId = state.NextBuffInstanceId++;
state.ActiveFeiSuoBuffInstanceId = instanceId;
state.ActiveFeiSuoBuffUnitId = state.ActiveSpiritUnitId;
await ctx.NotifyAsync(MethodId.SyncUnitAddBuff,
WorldCodec.UnitAddBuff(state.ActiveSpiritUnitId, WebTraversal.FeiSuoBuff, instanceId));
}
async Task OnEnterFeiSuoCrouch(RpcContext ctx)
{
var state = GetWorldState(ctx);
if (!IsActiveProtagonist(state))
{
ctx.Session.Log.Info($"[WEB] feisuo-enter ignored template={state.ActiveSpiritTemplateId} protagonist=false");
return;
}
await PublishInitialCapabilityBuffs(ctx);
await EnsureFeiSuoActionBuff(ctx);
ctx.Session.Log.Info($"[WEB] feisuo-enter ack=true transient={WebTraversal.FeiSuoBuff}");
}
async Task OnFeiSuoSuccess(RpcContext ctx, int feiSuoId)
{
var state = GetWorldState(ctx);
if (!IsActiveProtagonist(state))
{
ctx.Session.Log.Info($"[WEB] feisuo-success ignored template={state.ActiveSpiritTemplateId} protagonist=false");
return;
}
await EnsureFeiSuoActionBuff(ctx);
ctx.Session.Log.Info($"[WEB] feisuo-success id={feiSuoId} transient={WebTraversal.FeiSuoBuff}");
}
async Task OnLeaveFeiSuoCrouch(RpcContext ctx)
{
var state = GetWorldState(ctx);
if (state.ActiveFeiSuoBuffInstanceId == 0)
return;
var instanceId = state.ActiveFeiSuoBuffInstanceId;
var unitId = state.ActiveFeiSuoBuffUnitId != 0
? state.ActiveFeiSuoBuffUnitId
: state.ActiveSpiritUnitId;
state.ActiveFeiSuoBuffInstanceId = 0;
state.ActiveFeiSuoBuffUnitId = 0;
await ctx.NotifyAsync(MethodId.SyncUnitRemoveBuff,
WorldCodec.UnitRemoveBuff(unitId, instanceId));
ctx.Session.Log.Info($"[WEB] feisuo-leave transient-removed instance={instanceId}");
}
async Task OnClientBuffAdd(RpcContext ctx, ulong unitId, uint buffId)
{
var state = GetWorldState(ctx);
if (WebTraversal.IsWebBuff(buffId) && !IsActiveProtagonist(state, unitId))
{
ctx.Session.Log.Info($"[WEB] client-buff-add denied unit={unitId} template={state.ActiveSpiritTemplateId} buff={buffId} protagonist=false");
return;
}
// SwingBuff/WallRushBuff are action states, not unlocks. The 4229938 client requests them
// when an action starts and removes them when it ends. Track one exact instance per unit/buff
// so repeated client requests refresh the state instead of stacking permanent super-armor.
if (WebTraversal.IsClientLifecycleBuff(buffId))
{
var key = (unitId, buffId);
if (state.ActiveClientWebBuffInstances.Remove(key, out var previousInstanceId))
await ctx.NotifyAsync(MethodId.SyncUnitRemoveBuff, WorldCodec.UnitRemoveBuff(unitId, previousInstanceId));
var lifecycleInstanceId = state.NextBuffInstanceId++;
state.ActiveClientWebBuffInstances[key] = lifecycleInstanceId;
await ctx.NotifyAsync(MethodId.SyncUnitAddBuff,
WorldCodec.UnitAddBuff(unitId, buffId, lifecycleInstanceId));
ctx.Session.Log.Info($"[WEB] lifecycle-add unit={unitId} buff={buffId} instance={lifecycleInstanceId} refreshed={previousInstanceId != 0}");
return;
}
var instanceId = state.NextBuffInstanceId++;
await ctx.NotifyAsync(MethodId.SyncUnitAddBuff, WorldCodec.UnitAddBuff(unitId, buffId, instanceId));
if (buffId == WebTraversal.FeiSuoBuff)
{
state.ActiveFeiSuoBuffInstanceId = instanceId;
state.ActiveFeiSuoBuffUnitId = unitId;
}
if (buffId == WebTraversal.FeiSuoBuff || WebTraversal.IsProtected(buffId))
ctx.Session.Log.Info($"[WEB] client-buff-add unit={unitId} buff={buffId} instance={instanceId}");
}
async Task OnClientBuffRemove(RpcContext ctx, ulong unitId, uint buffId)
{
var state = GetWorldState(ctx);
if (WebTraversal.IsWebBuff(buffId) && !IsActiveProtagonist(state, unitId))
return;
if (WebTraversal.IsClientLifecycleBuff(buffId))
{
var key = (unitId, buffId);
if (state.ActiveClientWebBuffInstances.Remove(key, out var lifecycleInstanceId))
{
await ctx.NotifyAsync(MethodId.SyncUnitRemoveBuff,
WorldCodec.UnitRemoveBuff(unitId, lifecycleInstanceId));
ctx.Session.Log.Info($"[WEB] lifecycle-remove unit={unitId} buff={buffId} instance={lifecycleInstanceId}");
}
return;
}
if (buffId == WebTraversal.FeiSuoBuff)
{
if (state.ActiveFeiSuoBuffInstanceId != 0)
{
var instanceId = state.ActiveFeiSuoBuffInstanceId;
var transientUnitId = state.ActiveFeiSuoBuffUnitId != 0
? state.ActiveFeiSuoBuffUnitId
: unitId;
state.ActiveFeiSuoBuffInstanceId = 0;
state.ActiveFeiSuoBuffUnitId = 0;
await ctx.NotifyAsync(MethodId.SyncUnitRemoveBuff,
WorldCodec.UnitRemoveBuff(transientUnitId, instanceId));
ctx.Session.Log.Info($"[WEB] client-buff-remove transient={buffId} instance={instanceId}");
}
// Client 4229938 reports grapple teardown by buff *id*, not by instance.
// Locally that teardown can consume the configured resident grapple gate as well as the short-lived
// action copy. This is especially visible on the female protagonist: the first grapple
// succeeds, then no second client-buff-add is emitted. Re-arm one untracked resident gate
// after teardown for either protagonist. It is deliberately NOT stored as
// ActiveFeiSuoBuffInstanceId; that slot is reserved for the next action copy only.
await Task.Delay(WebTraversal.GrappleRearmDelayMs);
if (IsActiveProtagonist(state, unitId))
{
var residentInstanceId = state.NextBuffInstanceId++;
await ctx.NotifyAsync(MethodId.SyncUnitAddBuff,
WorldCodec.UnitAddBuff(unitId, WebTraversal.FeiSuoBuff, residentInstanceId));
ctx.Session.Log.Info($"[WEB] persistent-gate-reassert unit={unitId} buff={WebTraversal.FeiSuoBuff} instance={residentInstanceId}");
}
return;
}
if (!WebTraversal.IsProtected(buffId))
return;
await ctx.NotifyAsync(MethodId.SyncUnitAddBuff, WorldCodec.UnitAddBuff(unitId, buffId, state.NextBuffInstanceId++));
ctx.Session.Log.Info($"[WEB] protected-buff-reassert unit={unitId} buff={buffId}");
}
async Task ClearClientWebLifecycleBuffs(RpcContext ctx, ulong unitId, string phase)
{
var state = GetWorldState(ctx);
var active = state.ActiveClientWebBuffInstances
.Where(x => x.Key.UnitId == unitId)
.ToArray();
foreach (var item in active)
{
state.ActiveClientWebBuffInstances.Remove(item.Key);
await ctx.NotifyAsync(MethodId.SyncUnitRemoveBuff,
WorldCodec.UnitRemoveBuff(unitId, item.Value));
}
if (active.Length > 0)
ctx.Session.Log.Info($"[WEB] lifecycle-clear phase={phase} unit={unitId} count={active.Length}");
}
}