170 lines
9.9 KiB
C#
170 lines
9.9 KiB
C#
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));
|
|||
|
|
}
|
|||
|
|
}
|