185 lines
7.5 KiB
C#
185 lines
7.5 KiB
C#
using Ananta.SDK.Network;
|
||
using Ananta.Server.ClientData.Client4229938;
|
||
using Ananta.Server.Configuration;
|
||
using Ananta.Server.Protocol.Client4229938;
|
||
|
||
namespace Ananta.Server.Handlers.Game;
|
||
|
||
/// <summary>
|
||
/// General restock of client-blob props: whenever the player walks into a world cell whose props
|
||
/// were captured out of <c>LX6.Item.GadgetBlob</c>, the server places them itself.
|
||
///
|
||
/// This is the same trick as <see cref="TickGymEquipmentAsync"/> widened to every prop family —
|
||
/// vending machines, indoor chairs and sofas, claw machines, mini-game cabinets, basketball
|
||
/// courts, bouncy beds, market stalls. None of them are in <c>SceneitemConfig.json</c> by pathId,
|
||
/// and the grid-AOI push (68956797) alone does not make the client instantiate them, so the server
|
||
/// has to place them with <c>SyncGadgetAdd</c> using the blob's own uid and placement.
|
||
///
|
||
/// ⛔ <c>PackedGadgetInfo.iScale</c> must be 0 — see PropRestockCatalog4229938 remarks.
|
||
///
|
||
/// Rollback: <c>world.content.propRestock.enabled = false</c>.
|
||
/// </summary>
|
||
internal sealed partial class GameRouter
|
||
{
|
||
private const string PropRestockKey = "client4229938-prop-restock";
|
||
|
||
internal sealed class PropRestockSessionState
|
||
{
|
||
internal object SyncRoot { get; } = new();
|
||
|
||
/// <summary>"raid:cx,cz" cells already stocked this session.</summary>
|
||
internal HashSet<string> Placed { get; } = new(StringComparer.Ordinal);
|
||
|
||
internal int PlacedCount;
|
||
|
||
internal bool WarnedEmpty;
|
||
|
||
/// <summary>已经提示过「本张图没有点位数据」—— 每个会话只说一次。</summary>
|
||
internal bool WarnedNoRaid;
|
||
|
||
/// <summary>Catalog 加载期告警已经打印过(每会话一次)。★ R143 补:见下文。</summary>
|
||
internal bool WarnedCatalog;
|
||
}
|
||
|
||
private static PropRestockSessionState PropRestockStateOf(TcpSession session)
|
||
{
|
||
if (session.Items.TryGetValue(PropRestockKey, out var existing)
|
||
&& existing is PropRestockSessionState typed)
|
||
return typed;
|
||
|
||
var created = new PropRestockSessionState();
|
||
session.Items[PropRestockKey] = created;
|
||
return created;
|
||
}
|
||
|
||
/// <summary>Reconnect / scene switch: forget what was stocked so the next visit re-places it.</summary>
|
||
internal static void ResetPropRestock(TcpSession session)
|
||
{
|
||
var owner = PropRestockStateOf(session);
|
||
lock (owner.SyncRoot)
|
||
{
|
||
owner.Placed.Clear();
|
||
owner.PlacedCount = 0;
|
||
owner.WarnedEmpty = false;
|
||
owner.WarnedNoRaid = false;
|
||
owner.WarnedCatalog = false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Called from the grid-AOI tick. Stocks every captured cell within
|
||
/// <c>PropRestockSettings.RadiusCells</c> of the player's current cell, once per cell.
|
||
/// </summary>
|
||
internal static async Task TickPropRestockAsync(
|
||
TcpSession session, WorldEntryState state, float px, float pz)
|
||
{
|
||
var cfg = PrivateServerConfigStore.Current.World.Content.PropRestock;
|
||
if (!cfg.Enabled)
|
||
return;
|
||
|
||
var owner = PropRestockStateOf(session);
|
||
if (PropRestockCatalog4229938.Count == 0)
|
||
{
|
||
lock (owner.SyncRoot)
|
||
{
|
||
if (owner.WarnedEmpty)
|
||
return;
|
||
owner.WarnedEmpty = true;
|
||
}
|
||
session.Log.Info($"[RESTOCK] 没有可用点位:{PropRestockCatalog4229938.LoadError}");
|
||
return;
|
||
}
|
||
|
||
// ★★★★★ R143 —— 加载期告警必须露一次脸(v1.9 R95 教训,铁律 102)。
|
||
// 典型:某条 blob uid 超出 ulong ⇒ 加载器按 mod 2^64 回绕。若这条不打印,
|
||
// 「uid 解析失败 ⇒ 每次补货 mint 新实例 ⇒ 同坐标多出一件」可以潜伏几个版本,
|
||
// 而表现只是用户看到「两个摊叠在一起」(R95 就漏看过一次)。
|
||
// ⚠️ 本文件的 raid 维度逻辑(下方)是 v2.0 上游新增的,比 v1.9 那份更完整 —— 两者都要。
|
||
lock (owner.SyncRoot)
|
||
{
|
||
if (!owner.WarnedCatalog)
|
||
{
|
||
owner.WarnedCatalog = true;
|
||
foreach (var warning in PropRestockCatalog4229938.LoadWarnings)
|
||
session.Log.Warn($"[RESTOCK] 配置告警:{warning}");
|
||
}
|
||
}
|
||
|
||
// ⛔ (cx,cz) 只在单张地图内唯一:新启 23300888 和 重霄·凌云 23300999 的格子编号会撞车。
|
||
// 少这一步,第二张图采集来的物件会刷在第一张图上。
|
||
var raid = state.ActiveRaidId != 0 ? state.ActiveRaidId : PrivateServerConfigStore.Current.World.RaidId;
|
||
if (PropRestockCatalog4229938.CountForRaid(raid) == 0)
|
||
{
|
||
lock (owner.SyncRoot)
|
||
{
|
||
if (owner.WarnedNoRaid)
|
||
return;
|
||
owner.WarnedNoRaid = true;
|
||
}
|
||
// 常见情况:整表数据都来自另一张地图。别静默什么都不刷,说清楚。
|
||
session.Log.Info($"[RESTOCK] 点位表共 {PropRestockCatalog4229938.Count} 格," +
|
||
$"但没有 raid={raid} 的数据(已有 raid:" +
|
||
$"{string.Join(",", PropRestockCatalog4229938.Raids)})");
|
||
return;
|
||
}
|
||
|
||
var cellSize = PropRestockCatalog4229938.CellSize > 0 ? PropRestockCatalog4229938.CellSize : 40;
|
||
var cx = (int)Math.Floor(px / cellSize);
|
||
var cz = (int)Math.Floor(pz / cellSize);
|
||
|
||
var channel = cfg.Channel switch
|
||
{
|
||
1 => GadgetSendChannel.Aoi,
|
||
3 => GadgetSendChannel.AddList,
|
||
_ => GadgetSendChannel.Add,
|
||
};
|
||
|
||
foreach (var cell in PropRestockCatalog4229938.NearCell(raid, cx, cz, cfg.RadiusCells))
|
||
{
|
||
var key = $"{raid}:{cell.Cx},{cell.Cz}";
|
||
lock (owner.SyncRoot)
|
||
{
|
||
if (!owner.Placed.Add(key))
|
||
continue;
|
||
}
|
||
|
||
session.Log.Info($"[RESTOCK] raid={raid} 进入格子 ({cell.Cx},{cell.Cz}),补货 {cell.Items.Count} 件 " +
|
||
$"channel={channel} logOnly={cfg.LogOnly}");
|
||
|
||
if (cfg.LogOnly)
|
||
continue;
|
||
|
||
var placed = 0;
|
||
|
||
if (channel == GadgetSendChannel.AddList)
|
||
{
|
||
var specs = cell.Items
|
||
.Select(i => new GadgetSpawnSpec(i.PathId, i.X, i.Y, i.Z, i.Yaw, i.UniqueId, i.Name))
|
||
.ToList();
|
||
var batch = await SpawnGadgetListAsync(session, state, specs, cfg.IScale);
|
||
placed = batch.Ok ? batch.Count : 0;
|
||
if (!batch.Ok)
|
||
session.Log.Warn($"[RESTOCK] 格子 ({cell.Cx},{cell.Cz}) 批量放置失败:{batch.Message}");
|
||
}
|
||
else
|
||
{
|
||
foreach (var item in cell.Items)
|
||
{
|
||
var result = await SpawnGadgetAsync(
|
||
session, state, item.PathId, item.X, item.Y, item.Z, item.Yaw, channel,
|
||
item.UniqueId, cfg.IScale);
|
||
if (result.Ok)
|
||
placed++;
|
||
else
|
||
session.Log.Warn($"[RESTOCK] {item.Name}({item.PathId}) 放置失败:{result.Message}");
|
||
}
|
||
}
|
||
|
||
lock (owner.SyncRoot)
|
||
owner.PlacedCount += placed;
|
||
|
||
session.Log.Info($"[RESTOCK] 格子 ({cell.Cx},{cell.Cz}) 放置完成 {placed}/{cell.Items.Count}");
|
||
}
|
||
}
|
||
}
|