184 lines
7.5 KiB
C#
184 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}");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|