using System.Buffers.Binary;
using Ananta.SDK.Rpc;
using Ananta.Server.Protocol.Client4229938;
using Auto = Ananta.Server.RpcTypes.Client4229938.Auto;
using GameMethods = Ananta.Server.RpcTypes.Client4229938.Methods.Game;
namespace Ananta.Server.Handlers.Game;
///
/// GM 控制台(ALT+F1)职业命令族 —— GmJobTake / GmJobStart / GmJobQuit /
/// GmJobPromote / GmSetJobLevel / GmJobEnd。
///
/// 移植自捐赠端 DremkaPS(2026-09-24)。他们这一族里 GmTaskRecord4229938 的 TaskState 字节
/// 标了 "verified in game",是整份捐赠材料里极少数实机验证过的东西;其余(钱包 / 台词 / 过场 /
/// 敌群)捐赠端自己承认"排队做完、都还没实机验证"。
///
/// ⛑ 三条本服的取舍,与捐赠端不同:
/// 1. 推送是「合并」不是「替换」。捐赠端把 spiritJobs 整个换成 GM 记录,本服则把它
/// 叠加在 配置驱动的那份之上 ——
/// 否则玩家在控制台点一条职业命令,角色原本的职业就整片消失。
/// 2. CurrentJob 默认 100(Jobless)不是 0。0 不是合法 UrbanJobConfig id,客户端
/// SpiritJobManager 会去解引用一个不存在的配置(本服踩过,见
/// RuntimePayloadFactory.DefaultSpiritJobInfo4229938 的注释)。
/// 3. 职业记录带天赋。捐赠端因无表而填 TalentInfo = null / RegisterTime = 0;
/// 本服统一走 ,与本服 7k 验证过的 wire 一致。
///
/// 捐赠端注释还点明了一件关键事实:客户端自己在本地生成 GM 实体,GM invoke 的调用点根本不看
/// 返回值。所以这些 handler 的价值是「服务端认得这条指令、不再掉进 unknown-methods.log,并且
/// 能把职业变化真的推给客户端」,而不是「返回一个客户端会用的值」。
///
internal sealed partial class GameRouter
{
[Handler(MethodId.GmJobTake)]
private Task GmJobTake(Connection conn, UxRpcMessage msg)
=> HandleGmJobAsync(conn, msg, GmJobAction4229938.Take);
[Handler(MethodId.GmJobStart)]
private Task GmJobStart(Connection conn, UxRpcMessage msg)
=> HandleGmJobAsync(conn, msg, GmJobAction4229938.Start);
[Handler(MethodId.GmJobQuit)]
private Task GmJobQuit(Connection conn, UxRpcMessage msg)
=> HandleGmJobAsync(conn, msg, GmJobAction4229938.Quit);
[Handler(MethodId.GmJobPromote)]
private Task GmJobPromote(Connection conn, UxRpcMessage msg)
=> HandleGmJobAsync(conn, msg, GmJobAction4229938.Promote);
[Handler(MethodId.GmSetJobLevel)]
private Task GmSetJobLevel(Connection conn, UxRpcMessage msg)
=> HandleGmJobAsync(conn, msg, GmJobAction4229938.SetLevel);
///
/// GmJobEnd(65110787) 是这一族里唯一不带参数的(客户端序列化器为空),
/// 结束的是「当前职业」而不是命令里点名的那个。
///
[Handler(MethodId.GmJobEnd)]
private async Task GmJobEnd(Connection conn, UxRpcMessage msg)
{
var state = GetWorldState(msg.Context);
Auto.SpiritJob job;
string detail;
lock (state.GmJobs.SyncRoot)
{
var target = state.GmJobs.CurrentJob;
if (target == 0 || target == RuntimePayloadFactory.JoblessUrbanJob4229938)
{
job = new Auto.SpiritJob();
detail = "no current job (ignored)";
}
else
{
(job, detail) = ApplyGmJob(state.GmJobs, target, 0, GmJobAction4229938.End);
}
}
conn.Log.Info($"[GM-JOB] GmJobEnd {detail}");
await PushGmJobInfoAsync(conn, state, "GmJobEnd");
if (msg.IsInvoke)
await conn.ReturnAsync(msg, job);
}
// ------------------------------------------------------------------ shared path
private async Task HandleGmJobAsync(Connection conn, UxRpcMessage msg, GmJobAction4229938 action)
{
var (jobClassId, level) = ReadGmJobArgs(msg);
var state = GetWorldState(msg.Context);
Auto.SpiritJob job;
string detail;
lock (state.GmJobs.SyncRoot)
(job, detail) = ApplyGmJob(state.GmJobs, jobClassId, level, action);
conn.Log.Info($"[GM-JOB] {action} {detail}");
await PushGmJobInfoAsync(conn, state, action.ToString());
if (msg.IsInvoke)
await conn.ReturnAsync(msg, job);
}
///
/// 解参数。契约解码失败时退回裸字节 —— GM 控制台的序列化器是客户端自己生成的,
/// 服务端这一侧的契约一旦和它对不齐,最坏情况应该是「还能读出 jobClassId」而不是整条丢弃。
///
private static (uint JobClassId, byte Level) ReadGmJobArgs(UxRpcMessage msg)
{
// GmSetJobLevel 的 body 是 5 字节(uint32 + byte),其余四条是 4 字节。
// GmJobCommand 对 5 字节 body 也能"解码成功"(只读前 4 字节),所以必须先试长的那个。
if (msg.Body.Length >= 5
&& msg.TryGetArgs(out var setLevel)
&& setLevel is not null)
{
return (setLevel.JobClassId, setLevel.Level);
}
if (msg.TryGetArgs(out var cmd) && cmd is not null)
return (cmd.JobClassId, 1);
if (msg.Body.Length == 0)
return (0, 1);
return (msg.Body.Length >= 4 ? BinaryPrimitives.ReadUInt32LittleEndian(msg.Body) : 0u,
msg.Body.Length >= 5 ? msg.Body[4] : (byte)1);
}
private static (Auto.SpiritJob Job, string Detail) ApplyGmJob(
GmJobState4229938 state,
uint jobClassId,
byte level,
GmJobAction4229938 action)
{
if (jobClassId == 0)
return (new Auto.SpiritJob(), "no job class id (ignored)");
state.HasGmOverride = true;
var record = state.Ensure(jobClassId, level);
switch (action)
{
case GmJobAction4229938.Take:
// 只登记职业,不切换当前职业(客户端的 take 与 start 是两个动作)。
break;
case GmJobAction4229938.Start:
state.CurrentJob = jobClassId;
break;
case GmJobAction4229938.Quit:
state.Jobs.Remove(jobClassId);
if (state.CurrentJob == jobClassId)
state.CurrentJob = RuntimePayloadFactory.JoblessUrbanJob4229938;
break;
case GmJobAction4229938.Promote:
record.Level = (byte)Math.Min(byte.MaxValue, record.Level + 1);
break;
case GmJobAction4229938.SetLevel:
record.Level = Math.Max((byte)1, level);
break;
case GmJobAction4229938.End:
if (state.CurrentJob == jobClassId)
state.CurrentJob = RuntimePayloadFactory.JoblessUrbanJob4229938;
break;
}
record.Action = action;
record.UpdatedUtc = DateTime.UtcNow;
var job = RuntimePayloadFactory.GmJobRecord4229938(jobClassId, record.Level);
return (job, $"job={jobClassId} action={action} level={job.Level} "
+ $"tracked={state.Jobs.Count} current={state.CurrentJob}");
}
///
/// 推 SyncSpiritJobInfo(64913222)。
///
/// ⛑ 合并而非替换:以 的配置结果为底,
/// GM 记录覆盖/追加在上面。currentJob 只有在 GM 真的动过职业之后才改用控制台的值。
///
private async Task PushGmJobInfoAsync(Connection conn, WorldEntryState state, string trigger)
{
var spiritId = state.ActiveSpiritTemplateId;
var base_ = RuntimePayloadFactory.SpiritJobInfo4229938(spiritId);
var jobs = new Dictionary(base_.AvailableJobs);
uint currentJob;
lock (state.GmJobs.SyncRoot)
{
foreach (var (id, record) in state.GmJobs.Jobs)
jobs[id] = RuntimePayloadFactory.GmJobRecord4229938(id, record.Level);
currentJob = state.GmJobs.HasGmOverride ? state.GmJobs.CurrentJob : base_.CurrentJob;
}
// 0 不是合法 id —— 兜底到 Jobless(100),别把客户端的 SpiritJobManager 打崩。
if (currentJob == 0)
currentJob = RuntimePayloadFactory.JoblessUrbanJob4229938;
await conn.NotifyAsync(MethodId.SyncSpiritJobInfo, new GameMethods.SyncSpiritJobInfo4229938
{
spiritId = spiritId,
spiritJobs = jobs,
currentJob = currentJob,
});
conn.Log.Info($"[GM-JOB] push {trigger} spirit={spiritId} jobs={jobs.Count} "
+ $"current={currentJob} gmOverride={state.GmJobs.HasGmOverride}");
}
}