Files
Ananta-ps/Ananta.Server/Ananta.Handlers/Game/GameRouter.FactionInvasion.cs
T

170 lines
9.9 KiB
C#
Raw Normal View History

using Ananta.SDK.Serialization;
using Ananta.Server.ClientData.Client4229938;
using Ananta.Server.Configuration;
using Ananta.Server.Protocol.Client4229938;
namespace Ananta.Server.Handlers.Game;
/// <summary>
/// 嘉姆入侵敌方地盘(Gangster Area / 势力地盘占领)。
///
/// 官方这套玩法客户端是全的:<c>MapSubSystem_Gangster.lua</c>、<c>GangsterAreaClientState.lua</c>、
/// <c>BigMapComp_GangsterArea.lua</c>,占领结果只是一个 S2C 推送
/// <c>SyncFactionInfluenceAreaOccupy</c>(payload 只有 <c>areaId</c> + <c>occupy</c> 两个字段)。
/// 服务端这边主线原本**一个字节都没有**,所以这是纯新增,不动任何既有琉轮。
///
/// 一次入侵 = 三步,每步都能单独触发,方便蹚:
/// 1. 传送到入侵点用现成的 <c>TeleportWithLoadingAsync</c>;
/// 2. 两个随从用 <c>SpawnAgentAsync</c>(<c>SpawnNpcAsync</c> 那条走不通 —— 它们是 companion,没有自己的 FightSpirit);
/// 3. 占领 = 推一个 Notify。
///
/// ⛑ 本文件<b>不负责把玩家变成嘉姆</b>:切换角色是另一套琉轮,玩家自己在角色界面切到嘉姆后再用这里。
/// 服务端只是「把人送到地方、把小弟放出来、tell 客户端这块地占了」。
/// </summary>
internal sealed partial class GameRouter
{
/// <summary>
/// 入侵时带在身边的两个随从。
///
/// ⛑⛑ 2026-10-01 改过:原来是 40651920 拥护者 / 40651921 格斗家,那是
/// <c>UnlockConditionDes="成立犬魔社"</c> 的两条 —— 玩家<b>没解锁</b>,所以 TAB 轮盘里根本
/// 不出现它们(轮盘下发的 slot 只有 10520000 和 10520017)。按一个没解锁的随从来铺,
/// 玩家眼里就是「召了两个不认识的人出来」,跟玩法对不上。
///
/// 真正跟着玩家的是轮盘那两个槽,解锁条件是「通过角色手帐认可度解锁」:
/// slot 10520000 → agent 40651922(犬魔社-晴 / DK9 - Sunny,机动协同突击者)
/// slot 10520017 → agent 40155016(盖斯 / Gus,机动协同突击者)
/// 依据是 2026-10-01 的实机日志:<c>[SUMMON-WHEEL] first sighting of slot id 10520000 / 10520017</c>。
/// 剩下三条(10520044→40155056 勇次、10520050→40651920、10520052→40651921)轮盘没发过,不默认带。
/// </summary>
private const uint GarmCompanionSunny = 40651922; // slot 10520000 — 犬魔社-晴 / DK9 - Sunny
private const uint GarmCompanionGus = 40155016; // slot 10520017 — 盖斯 / Gus
/// <summary>The player must already be playing as Garm for any of this to make narrative sense.</summary>
private const uint GarmFightSpiritId = 15020989;
/// <summary>
/// 推一次 <c>SyncFactionInfluenceAreaOccupy</c>(64830440):uint32 areaId + bool occupy。
///
/// wire 已对着客户端 <c>RPCDeserializeAuto.lua</c> 的 <c>midToReader[64830440]</c> 核过 ——
/// 它不是登录包,lua 那份是权威源(硬规则 239)。<c>UxWriter.Bool</c> 写 1 字节 0/1,
/// 正好对 <c>reader:ReadBoolean()</c>。
/// </summary>
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)");
}
/// <summary>
/// 放一个随从。
///
/// ⛑⛑ <c>hostile</c> 必须传 <b>true</b>:<c>SpawnAgentAsync</c> 开头就按
/// <c>AgentSummonCatalog</c> 里登记的敌友属性对账,不一致直接拒
/// (<c>IsHostileClass(enemyClassType) => ect is 1 or 2 or 4 or 6 or 7</c>,
/// 而 40651920/40651921 的 <c>EnemyClassType=1</c> ⇒ 在目录里算敌对单位)。
/// 换来不被追打的是后面三个开关:<c>engage=false</c> 不给攻击 AI(也就<b>不进敌人账本</b>,
/// 主线 EnemyAiActions 因此不会插手它),
/// <c>forceEnemyRelation=false</c> 不标敌对关系(否则玩家能锁定自己人)。
///
/// ⛑⛑ 2026-10-01:<c>camp</c> 原来是硬编码 0 ⇒ UnitCamp.MonsterDefault ⇒ 客户端把它当敌人
/// (挂血条、可锁定、Companion 行为树不接管、空手出招)。而这两个模板在 AgentConfig 里
/// 的 <c>Camp</c> <b>本来就是 1 = UnitCamp.PlayerDefault</b>,它们的树分别是
/// <c>Companion_JiaMu_Shuangmawei</c>(晴)/ <c>Companion_Guys</c>(盖斯)—— 战斗能力写在
/// 行为树里、不在 AI 行为集里,所以服务端查不到攻击模组是正常的,别硬配。
/// 现在读 <c>gameplay.aether.companionCamp</c>(默认 1)。
/// </summary>
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);
}
/// <summary>
/// 一次完整的入侵。三步都能单跑:对应布尔开关。返回给人看的多行明细。
/// </summary>
internal static async Task<(bool Ok, string Message, string Detail)> RunFactionInvasionAsync(
int eventId, bool teleport, bool summon, bool occupy)
{
var lines = new List<string>();
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));
}
}