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

238 lines
10 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 System.Globalization;
using Ananta.SDK.Network;
using Ananta.SDK.Rpc;
using Ananta.SDK.Serialization;
using Ananta.Server.Configuration;
using Ananta.Server.Protocol.Client4229938;
namespace Ananta.Server.Handlers.Game;
/// <summary>
/// ★★ 第五条管线:**Plate(盘)演出** —— <c>SyncPlateAddList</c>(68554224)。
///
/// 为什么需要它:可玩角色按日程站到跑步机 / 娃娃机 / 床旁边,但**不会真的用它们**
/// (只是站着玩手机)。官方的生活 NPC 是场景里 Spoon 图上的一个「盘」——
/// <c>SpoonAgentUnit</c> 同时带 <c>AgentDataSetsActivityCfgId</c> /
/// <c>ActivityPlateCreator</c> / <c>ActivityEnterPoint</c>,而盘要由服务端推下来客户端才会建
/// (硬规则 157)。光在出场包里填活动 id 是不够的。
///
/// 客户端形状(`UX.Game.PlateInfo`,dump 的 <c>06_RUNTIME_CODE/.../Auto.Client/UX/Game/PlateInfo.cs</c>):
/// <code>
/// ulong UniqueId; int GraphId; UXVector3 Position; UXVector3 Rotation;
/// Dictionary&lt;int,ulong&gt; GadgetDic / DestructibleDic / DynamicAgentDic / DynamicVehicleDic;
/// Dictionary&lt;int,int&gt; AgentDic / VehicleDic / StaticNpcDic / RoomDic;
/// uint FavorNpcActivityId; // 包尾再跟一个 uint taskId
/// </code>
///
/// ⛑⛑ 68554224 在 <c>RPCDeserializeAuto.lua</c> 里是 <b>CSharpOnly</b> ⇒ 客户端 reader 是
/// C# 生成的,我们拿不到它的线格式。所以这里<b>手写字节</b>,所有「有没有 0xFF 标记 /
/// count 用哪种写法」都做成配置开关 —— 试错不用重编译,改配置再推一次即可。
/// 判据只有一条:客户端回 <c>AskPlateLoadComplete</c>(67658390) /
/// <c>AskPlateResourceReady</c>(67916920),这两个 ack 在 unknown-methods.log 里
/// <b>从来没有出现过</b>(它们是服务端先推盘之后才发的),一旦出现就说明线格式对了。
/// </summary>
internal sealed partial class GameRouter
{
/// <summary>常驻 NPC 账本的 session key(与 <c>GameRouter.RosterNpc.cs</c> 同一个)。</summary>
private const string RosterNpcStateKeyForPlate = "client4229938-roster-npcs";
// =================================================================================
// ① C2S ack:只要出现就是通关信号,全部接住并打日志(INVOKE 必须回执,否则客户端 await 卡死)
// =================================================================================
/// <summary>
/// C2S <c>AskPlateLoadComplete</c>(67658390) —— ★ 通关判据 ①:盘的图加载完了。
/// </summary>
[Handler(MethodId.AskPlateLoadComplete, HandlerPacketKind.Invoke)]
private async Task OnAskPlateLoadComplete(Connection conn, UxRpcMessage msg)
{
conn.Log.Info($"[PLATE] ★★★ AskPlateLoadComplete bytes={msg.Body.Length} " +
$"hex={Hex(msg.Body, 32)}");
await conn.ReturnEmptyOkAsync(msg);
}
/// <summary>
/// C2S <c>AskPlateResourceReady</c>(67916920) —— ★ 通关判据 ②:盘要的资源(NPC / 物件)齐了。
/// </summary>
[Handler(MethodId.AskPlateResourceReady, HandlerPacketKind.Invoke)]
private async Task OnAskPlateResourceReady(Connection conn, UxRpcMessage msg)
{
conn.Log.Info($"[PLATE] ★★★ AskPlateResourceReady bytes={msg.Body.Length} " +
$"hex={Hex(msg.Body, 32)}");
await conn.ReturnEmptyOkAsync(msg);
}
/// <summary>
/// C2S <c>AskPlateAgentCreateConfirmed</c>(67163577) —— 盘上的 agent 造好了。
/// </summary>
[Handler(MethodId.AskPlateAgentCreateConfirmed, HandlerPacketKind.Invoke)]
private async Task OnAskPlateAgentCreateConfirmed(Connection conn, UxRpcMessage msg)
{
conn.Log.Info($"[PLATE] ★ AskPlateAgentCreateConfirmed bytes={msg.Body.Length} " +
$"hex={Hex(msg.Body, 32)}");
await conn.ReturnEmptyOkAsync(msg);
}
/// <summary>
/// C2S <c>AskNotifyPlateGameStallEvent</c>(67733503) —— 盘上的小游戏/摊位事件。
/// </summary>
[Handler(MethodId.AskNotifyPlateGameStallEvent, HandlerPacketKind.Invoke)]
private async Task OnAskNotifyPlateGameStallEvent(Connection conn, UxRpcMessage msg)
{
conn.Log.Info($"[PLATE] ★ AskNotifyPlateGameStallEvent bytes={msg.Body.Length} " +
$"hex={Hex(msg.Body, 32)}");
await conn.ReturnEmptyOkAsync(msg);
}
private static string Hex(byte[]? bytes, int max)
{
if (bytes is null || bytes.Length == 0) return "(empty)";
var n = Math.Min(bytes.Length, max);
return Convert.ToHexString(bytes, 0, n);
}
// =================================================================================
// ② S2C:推盘
// =================================================================================
/// <summary>HTTP / 世界就绪 两处共用的入口。返回 0 = 没发(配置关了)。</summary>
internal static async Task<int> PushPlateAsync(TcpSession session, PlatePilotSettings cfg)
{
if (!cfg.Enabled)
return 0;
var uniqueId = ParseUniqueId(cfg.UniqueId);
// 绑人:先去常驻 NPC 账本里找这个角色此刻在不在场上(拿到它的 ulong uid),
// 不在就用它的 AgentId 当 spoonAgentId 兜底。
ulong rosterUid = 0;
uint agentId = 0;
if (!string.IsNullOrWhiteSpace(cfg.NpcBind) && cfg.SpiritId != 0)
{
agentId = GameAdminBridge.AgentIdForSpirit(cfg.SpiritId);
if (session.Items.TryGetValue(RosterNpcStateKeyForPlate, out var boxed)
&& boxed is RosterNpcSessionState roster)
{
lock (roster.SyncRoot)
{
if (roster.Active.TryGetValue(cfg.SpiritId, out var placed))
rosterUid = placed.UnitId;
}
}
}
var body = BuildPlateAddListBody(cfg, uniqueId, rosterUid, agentId);
await session.NotifyAsync(MethodId.SyncPlateAddList, body, CancellationToken.None);
session.Log.Info($"[PLATE] push graph={cfg.GraphId} uid={uniqueId} " +
$"at=({cfg.X:0.##},{cfg.Y:0.##},{cfg.Z:0.##}) favor={cfg.FavorNpcActivityId} " +
$"task={cfg.TaskId} bind={cfg.NpcBind} slot={cfg.SlotIndex} " +
$"rosterUid={rosterUid} agent={agentId} bytes={body.Length} " +
$"hex={Hex(body, 48)}");
return body.Length;
}
/// <summary>世界就绪钩子:配置 <c>pushOnWorldReady=true</c> 才推。</summary>
internal static async Task EnsurePlateAsync(TcpSession session)
{
var cfg = PrivateServerConfigStore.Current.Gameplay.Plate;
if (!cfg.Enabled || !cfg.PushOnWorldReady)
return;
await PushPlateAsync(session, cfg);
}
// =================================================================================
// ③ 手写线格式(旋钮全在配置里)
// =================================================================================
/// <summary>
/// 包体 = [rootMarker] + 列表(1 个 PlateInfo) + taskId。
/// ⛑ 顺序照抄 <c>UX.Game.PlateInfo</c> 的字段声明顺序,一个都不能挪。
/// </summary>
private static byte[] BuildPlateAddListBody(
PlatePilotSettings cfg, ulong uniqueId, ulong rosterUid, uint agentId)
{
var w = new UxWriter();
if (cfg.RootMarker) w.U8(0xFF);
// —— List<PlateInfo> ls ——
if (cfg.ListMarker) w.U8(0xFF);
WriteCount(w, cfg.ListCountEncoding, 1);
if (cfg.ItemMarker) w.U8(0xFF);
w.U64(uniqueId);
w.I32(cfg.GraphId);
WriteVec3(w, cfg.VectorMarker, cfg.X, cfg.Y, cfg.Z);
WriteVec3(w, cfg.VectorMarker, cfg.RotX, cfg.RotY, cfg.RotZ);
// 八个字典,声明顺序不可变;只有被 NpcBind 选中的那个填一条,其余留空。
var bind = (cfg.NpcBind ?? "").Trim().ToLowerInvariant();
var slot = cfg.SlotIndex;
WriteEmptyDict(w, cfg); // GadgetDic (int → ulong)
WriteEmptyDict(w, cfg); // DestructibleDic (int → ulong)
WriteDictInt32(w, cfg, bind == "agent", slot, (int)agentId); // AgentDic (int → int)
WriteEmptyDict(w, cfg); // VehicleDic (int → int)
WriteDictUInt64(w, cfg, bind == "dynamicagent", slot, rosterUid); // DynamicAgentDic
WriteEmptyDict(w, cfg); // DynamicVehicleDic(int → ulong)
WriteDictInt32(w, cfg, bind == "staticnpc", slot, (int)rosterUid); // StaticNpcDic(int → int)
WriteEmptyDict(w, cfg); // RoomDic (int → int)
w.U32(cfg.FavorNpcActivityId);
// —— uint taskId ——
w.U32(cfg.TaskId);
return w.ToArray();
}
private static void WriteVec3(UxWriter w, bool marker, float x, float y, float z)
{
if (marker) w.U8(0xFF);
w.F32(x); w.F32(y); w.F32(z);
}
private static void WriteEmptyDict(UxWriter w, PlatePilotSettings cfg)
{
if (cfg.DictMarker) w.U8(0xFF);
WriteCount(w, cfg.DictCountEncoding, 0);
}
private static void WriteDictInt32(UxWriter w, PlatePilotSettings cfg, bool fill, int slot, int value)
{
if (cfg.DictMarker) w.U8(0xFF);
WriteCount(w, cfg.DictCountEncoding, fill ? 1 : 0);
if (!fill) return;
w.I32(slot);
w.I32(value);
}
private static void WriteDictUInt64(UxWriter w, PlatePilotSettings cfg, bool fill, int slot, ulong value)
{
if (cfg.DictMarker) w.U8(0xFF);
WriteCount(w, cfg.DictCountEncoding, fill ? 1 : 0);
if (!fill) return;
w.I32(slot);
w.U64(value);
}
private static void WriteCount(UxWriter w, string? encoding, int count)
{
switch ((encoding ?? "").Trim().ToLowerInvariant())
{
case "raw7": w.Raw7(count); break;
case "int32": w.I32(count); break;
default: w.Int7(count); break; // 偏置:线上写 count+1
}
}
private static ulong ParseUniqueId(string raw)
{
if (ulong.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var v))
return v;
return 830000000001UL;
}
}