using Ananta.SDK.Network;
using Ananta.Server.ClientData.Client4229938;
using Ananta.Server.Configuration;
using Ananta.Server.Protocol.Client4229938;
namespace Ananta.Server.Handlers.Game;
///
/// General restock of client-blob props: whenever the player walks into a world cell whose props
/// were captured out of LX6.Item.GadgetBlob, the server places them itself.
///
/// This is the same trick as 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 SceneitemConfig.json by pathId,
/// and the grid-AOI push (68956797) alone does not make the client instantiate them, so the server
/// has to place them with SyncGadgetAdd using the blob's own uid and placement.
///
/// ⛔ PackedGadgetInfo.iScale must be 0 — see PropRestockCatalog4229938 remarks.
///
/// Rollback: world.content.propRestock.enabled = false.
///
internal sealed partial class GameRouter
{
private const string PropRestockKey = "client4229938-prop-restock";
internal sealed class PropRestockSessionState
{
internal object SyncRoot { get; } = new();
/// "raid:cx,cz" cells already stocked this session.
internal HashSet Placed { get; } = new(StringComparer.Ordinal);
internal int PlacedCount;
internal bool WarnedEmpty;
/// 已经提示过「本张图没有点位数据」—— 每个会话只说一次。
internal bool WarnedNoRaid;
/// Catalog 加载期告警已经打印过(每会话一次)。★ R143 补:见下文。
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;
}
/// Reconnect / scene switch: forget what was stocked so the next visit re-places it.
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;
}
}
///
/// Called from the grid-AOI tick. Stocks every captured cell within
/// PropRestockSettings.RadiusCells of the player's current cell, once per cell.
///
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}");
}
}
}