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