This repository has been archived on 2026-10-31. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files

237 lines
10 KiB
C#
Raw Permalink Normal View History

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;
}
}