238 lines
10 KiB
C#
238 lines
10 KiB
C#
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<int,ulong> GadgetDic / DestructibleDic / DynamicAgentDic / DynamicVehicleDic;
|
||
/// Dictionary<int,int> 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;
|
||
}
|
||
}
|