116 lines
6.7 KiB
C#
116 lines
6.7 KiB
C#
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;
|
||
|
||
/// <summary>
|
||
/// 里希(Garm) 之外的另一条「可控非角色原型」:<b>科斯机甲</b>(2026-09-26,移植捐赠端 7k-v0.5
|
||
/// <c>GameRouter.Kesi.cs</c>)。
|
||
///
|
||
/// 根因(客户端配置侧,已在本服 ClientData 上复核):
|
||
/// - FightSpiritConfig <c>15022030</c> = "特摄战原型-科斯",<c>IsPublish=false / CanJoin=false /
|
||
/// Invisible=true</c> ⇒ 它<b>永远不在 Characters() 花名册里</b>,roster 上点了也不会出现。
|
||
/// - SceneitemConfig <c>80003500</c> = "科斯冲锋枪",<c>WeaponBelong=15022030</c>、
|
||
/// <c>FixedFightSkill=39010048</c>,且<b>没有 WheelIcon</b> ⇒ 常规军械库过滤器会把它判掉,
|
||
/// 所以它连「随便塞把枪」都做不到,必须走 CombatCatalogRepository 里那份孤立装配。
|
||
/// - 客户端 <c>CheckIsKesi</c> 拿 cardId <c>15022030</c> 认机甲(捐赠端注释)。
|
||
///
|
||
/// 供给链三条,缺一不可:
|
||
/// 1. <c>SyncPlayerAddNewSpirit</c>(64691088) 增量补 SpiritViewData —— 登录包里没有它,
|
||
/// 客户端 SpiritManager.Id_to_Tid 查不到 tid,后面所有和它相关的包都会被静默丢掉;
|
||
/// 2. CombatCatalogRepository 里那份科斯 loadout(slot0 科斯冲锋枪 80003500 + 风格 39010048);
|
||
/// 3. 走本服已在用的 <see cref="OnSwitchSpirit"/> 做身份移交 —— 不另发明一套切人。
|
||
///
|
||
/// ⛑ 这是<b>需实机验证</b>的实验入口:捐赠端自己也标注"尚需游戏内验证"。
|
||
/// 能确认的只有服务端协议侧齐了;模型加载、镜头、机甲技能面板和按键都得进游戏看。
|
||
/// </summary>
|
||
internal sealed partial class GameRouter
|
||
{
|
||
private static readonly ConditionalWeakTable<TcpSession, KesiControlRuntime4229938> KesiControls4229938 = new();
|
||
|
||
private sealed class KesiControlRuntime4229938
|
||
{
|
||
// 与普通角色切换共用一把锁:GM 切机甲和游戏内主动切人不能互相打断,
|
||
// 否则两份 PlayerInfo/SpiritViewData 会交错下发,客户端 roster 直接错位。
|
||
internal readonly SemaphoreSlim Gate = new(1, 1);
|
||
|
||
/// <summary>进入机甲前的角色 template;return 时切回它。</summary>
|
||
internal uint ReturnTemplate;
|
||
|
||
/// <summary>只有走过 ControlKesi 才允许 <see cref="OnSwitchSpirit"/> 接受 Kesi 作为目标。</summary>
|
||
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<byte>()),
|
||
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(); }
|
||
}
|
||
}
|