Files
Ananta-ps/Ananta.Server/Ananta.Handlers/Game/GameRouter.PropRestock.cs

185 lines
7.5 KiB
C#
Raw Permalink 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.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}");
}
}
}