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

175 lines
7.1 KiB
C#
Raw 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.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&lt;ulong&gt; 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}"))}]");
}
}