Files

194 lines
11 KiB
C#
Raw Permalink Normal View History

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Ananta.SDK.Network;
using Ananta.SDK.Rpc;
using Ananta.SDK.Serialization;
using Ananta.Server.Configuration;
using Ananta.Server.Protocol.Client4229938;
using Ananta.Server.RpcTypes.Client4229938;
using Auto = Ananta.Server.RpcTypes.Client4229938.Auto;
using SceneMethods = Ananta.Server.RpcTypes.Client4229938.Methods.GameScene;
namespace Ananta.Server.Handlers.Game;
/// <summary>
/// ★ 诊断实验(2026-09-24 深夜):把 10 个球瓶改走**健身房器材那条路**推。
///
/// 起因(客户端日志给出的硬证据):
/// [error] ,scene item,910000000015, hold not found,
/// BowlingGameManager.lua:670 in 'getItemsFromIdList'
/// BowlingGameManager.lua:680 in 'InitSceneItems'
/// ⇒ 客户端确实收到了我们推的 id(910000000001..15,见 BowlingZoneInfo 的两个 id 列表),
/// 但 `SceneItemMgr.Instance:GetSceneItemHold(id)` 查不到 hold,
/// 于是 `sceneItemPinList` 为空 → `Rent(101)` 报 "pool[101] is nil" →
/// `BowlingPinSetter.CreatePin` 直接 return → 官方那套带模型的球瓶根本没被建出来。
/// 我们走的 `DestructibleInfo` 只造出「物理 + 音效」的实体,不产生 hold、不产生渲染。
///
/// 而本项目里**唯一被实测证明能生成"看得见"的物体**的通道是 gadget:
/// `GameRouter.GymEquipment.cs` → GadgetEntityInfo(Pack.PathId 直接给资源 id)
/// → `GadgetGridAOIIncrease` → `SyncGadgetAOIAddAndRemove`(68169646)
/// 健身房器材就是这么生成出来的,肉眼可见。
///
/// 所以本实验:同样用 gadget AOI 把东西推到球瓶位置,看客户端认不认。
///
/// 配置(config/bowling_wire.json):
/// "pinAsGadget": 1 打开本实验(0 = 关)
/// "gadgetProbePathId": N 对照组的 pathId,默认 79300636 = MiniGameMachine_Bowling
/// (GadgetConfig 里保龄球机器,写 0 = 不放对照组)
///
/// 一次测试能读出三件事:
/// ① 对照机器的 gadget 现身了?→ 说明「服务端推 gadget + AOI 通道」在球馆里也成立
/// ② 10 个球瓶位置现出物体了?→ 说明客户端能把 `pinPathId` 当资源 id 解析出来 ⇒ 收工
/// ③ 都不现 → gadget 通道认不得场景物 id,得转客户端侧(frida)方案
///
/// ⚠ 本实验**不动**原来的 DestructibleInfo 推送(物理/计分/音效照旧),
/// 只是在同一位置上再叠一层"看得见"的 gadget。两者互不干扰。
///
/// ★★ 2026-09-24 A 方案移植说明(本文件与 PrivateServer修改 版的差异)
///
/// 本仓库(DRMK-PS_09_HyNB-v1.7)里,机关那一族类型在 `...Methods.GameScene` 命名空间下,
/// 且 `GadgetEntityInfo.Position/Facing` 的类型是 `Auto.UXVector3`:
/// GadgetEntityInfo → Methods.GameScene(本仓库,下方用 SceneMethods. 限定)
/// PackedGadgetInfo → Methods.GameScene(= 我们那边的 GadgetPackInfo,**字段顺序前 13 项完全一致**,
/// 本仓库末位多出 extractionItemContainerId / isStatic 两个字段,
/// 默认 0 即可,与 v1.7 自己的机关推送同形)
/// 字段名对照(我们 → 本仓库):Jnc/Jnb/Jna → posX/posY/posZ;EulerX/Y/Z → eulerX/eulerY/eulerZ;
/// IScale → iScale;UniqueId → uniqueId;PathId → pathId。
/// ⚠ iScale 保持 100:这是**机关族**的正确取值(本仓库 PackedGadgetInfo.iScale 的注释写明
/// "Uniform scale in percent (100 = authored size)",且 GameRouter.Gadget.cs / GymEquipment.cs
/// 的默认参数就是 100)。我们那条「iScale 必须写 0」的教训针对的是**另一个**结构
/// `SceneItemPlaceInfoData +0x3C`,两者不要混。
///
/// ★★★ 第二处、也是**唯一改变语义**的一处:68169646 的顶层 args 换成本仓库既有的
/// `SceneMethods.SyncGadgetAOIAddAndRemoveArgs4229938`(6 字段),不再用我们那边的
/// `GymGadgetContracts.GadgetGridAOIIncrease`(7 字段)。理由是**证据打架时选宿主仓**:
///
/// (a) 我们那边的 7 字段版本,来源是 Drmk.Proxy 的 proxy/rpc_schema.json 读端 schema
/// `433`(已实测抽取,字段 = addInfos / indexList / addUniqueIds / removeIds /
/// activeIds / inactiveIds / reason(ReadByte)),但本项目自己的
/// `Ananta.RpcTypes\Client4229938\BowlingWireContracts.cs:271-276` 的注释把同一个
/// DTO 判为 "uncalled rpc_schema-era gym DTO"(把 Destructible 与 Gadget 两族混用)。
/// (b) 本仓库对 68169646 的既有写法就是 6 字段(GameRouter.Gadget.cs:257、Blackout.cs:335),
/// 且 GameRouter.Gadget.cs:33-38 自述这条通道 "which one the client honours … is still
/// unknown" —— 也就是说**本仓也从未验证过它**。
/// (c) 本文件是**诊断旁路**,默认关闭(`_pinAsGadget = 0`),不参与保龄球主功能。
/// 保龄球能不能玩,取决于服务端推球瓶实体(GameRouter.GameGround.cs + 本仓
/// BowlingWireContracts.cs 的 DestructibleGridAOIIncrease,schema 396)**以及客户端
/// BF1 补丁(AnantaBowlingLoader.exe)**,与本文件无关。
///
/// ⇒ 结论:既然两种形状都未在本仓验证过,就**不引入第三种形状、不搬入被判可疑的 DTO**,
/// 直接用宿主仓既有类型,保持本仓内部完全同形。若日后要试 7 字段形状,请回
/// `PrivateServer修改\Ananta.Server\Ananta.RpcTypes\Client4229938\GymGadgetContracts.cs`
/// 整体搬入,本文件只需把 payload 类型与字段名换回大驼峰即可。
/// </summary>
internal sealed partial class GameRouter
{
/// <summary>0 = 关;1 = 开。见 BowlingWireNow()。</summary>
private static int _pinAsGadget = 0;
/// <summary>对照组 pathId(默认保龄球机器)。0 = 不放对照组。</summary>
private static int _gadgetProbePathId = 79300636;
/// <summary>推送前等多少毫秒(热改,不用重编译)。客户端还没准备好的话可以往后挪。</summary>
private static int _gadgetPushDelayMs = 0;
/// <summary>
/// gadget instance id 号段。**2026-09-24 移植时改过**:源版(PrivateServer修改)用
/// <c>920000000000</c>,但本仓库(DRMK-PS_09_HyNB-v1.7)已经把这个段占了 ——
/// · GameRouter.GmWallet.cs:21 <c>GmItemSeqFloor = 920000000000L</c>(GM 钱包发道具,
/// 经 IdCounterStore 持久化计数器,见 GameRouter.Economy.cs:246)
/// · GameAdminBridge.cs:1608-1611 的注释把 920 段明确称作「GameRouter 内部那个计数器」,
/// 并把管理面板发道具的 <c>PanelItemSeqFloor</c> 抬到 950000000000 来避开它
/// ⇒ 沿用 920 会和 GM 钱包发出同一个 <c>PlayerPackItem.UniqueId</c>。
///
/// 本仓库号段图(已逐个核过,930/940 空闲):
/// 320000000000 GM 实体 · 330000000000 GM 敌人 · 910000000000 背包弹药 uid
/// 920000000000 GM 钱包发道具 · 950000000000 管理面板发道具
/// ⇒ 取 <b>940000000000</b>:落在 920 与 950 之间,语义上不与任何一侧重叠。
/// </summary>
// ★ 规则 104 适配(v0.3):源版是 `private static long _bowlingGadgetSeq = 940000000000L;`
// 进程内自增。本仓同样改用 IdCounterStore 落盘,floor 仍取 940000000000
// (920 GM 钱包 与 950 面板发道具 之间;球瓶诊断旁路默认关闭,不会真发号)。
private const string BowlingGadgetIdCounter = "Bowling.GadgetProbeId";
private const long BowlingGadgetIdFloor = 940000000000L;
private async Task PushBowlingPinGadgetsAsync(Connection conn, BowlingSession session, Vec3 playerPos)
{
if (_pinAsGadget == 0) return;
if (_gadgetPushDelayMs > 0)
{
try { await Task.Delay(_gadgetPushDelayMs); }
catch { return; }
if (_pinAsGadget == 0) return;
}
var lane = BowlingPickLane(playerPos.Z);
var laneHeadX = lane.GameNodeX + 0.24f;
var addInfos = new List<SceneMethods.GadgetEntityInfo>();
void AddGadget(float x, float y, float z, float eulerY, int pathId, string tag)
{
var id = (ulong)IdCounterStore.Next(BowlingGadgetIdCounter, BowlingGadgetIdFloor);
addInfos.Add(new SceneMethods.GadgetEntityInfo
{
InstanceId = id,
Position = new Auto.UXVector3 { X = x, Y = y, Z = z },
Facing = new Auto.UXVector3 { X = 0, Y = eulerY, Z = 0 },
Pack = new SceneMethods.PackedGadgetInfo
{
posX = x,
posY = y,
posZ = z,
eulerX = 0,
eulerY = eulerY,
eulerZ = 0,
iScale = 100,
uniqueId = id,
pathId = pathId,
},
});
conn.Log.Info($"[BOWLING-GADGET] {tag} id={id} pathId={pathId} at=({x:F2},{y:F2},{z:F2}) yaw={eulerY:F1}");
}
// 10 个球瓶位置(1-2-3-4 三角阵),用球瓶的 pathId
for (int r = 0; r < 4; r++)
{
int n = r + 1;
for (int c = 0; c < n; c++)
{
float lateral = (c - (n - 1) * 0.5f) * BowlingPinLateralStep;
float depth = r * BowlingPinDepthStep;
AddGadget(lane.PinApexX + depth, BowlingPinBaseY, lane.LaneZ + lateral, 0f,
_pinPath, $"pin[{r},{c}]");
}
}
// 对照组:一个保龄球机器 gadget,放在犯规线后 3 米(玩家身后),明显不挡道
if (_gadgetProbePathId != 0)
{
AddGadget(laneHeadX - 3.0f, BowlingPinBaseY, lane.LaneZ, 180f, _gadgetProbePathId, "CONTROL-machine");
}
// ★ 移植差异:本仓库对 68169646 的既有写法 = SceneMethods.SyncGadgetAOIAddAndRemoveArgs4229938
// (6 字段:addInfos / addPackSyncInfos / removeIds / activeIds / inacticeIds / reason),
// 见 GameRouter.Gadget.cs:255-262 与 GameRouter.Blackout.cs:334-336。
// 我们那边的 7 字段 gym DTO 在此不引入 —— 详见文件顶部说明。
// reason 保持我们那条线的原值 0(AoiAddAndRemoveReason.Init),因为这是**首次下发**;
// 本仓 GameRouter.Gadget.cs 用的是 2(AOIMove),两者含义不同,此处不跟随。
var payload = new SceneMethods.SyncGadgetAOIAddAndRemoveArgs4229938
{
addInfos = addInfos,
reason = 0,
};
var bytes = UxSerializer.Serialize(payload);
await conn.NotifyAsync(MethodId.SyncGadgetAOIAddAndRemove, bytes);
conn.Log.Info(
$"[BOWLING-GADGET] pushed {addInfos.Count} gadgets " +
$"(10 pins pathId={_pinPath} + control={_gadgetProbePathId}) args={nameof(SceneMethods.SyncGadgetAOIAddAndRemoveArgs4229938)} bytes={bytes.Length}");
}
}