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;
///
/// ★★ 第五条管线:**Plate(盘)演出** —— SyncPlateAddList(68554224)。
///
/// 为什么需要它:可玩角色按日程站到跑步机 / 娃娃机 / 床旁边,但**不会真的用它们**
/// (只是站着玩手机)。官方的生活 NPC 是场景里 Spoon 图上的一个「盘」——
/// SpoonAgentUnit 同时带 AgentDataSetsActivityCfgId /
/// ActivityPlateCreator / ActivityEnterPoint,而盘要由服务端推下来客户端才会建
/// (硬规则 157)。光在出场包里填活动 id 是不够的。
///
/// 客户端形状(`UX.Game.PlateInfo`,dump 的 06_RUNTIME_CODE/.../Auto.Client/UX/Game/PlateInfo.cs):
///
/// ulong UniqueId; int GraphId; UXVector3 Position; UXVector3 Rotation;
/// Dictionary<int,ulong> GadgetDic / DestructibleDic / DynamicAgentDic / DynamicVehicleDic;
/// Dictionary<int,int> AgentDic / VehicleDic / StaticNpcDic / RoomDic;
/// uint FavorNpcActivityId; // 包尾再跟一个 uint taskId
///
///
/// ⛑⛑ 68554224 在 RPCDeserializeAuto.lua 里是 CSharpOnly ⇒ 客户端 reader 是
/// C# 生成的,我们拿不到它的线格式。所以这里手写字节,所有「有没有 0xFF 标记 /
/// count 用哪种写法」都做成配置开关 —— 试错不用重编译,改配置再推一次即可。
/// 判据只有一条:客户端回 AskPlateLoadComplete(67658390) /
/// AskPlateResourceReady(67916920),这两个 ack 在 unknown-methods.log 里
/// 从来没有出现过(它们是服务端先推盘之后才发的),一旦出现就说明线格式对了。
///
internal sealed partial class GameRouter
{
/// 常驻 NPC 账本的 session key(与 GameRouter.RosterNpc.cs 同一个)。
private const string RosterNpcStateKeyForPlate = "client4229938-roster-npcs";
// =================================================================================
// ① C2S ack:只要出现就是通关信号,全部接住并打日志(INVOKE 必须回执,否则客户端 await 卡死)
// =================================================================================
///
/// C2S AskPlateLoadComplete(67658390) —— ★ 通关判据 ①:盘的图加载完了。
///
[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);
}
///
/// C2S AskPlateResourceReady(67916920) —— ★ 通关判据 ②:盘要的资源(NPC / 物件)齐了。
///
[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);
}
///
/// C2S AskPlateAgentCreateConfirmed(67163577) —— 盘上的 agent 造好了。
///
[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);
}
///
/// C2S AskNotifyPlateGameStallEvent(67733503) —— 盘上的小游戏/摊位事件。
///
[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:推盘
// =================================================================================
/// HTTP / 世界就绪 两处共用的入口。返回 0 = 没发(配置关了)。
internal static async Task 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;
}
/// 世界就绪钩子:配置 pushOnWorldReady=true 才推。
internal static async Task EnsurePlateAsync(TcpSession session)
{
var cfg = PrivateServerConfigStore.Current.Gameplay.Plate;
if (!cfg.Enabled || !cfg.PushOnWorldReady)
return;
await PushPlateAsync(session, cfg);
}
// =================================================================================
// ③ 手写线格式(旋钮全在配置里)
// =================================================================================
///
/// 包体 = [rootMarker] + 列表(1 个 PlateInfo) + taskId。
/// ⛑ 顺序照抄 UX.Game.PlateInfo 的字段声明顺序,一个都不能挪。
///
private static byte[] BuildPlateAddListBody(
PlatePilotSettings cfg, ulong uniqueId, ulong rosterUid, uint agentId)
{
var w = new UxWriter();
if (cfg.RootMarker) w.U8(0xFF);
// —— List 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;
}
}