130 lines
7.0 KiB
C#
130 lines
7.0 KiB
C#
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})";
|
||
}
|