using System.Runtime.CompilerServices;
using Ananta.SDK.Network;
using Ananta.SDK.Rpc;
using Ananta.Server.ClientData.Client4229938;
using Ananta.Server.Protocol.Client4229938;
namespace Ananta.Server.Handlers.Game;
///
/// 里希(Garm) 之外的另一条「可控非角色原型」:科斯机甲(2026-09-26,移植捐赠端 7k-v0.5
/// GameRouter.Kesi.cs)。
///
/// 根因(客户端配置侧,已在本服 ClientData 上复核):
/// - FightSpiritConfig 15022030 = "特摄战原型-科斯",IsPublish=false / CanJoin=false /
/// Invisible=true ⇒ 它永远不在 Characters() 花名册里,roster 上点了也不会出现。
/// - SceneitemConfig 80003500 = "科斯冲锋枪",WeaponBelong=15022030、
/// FixedFightSkill=39010048,且没有 WheelIcon ⇒ 常规军械库过滤器会把它判掉,
/// 所以它连「随便塞把枪」都做不到,必须走 CombatCatalogRepository 里那份孤立装配。
/// - 客户端 CheckIsKesi 拿 cardId 15022030 认机甲(捐赠端注释)。
///
/// 供给链三条,缺一不可:
/// 1. SyncPlayerAddNewSpirit(64691088) 增量补 SpiritViewData —— 登录包里没有它,
/// 客户端 SpiritManager.Id_to_Tid 查不到 tid,后面所有和它相关的包都会被静默丢掉;
/// 2. CombatCatalogRepository 里那份科斯 loadout(slot0 科斯冲锋枪 80003500 + 风格 39010048);
/// 3. 走本服已在用的 做身份移交 —— 不另发明一套切人。
///
/// ⛑ 这是需实机验证的实验入口:捐赠端自己也标注"尚需游戏内验证"。
/// 能确认的只有服务端协议侧齐了;模型加载、镜头、机甲技能面板和按键都得进游戏看。
///
internal sealed partial class GameRouter
{
private static readonly ConditionalWeakTable KesiControls4229938 = new();
private sealed class KesiControlRuntime4229938
{
// 与普通角色切换共用一把锁:GM 切机甲和游戏内主动切人不能互相打断,
// 否则两份 PlayerInfo/SpiritViewData 会交错下发,客户端 roster 直接错位。
internal readonly SemaphoreSlim Gate = new(1, 1);
/// 进入机甲前的角色 template;return 时切回它。
internal uint ReturnTemplate;
/// 只有走过 ControlKesi 才允许 接受 Kesi 作为目标。
internal bool Unlocked;
}
internal static async Task<(bool Ok, string Message)> ControlKesi4229938(TcpSession session, bool enter)
{
var runtime = KesiControls4229938.GetValue(session, _ => new());
await runtime.Gate.WaitAsync(CancellationToken.None);
try
{
var state = GetStateIfExists(session);
if (state is not { Ready: true, CombatProfilePublished: true, HasLastReportedPlayerTransform: true }
|| state.ActiveSpiritUnitId == 0 || state.PendingSwitchTemplateId != 0 || state.PendingTeleportId != 0
|| GetVehicleRuntimeState(session).CurrentVehicleId != 0)
return (false, "请进入游戏,离开载具并等待角色和场景加载完成。");
var p = state.LastReportedPlayerPosition;
if (!float.IsFinite(p.X) || !float.IsFinite(p.Y) || !float.IsFinite(p.Z)
|| !float.IsFinite(state.LastReportedPlayerRotation.Y))
return (false, "角色坐标尚未就绪,请移动几步后重试。");
var kesi = ClientConfigRepository.Kesi;
if (enter && state.ActiveSpiritTemplateId == kesi.TemplateId)
return (true, "当前已经是科斯机甲,不重复生成。");
if (!enter && state.ActiveSpiritTemplateId != kesi.TemplateId)
return (true, "当前不在科斯机甲中,无需返回。");
var target = enter ? kesi.TemplateId : runtime.ReturnTemplate;
if (!enter && ClientConfigRepository.ControlCharacter(target) is null)
target = Profile.InitialSpiritTemplateId;
var ctx = new RpcContext(
session,
new RpcPacket(RpcPacketKind.Invoke, MethodId.AskSwitchSpirit, 0, Array.Empty()),
CancellationToken.None);
if (enter)
{
// ⛑ 移交归属之前先自检:本服这份 client-data 抓包要是缺了科斯的原生武器/技能行,
// 切过去会得到一个空手且技能全空的单位,比直接拒绝难排查。
// Sceneitem 80003500 / FightSkill 39010048 已在本服 ClientData 上核到。
var weapon = CombatCodec.DefaultWeapon(kesi.TemplateId);
if (weapon.TemplateId != 80003500 || weapon.Style.Id != 39010048)
return (false, "缺少科斯原生武器/技能配置,已取消切换。");
// 记得来路,否则永远回不去 —— return 时 Target 就是它。
runtime.ReturnTemplate = state.ActiveSpiritTemplateId;
runtime.Unlocked = true;
// ⛑ 防御:旧版本会在 LeftBehindCharacters 里留一具科斯(T-pose 残骸)。
// 进来前先把那条记录抹掉,否则 OnSwitchSpirit 会把它当成「已在大世界」而走
// recreate 分支,反而又把那具旧身体重建一次。
state.LeftBehindCharacters.Remove(kesi.TemplateId);
await ctx.NotifyAsync(MethodId.SyncPlayerAddNewSpirit, RuntimePayloadFactory.KesiSpirit4229938(state));
}
await new GameRouter().OnSwitchSpirit(ctx, target, allowShow: false);
var ok = state.ActiveSpiritTemplateId == target && state.PendingSwitchTemplateId == 0;
session.Log.Info($"[KESI] enter={enter} ok={ok} active={state.ActiveSpiritTemplateId}/{state.ActiveSpiritUnitId} " +
$"return={runtime.ReturnTemplate} reasonWeapon={CombatCodec.DefaultWeapon(kesi.TemplateId).TemplateId}");
return (ok, ok
? enter
? "已下发科斯原生角色、控制切换与技能。请检查模型、移动和机甲技能面板;可点“返回原角色”退出。"
: "已切回进入机甲前的角色,保留当前位置。"
: "切换未完成,请查看服务端 [KESI]/[SWITCH-MIN] 日志。");
}
catch (Exception ex)
{
session.Log.Warn($"[KESI] command failed: {ex.Message}");
return (false, "机甲指令未完成,请查看服务端 [KESI] 日志。");
}
finally { runtime.Gate.Release(); }
}
}