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

130 lines
7.0 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.SDK.Rpc;
using Ananta.Server.Protocol.Client4229938;
using SceneMethods = Ananta.Server.RpcTypes.Client4229938.Methods.GameScene;
namespace Ananta.Server.Handlers.Game;
/// <summary>
/// 环境 NPC 的<b>剧情 / 刺激事件收尾</b>(2026-10-02 自 7k-v0.6 捐赠端
/// <c>GameRouter.Plot.cs</c> 移植,三个全是 <b>Notify</b>)。
///
/// <para>★ 它<b>不驱动任务剧情</b>,只维护一个账本
/// <c>WorldEntryState.StaticNpcPreparedPlotEvents[agentEntityId]</c>
/// (= 这个环境 NPC 当前挂着哪条剧情事件,0 表示没有)。
/// 这条账本主线<b>早就有了</b>(<c>WorldEntryState.cs:27</c>,注释里就写着
/// "GameRouter.Plot uses it",但主线一直没有 Plot 文件 ⇒ 从来没人真正消费它)。
/// 铺市民 / DonorNpc / FunctionalNpc / RosterNpc 在生成时都会 <c>[id] = 0</c> 播种,
/// 骰子对手离场时 <c>Remove(id)</c>。</para>
///
/// <para>★ 三条的语义:</para>
/// <list type="number">
/// <item><c>AskPreparePlotEvent</c>(67966346):客户端说「这个 NPC 准备演 event X」⇒ 记账。</item>
/// <item><c>AskEndPreparePlotEvent</c>(67025961):准备结束 ⇒ 清账。</item>
/// <item><c>AskTriggerPlotEvent</c>(67895788):<b>演完了</b>,进入服务端反应阶段。
/// 我们的环境 NPC <b>没有剧情后续</b>,所以显式把它关掉 —— 发
/// <c>SyncPlotCurrentEvent(AgentEntityId, EventId=0)</c>,让客户端
/// <c>SelectStimOnline</c> 不再拿「已经演完的那条事件」的优先级下限去压新的刺激。
/// 只有账本里 pending 的那条对得上才发(<c>TryCompleteStaticNpcPlot</c>)。</item>
/// </list>
///
/// <para>⛑ 游戏里的可见效果:环境 NPC 演完一次反应(被撞、被喊、盘查反应……)之后
/// <b>能回到待机、能接受下一次刺激</b>,而不是卡在那条已完成的反应上不理人。</para>
///
/// <para>⛑ 坐标必须是有限数(<c>HasFinitePosition</c>):捐赠端用这个挡掉
/// 客户端偶尔发出来的 NaN / Infinity,避免日志和下游逻辑被脏数据带偏。</para>
///
/// <para>⛑ 回滚:把三个 <c>MethodId.*</c> 从 <c>GameRouter.cs</c> 的
/// <c>Enabled4229938MethodIds</c> 里删掉即可 —— Notify 不回包,
/// 不注册只是回到 <c>DefaultUnknownNotify4229938</c>(接受 + 打日志),不会卡客户端。</para>
/// </summary>
internal sealed partial class GameRouter
{
[Handler(MethodId.AskPreparePlotEvent, HandlerPacketKind.Notify)]
private Task AskPreparePlotEvent(Connection conn, UxRpcMessage msg)
{
if (msg.Body.Length != SceneMethods.AskPreparePlotEventArgs4229938.WireSize
|| !msg.TryGetArgs<SceneMethods.AskPreparePlotEventArgs4229938>(out var args)
|| args is null || !HasFinitePosition(args.Source.Source)
|| !HasFinitePosition(args.Source.Source2))
{
conn.Log.Warn($"[PLOT] AskPreparePlotEvent invalid payload bytes={msg.Body.Length}");
return Task.CompletedTask;
}
conn.Log.Info($"[PLOT] AskPreparePlotEvent event={args.EventId} agent={args.AgentEntityId} "
+ $"response={args.ResponseId} source=[{FormatPlotTarget(args.Source.Source)}] "
+ $"source2=[{FormatPlotTarget(args.Source.Source2)}]");
var state = GetStateIfExists(conn.Session);
if (state is not null)
lock (state.SyncRoot)
if (state.StaticNpcPreparedPlotEvents.ContainsKey(args.AgentEntityId))
state.StaticNpcPreparedPlotEvents[args.AgentEntityId] = args.EventId;
return Task.CompletedTask;
}
[Handler(MethodId.AskEndPreparePlotEvent, HandlerPacketKind.Notify)]
private Task AskEndPreparePlotEvent(Connection conn, UxRpcMessage msg)
{
if (msg.Body.Length != SceneMethods.AskEndPreparePlotEventArgs4229938.WireSize
|| !msg.TryGetArgs<SceneMethods.AskEndPreparePlotEventArgs4229938>(out var args)
|| args is null)
{
conn.Log.Warn($"[PLOT] AskEndPreparePlotEvent invalid payload bytes={msg.Body.Length}");
return Task.CompletedTask;
}
conn.Log.Info($"[PLOT] AskEndPreparePlotEvent event={args.EventId} agent={args.AgentEntityId}");
TryCompleteStaticNpcPlot(GetStateIfExists(conn.Session), args.AgentEntityId, args.EventId);
return Task.CompletedTask;
}
[Handler(MethodId.AskTriggerPlotEvent, HandlerPacketKind.Notify)]
private async Task AskTriggerPlotEvent(Connection conn, UxRpcMessage msg)
{
if (msg.Body.Length != SceneMethods.AskTriggerPlotEventArgs4229938.WireSize
|| !msg.TryGetArgs<SceneMethods.AskTriggerPlotEventArgs4229938>(out var args)
|| args is null || !HasFinitePosition(args.Parameter.Source)
|| !HasFinitePosition(args.Parameter.Source2))
{
conn.Log.Warn($"[PLOT] AskTriggerPlotEvent invalid payload bytes={msg.Body.Length}");
return;
}
conn.Log.Info($"[PLOT] AskTriggerPlotEvent event={args.EventId} agent={args.AgentEntityId} "
+ $"source=[{FormatPlotTarget(args.Parameter.Source)}] source2=[{FormatPlotTarget(args.Parameter.Source2)}] "
+ $"clientDmemOverrideCfgId={args.ClientDmemOverrideCfgId}");
// The native response has finished and entered its server-reaction phase.
// These ambient NPCs have no plot continuation. Explicitly close it so
// SelectStimOnline no longer uses the completed event's priority floor.
if (TryCompleteStaticNpcPlot(GetStateIfExists(conn.Session), args.AgentEntityId, args.EventId))
{
conn.Log.Info($"[PLOT] AMBIENT_RESPONSE_FINISHED agent={args.AgentEntityId} event={args.EventId} currentEvent=0");
await conn.NotifyAsync(MethodId.SyncPlotCurrentEvent,
new SceneMethods.SyncPlotCurrentEvent4229938 { AgentEntityId = args.AgentEntityId });
}
}
/// <summary>
/// 账本里这条 NPC 挂着的确实是 <paramref name="eventId"/> 就清成 0(表示「演完了」)。
/// <c>EventId == 0</c> 或账本里没有这个 NPC ⇒ 返回 false(不关、不发 SyncPlotCurrentEvent),
/// 免得把服务端没参与过的反应误关掉。
/// </summary>
internal static bool TryCompleteStaticNpcPlot(WorldEntryState? state, ulong agentId, uint eventId)
{
if (state is null || eventId == 0) return false;
lock (state.SyncRoot)
{
if (!state.StaticNpcPreparedPlotEvents.TryGetValue(agentId, out var pending) || pending != eventId)
return false;
state.StaticNpcPreparedPlotEvents[agentId] = 0;
return true;
}
}
private static bool HasFinitePosition(SceneMethods.PlotEventTarget4229938 target)
=> float.IsFinite(target.Position.X) && float.IsFinite(target.Position.Y) && float.IsFinite(target.Position.Z);
private static string FormatPlotTarget(SceneMethods.PlotEventTarget4229938 target)
=> $"id={target.Id} type={target.Type} pos=({target.Position.X:F2},{target.Position.Y:F2},{target.Position.Z:F2})";
}