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

171 lines
9.9 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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));
}
}