175 lines
7.1 KiB
C#
175 lines
7.1 KiB
C#
using Ananta.SDK.Network;
|
||
using Ananta.SDK.Serialization;
|
||
using Ananta.Server.Configuration;
|
||
using Ananta.Server.Protocol.Client4229938;
|
||
using SceneMethods = Ananta.Server.RpcTypes.Client4229938.Methods.GameScene;
|
||
|
||
namespace Ananta.Server.Handlers.Game;
|
||
|
||
/// <summary>
|
||
/// 家具摆放探针(T1-22 排障用)—— 在玩家脚下周围摆一圈**指定 pathId 的真家具**。
|
||
///
|
||
/// 为什么要有它:网格流(68956797 / 68969593)到底有没有在客户端实例化东西,光看日志说不清;
|
||
/// 而 <c>SpawnGadgetAsync</c> 这条**显式摆放**通道是已经验证过的(健身房器材 / PropRestock 都走它,
|
||
/// 麦当劳座椅实测摆出来过)。所以拿它当基准:
|
||
/// · 摆出来的椅子看得到 ⇒ 显式摆放通,家具缺失纯粹是**缺坐标**(PropRestock 只覆盖 55/5116 格);
|
||
/// · 连摆在脚边都看不到 ⇒ 摆放通道本身有问题,先修通道,别再往坐标表里填数。
|
||
///
|
||
/// ⛑️ 全部参数走配置 + 热重载,改 pathId / 数量不用重新编译;想再摆一次就把 <c>gen</c> +1。
|
||
/// </summary>
|
||
internal sealed partial class GameRouter
|
||
{
|
||
private const string DebugPlaceKey = "client4229938-debug-place";
|
||
|
||
internal sealed class DebugPlaceSessionState
|
||
{
|
||
internal object SyncRoot { get; } = new();
|
||
|
||
/// <summary>上一次摆放用的 gen。gen 变了才重新摆 —— 这就是「热重载再试一次」的开关。</summary>
|
||
internal int LastGen = int.MinValue;
|
||
|
||
/// <summary>合成 uid 的自增源,高位起跳避免撞上客户端烘焙出来的真实 id。</summary>
|
||
internal long Seq = 0x5000000000000000;
|
||
|
||
/// <summary>
|
||
/// 当前场上由本探针摆出去的 uid。两个用途:
|
||
/// ① `gen` 变化时先把上一轮撤掉再摆新的(否则旧的不消失,越摆越多);
|
||
/// ② `enabled=false` 时整体撤掉 —— 这就是「清场」开关,不用重连。
|
||
/// ⛑ 不能走 `RemoveGadgetsAsync(null)`:那会把 `state.SpawnedGadgets` 里
|
||
/// PropRestock / GymEquipment 摆的全场物件一起清掉。
|
||
/// </summary>
|
||
internal List<ulong> PlacedUids { get; } = new();
|
||
}
|
||
|
||
private static DebugPlaceSessionState DebugPlaceStateOf(TcpSession session)
|
||
{
|
||
if (session.Items.TryGetValue(DebugPlaceKey, out var existing)
|
||
&& existing is DebugPlaceSessionState typed)
|
||
return typed;
|
||
|
||
var created = new DebugPlaceSessionState();
|
||
session.Items[DebugPlaceKey] = created;
|
||
return created;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 每 tick 调一次;gen 没变就直接返回,所以不会刷屏。
|
||
/// </summary>
|
||
internal static async Task TickDebugPlaceAsync(
|
||
TcpSession session, WorldEntryState state, float px, float pz)
|
||
{
|
||
var cfg = PrivateServerConfigStore.Current.World.Content.DebugPlace;
|
||
if (cfg.PathId <= 0)
|
||
return;
|
||
|
||
var owner = DebugPlaceStateOf(session);
|
||
List<ulong>? pendingClear = null;
|
||
|
||
// 撤场优先:enabled 关掉、或 gen 变了,都先把上一轮摆的撤干净。
|
||
// ⛑ 以前 `enabled=false` 是直接 return 的 —— 摆出去的东西只能靠重连才消失。
|
||
bool needPlace;
|
||
lock (owner.SyncRoot)
|
||
{
|
||
needPlace = cfg.Enabled && cfg.Gen != owner.LastGen;
|
||
var needClear = (!cfg.Enabled || needPlace) && owner.PlacedUids.Count > 0;
|
||
if (needClear)
|
||
pendingClear = new List<ulong>(owner.PlacedUids);
|
||
if (needPlace)
|
||
owner.LastGen = cfg.Gen;
|
||
}
|
||
|
||
if (pendingClear is not null)
|
||
await ClearDebugPlacedAsync(session, state, owner, pendingClear);
|
||
|
||
if (!needPlace)
|
||
return;
|
||
|
||
float py;
|
||
lock (state.SyncRoot)
|
||
py = state.LastReportedPlayerPosition.Y;
|
||
if (!float.IsFinite(py))
|
||
py = 0f;
|
||
|
||
var channel = cfg.Channel switch
|
||
{
|
||
1 => GadgetSendChannel.Aoi,
|
||
3 => GadgetSendChannel.AddList,
|
||
_ => GadgetSendChannel.Add,
|
||
};
|
||
|
||
var count = Math.Clamp(cfg.Count, 1, 16);
|
||
var placed = 0;
|
||
|
||
for (var i = 0; i < count; i++)
|
||
{
|
||
// 均匀撒一圈:+X 固定偏移会撞墙/在背后,摆一圈就不用管朝向了。
|
||
var angle = 2.0 * Math.PI * i / count;
|
||
var x = px + cfg.Radius * (float)Math.Cos(angle);
|
||
var z = pz + cfg.Radius * (float)Math.Sin(angle);
|
||
// 硬规则 181:facing 是**度数**,别把弧度直接喂进去。
|
||
var facing = (float)(angle * 180.0 / Math.PI);
|
||
|
||
ulong uid;
|
||
lock (owner.SyncRoot)
|
||
uid = (ulong)System.Threading.Interlocked.Increment(ref owner.Seq);
|
||
|
||
if (cfg.LogOnly)
|
||
{
|
||
session.Log.Info($"[DEBUG-PLACE] (log-only) pathId={cfg.PathId} #{i} " +
|
||
$"@({x:0.##},{py + cfg.OffsetY:0.##},{z:0.##}) facing={facing:0.##} uid=0x{uid:X}");
|
||
placed++;
|
||
continue;
|
||
}
|
||
|
||
var result = await SpawnGadgetAsync(session, state, cfg.PathId, x, py + cfg.OffsetY, z,
|
||
facing, channel, uid, cfg.IScale);
|
||
if (result.Ok)
|
||
{
|
||
lock (owner.SyncRoot)
|
||
owner.PlacedUids.Add(uid);
|
||
placed++;
|
||
}
|
||
else
|
||
session.Log.Warn($"[DEBUG-PLACE] #{i} 放置失败:{result.Message}");
|
||
}
|
||
|
||
session.Log.Info($"[DEBUG-PLACE] pathId={cfg.PathId} gen={cfg.Gen} 摆放 {placed}/{count} " +
|
||
$"channel={(int)channel} radius={cfg.Radius} iScale={cfg.IScale} " +
|
||
$"中心=({px:0.##},{py:0.##},{pz:0.##})");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 把本探针摆出去的那批 gadget 撤掉:`SyncGadgetRemoveList`(68199571, `List<ulong> ids` + int reason)。
|
||
///
|
||
/// ⛑ 只撤 <c>owner.PlacedUids</c> 记下来的这批 —— 不能用 <c>RemoveGadgetsAsync(state)</c> 全清,
|
||
/// 那份 <c>state.SpawnedGadgets</c> 里还混着 PropRestock / GymEquipment 摆的全场物件,会一起被端掉。
|
||
/// ⛑ 硬规则 238:移除是批量包,日志一行汇总,不逐条打。
|
||
/// </summary>
|
||
private static async Task ClearDebugPlacedAsync(
|
||
TcpSession session, WorldEntryState state, DebugPlaceSessionState owner, List<ulong> ids)
|
||
{
|
||
lock (owner.SyncRoot)
|
||
{
|
||
owner.PlacedUids.RemoveAll(ids.Contains);
|
||
}
|
||
|
||
await session.NotifyAsync(
|
||
MethodId.SyncGadgetRemoveList,
|
||
UxSerializer.Serialize(new SceneMethods.SyncGadgetRemoveListArgs4229938
|
||
{
|
||
ids = ids,
|
||
reason = GadgetDisposeReasonGm,
|
||
}),
|
||
CancellationToken.None);
|
||
|
||
lock (state.SyncRoot)
|
||
{
|
||
foreach (var id in ids)
|
||
state.SpawnedGadgets.Remove(id);
|
||
}
|
||
|
||
session.Log.Info($"[DEBUG-PLACE] clear count={ids.Count} " +
|
||
$"ids=[{string.Join(",", ids.Select(i => $"0x{i:X}"))}]");
|
||
}
|
||
}
|