using Ananta.SDK.Serialization; using Ananta.Server.ClientData.Client4229938; using Ananta.Server.Configuration; using Ananta.Server.Protocol.Client4229938; namespace Ananta.Server.Handlers.Game; /// /// 嘉姆入侵敌方地盘(Gangster Area / 势力地盘占领)。 /// /// 官方这套玩法客户端是全的:MapSubSystem_Gangster.lua、GangsterAreaClientState.lua、 /// BigMapComp_GangsterArea.lua,占领结果只是一个 S2C 推送 /// SyncFactionInfluenceAreaOccupy(payload 只有 areaId + occupy 两个字段)。 /// 服务端这边主线原本**一个字节都没有**,所以这是纯新增,不动任何既有琉轮。 /// /// 一次入侵 = 三步,每步都能单独触发,方便蹚: /// 1. 传送到入侵点用现成的 TeleportWithLoadingAsync; /// 2. 两个随从用 SpawnAgentAsync(SpawnNpcAsync 那条走不通 —— 它们是 companion,没有自己的 FightSpirit); /// 3. 占领 = 推一个 Notify。 /// /// ⛑ 本文件不负责把玩家变成嘉姆:切换角色是另一套琉轮,玩家自己在角色界面切到嘉姆后再用这里。 /// 服务端只是「把人送到地方、把小弟放出来、tell 客户端这块地占了」。 /// internal sealed partial class GameRouter { /// /// 入侵时带在身边的两个随从。 /// /// ⛑⛑ 2026-10-01 改过:原来是 40651920 拥护者 / 40651921 格斗家,那是 /// UnlockConditionDes="成立犬魔社" 的两条 —— 玩家没解锁,所以 TAB 轮盘里根本 /// 不出现它们(轮盘下发的 slot 只有 10520000 和 10520017)。按一个没解锁的随从来铺, /// 玩家眼里就是「召了两个不认识的人出来」,跟玩法对不上。 /// /// 真正跟着玩家的是轮盘那两个槽,解锁条件是「通过角色手帐认可度解锁」: /// slot 10520000 → agent 40651922(犬魔社-晴 / DK9 - Sunny,机动协同突击者) /// slot 10520017 → agent 40155016(盖斯 / Gus,机动协同突击者) /// 依据是 2026-10-01 的实机日志:[SUMMON-WHEEL] first sighting of slot id 10520000 / 10520017。 /// 剩下三条(10520044→40155056 勇次、10520050→40651920、10520052→40651921)轮盘没发过,不默认带。 /// private const uint GarmCompanionSunny = 40651922; // slot 10520000 — 犬魔社-晴 / DK9 - Sunny private const uint GarmCompanionGus = 40155016; // slot 10520017 — 盖斯 / Gus /// The player must already be playing as Garm for any of this to make narrative sense. private const uint GarmFightSpiritId = 15020989; /// /// 推一次 SyncFactionInfluenceAreaOccupy(64830440):uint32 areaId + bool occupy。 /// /// wire 已对着客户端 RPCDeserializeAuto.lua 的 midToReader[64830440] 核过 —— /// 它不是登录包,lua 那份是权威源(硬规则 239)。UxWriter.Bool 写 1 字节 0/1, /// 正好对 reader:ReadBoolean()。 /// private static async Task<(bool Ok, string Message)> SyncAreaOccupyAsync(uint areaId, bool occupy) { var target = GameAdminBridge.RequirePlayerSession(); if (target is null) return (false, "还没有玩家会话(先用游戏客户端登录)"); var body = new UxWriter().U32(areaId).Bool(occupy).ToArray(); await target.Value.Session.NotifyAsync( MethodId.SyncFactionInfluenceAreaOccupy, body, CancellationToken.None); // 2026-10-01:服务端推送不进包日志(console 只记收到的包),所以这行是唯一的落地证据。 // 下次再有人说「点了没反应」,先 grep 这个前缀。 target.Value.Session.Log.Info( $"[GARM] invasion occupy pushed areaId={areaId} occupy={occupy} ({body.Length}B)"); return (true, $"SyncFactionInfluenceAreaOccupy areaId={areaId} occupy={occupy} ({body.Length}B)"); } /// /// 放一个随从。 /// /// ⛑⛑ hostile 必须传 true:SpawnAgentAsync 开头就按 /// AgentSummonCatalog 里登记的敌友属性对账,不一致直接拒 /// (IsHostileClass(enemyClassType) => ect is 1 or 2 or 4 or 6 or 7, /// 而 40651920/40651921 的 EnemyClassType=1 ⇒ 在目录里算敌对单位)。 /// 换来不被追打的是后面三个开关:engage=false 不给攻击 AI(也就不进敌人账本, /// 主线 EnemyAiActions 因此不会插手它), /// forceEnemyRelation=false 不标敌对关系(否则玩家能锁定自己人)。 /// /// ⛑⛑ 2026-10-01:camp 原来是硬编码 0 ⇒ UnitCamp.MonsterDefault ⇒ 客户端把它当敌人 /// (挂血条、可锁定、Companion 行为树不接管、空手出招)。而这两个模板在 AgentConfig 里 /// 的 Camp 本来就是 1 = UnitCamp.PlayerDefault,它们的树分别是 /// Companion_JiaMu_Shuangmawei(晴)/ Companion_Guys(盖斯)—— 战斗能力写在 /// 行为树里、不在 AI 行为集里,所以服务端查不到攻击模组是正常的,别硬配。 /// 现在读 gameplay.aether.companionCamp(默认 1)。 /// private static async Task<(bool Ok, string Message, ulong Uid)> SpawnCompanionAsync( uint agentId, float x, float y, float z, float facing) { return await GameAdminBridge.SpawnAgentAsync( agentId, x, y, z, facing, hostile: true, // 匹配 AgentSummonCatalog 的登记,不是"让他打你" engage: false, forceEnemyRelation: false, campOverride: PrivateServerConfigStore.Current.Gameplay.Aether.CompanionCamp); } /// /// 一次完整的入侵。三步都能单跑:对应布尔开关。返回给人看的多行明细。 /// internal static async Task<(bool Ok, string Message, string Detail)> RunFactionInvasionAsync( int eventId, bool teleport, bool summon, bool occupy) { var lines = new List(); var loadError = FactionInvasionSpotCatalog4229938.LoadError; if (loadError is not null) return (false, $"入侵点位没加载起来:{loadError}", ""); var spot = FactionInvasionSpotCatalog4229938.ByEventId(eventId); if (spot is null) return (false, $"找不到 eventId={eventId} 的入侵点(表里共 {FactionInvasionSpotCatalog4229938.Count} 条)", ""); lines.Add($"目标:{spot.EventName}(eventId={spot.EventId}, areaId={spot.AreaId}, {spot.AreaName})"); lines.Add($" {spot.TypeName} · faction={spot.FactionId} · raid={spot.Raid}"); var target = GameAdminBridge.RequirePlayerSession(); if (target is null) return (false, "还没有玩家会话(先用游戏客户端登录)", string.Join("\n", lines)); // ---------- 1. 传送 ---------- if (teleport) { if (!spot.HasPosition) { lines.Add(" 传送:❌ 这个点在官方数据里 CenterPos 全是 0(type=1 大本营常见),跳过"); } else { var (ok, msg) = await GameAdminBridge.TeleportWithLoadingAsync(spot.X, spot.Y, spot.Z, null); lines.Add($" 传送:{(ok ? "✅" : "❌")} ({spot.X:F1}, {spot.Y:F1}, {spot.Z:F1}) {msg}"); if (!ok) return (false, "传送失败,后面两步没跑", string.Join("\n", lines)); // 落地需要时间;紧接着放人会把随从丢在传送前的位置。 await Task.Delay(1200); } } // ---------- 2. 两个随从 ---------- if (summon) { // 全 0 pose == "掉在玩家跟前"(SpawnAgentAsync 内部的约定),省得自己算落点。 var (okA, msgA, uidA) = await SpawnCompanionAsync(GarmCompanionSunny, 0f, 0f, 0f, 0f); lines.Add($" 随从1 晴({GarmCompanionSunny}):{(okA ? "✅" : "❌")} uid={uidA} {msgA}"); var (bindA, bindMsgA) = await BindCompanionAsync(uidA); lines.Add($" 原生随从绑定:{(bindA ? "✅" : "❌")} {bindMsgA}"); await Task.Delay(200); var (okB, msgB, uidB) = await SpawnCompanionAsync(GarmCompanionGus, 0f, 0f, 0f, 0f); lines.Add($" 随从2 盖斯({GarmCompanionGus}):{(okB ? "✅" : "❌")} uid={uidB} {msgB}"); var (bindB, bindMsgB) = await BindCompanionAsync(uidB); lines.Add($" 原生随从绑定:{(bindB ? "✅" : "❌")} {bindMsgB}"); // 2026-10-01:实机日志里这两个随从带 `no-hp-bar` 且有 `hackable=true`,站在原地不动, // 玩家会以为「什么都没发生」。留一行说明,别下次再从零查。 lines.Add(" ⚠ 随从模板没有 ShowHPBar/InitWeaponId(无血条)、attacks=none(不主动出手),"); lines.Add(" 生成成功时客户端会立刻查它的交互按钮 —— 日志里能看到 AskInteractCmd,那就是它出现了。"); } // ---------- 3. 占领 ---------- if (occupy) { var (ok, msg) = await SyncAreaOccupyAsync((uint)spot.AreaId, true); lines.Add($" 占领:{(ok ? "✅" : "❌")} {msg}"); if (ok) lines.Add(" ⚠ 地图变色由客户端自己画;要看到效果请打开大地图,且角色需为嘉姆。"); if (!ok) return (false, "占领推送失败", string.Join("\n", lines)); } lines.Add($" (提醒:请在角色界面先切到嘉姆 spiritId={GarmFightSpiritId},本流程不代切角色)"); return (true, "入侵流程已执行", string.Join("\n", lines)); } }