This repository has been archived on 2026-10-31. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Ananta-ps/Ananta.Server/Ananta.Handlers/Game/GameRouter.GymEquipment.cs

201 lines
7.9 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>
/// Gym equipment the server places itself — the server-side half of what AnantaGymLoader.exe does
/// by frida injection.
///
/// Why it is needed: the wodegym_* prefabs (treadmill 1371296286, bench press 1443522735, squat
/// rack 434521152, sit-up board 344334257, bench 684457731 — verified 2026-09-23 by uid-pairing
/// spawned instance names against blob records; the loader's own hard-coded 79302023/22/21 and
/// 79000586 are WRONG for this build) are absent from GadgetCatalog.json and from
/// SceneitemConfig.json. Their only home is the client's own <c>LX6.Item.GadgetBlob</c>, which is
/// populated by a pipeline our grid-AOI push (68956797) never drives:
/// GadgetBlob.OnEnterScene("WorldMap_Release") → ReadHeadInfo → InitSimpleGrid →
/// OnAoiAddBlobCfgByGridIndex(x, z) per cell.
/// The loader injects those calls, then reads the blob dictionary
/// (<c>uniqueIdToSceneGridSeekId</c> → <c>GetGadgetBlobData</c> → PackedGadgetInfo) and finishes
/// with <c>SyncGadgetAddList(List&lt;GadgetEntityInfo&gt;)</c>. The minigames themselves are client
/// lua, so the server only owes the placement — which is exactly what this file does with the
/// coordinates captured into Configs/GymEquipment.json.
///
/// Rollback: <c>world.content.gymEquipment.enabled = false</c>.
/// </summary>
internal sealed partial class GameRouter
{
private const string GymEquipmentKey = "client4229938-gym-equipment";
internal sealed class GymEquipmentSessionState
{
internal object SyncRoot { get; } = new();
/// <summary>Venues already stocked this session. One shot per venue, like the loader.</summary>
internal HashSet<string> Placed { get; } = new(StringComparer.Ordinal);
internal int PlacedCount;
/// <summary>「切室内 / 传送中,本次跳过」只打一次日志,避免每 tick 刷屏。</summary>
internal bool WarnedBusy;
}
private static GymEquipmentSessionState GymStateOf(TcpSession session)
{
if (session.Items.TryGetValue(GymEquipmentKey, out var existing)
&& existing is GymEquipmentSessionState typed)
return typed;
var created = new GymEquipmentSessionState();
session.Items[GymEquipmentKey] = created;
return created;
}
/// <summary>Reconnect / scene switch: forget what was stocked so the next visit re-places it.</summary>
internal static void ResetGymEquipment(TcpSession session)
{
var owner = GymStateOf(session);
lock (owner.SyncRoot)
{
owner.Placed.Clear();
owner.PlacedCount = 0;
}
}
/// <summary>
/// Called from the grid-AOI tick (which already knows the player position). Places the
/// equipment of every venue the player has walked into, once per venue per session.
/// </summary>
internal static async Task TickGymEquipmentAsync(
TcpSession session, WorldEntryState state, float px, float pz)
{
var cfg = PrivateServerConfigStore.Current.World.Content.GymEquipment;
if (!cfg.Enabled)
return;
var owner = GymStateOf(session);
if (owner.PlacedCount == 0 && GymEquipmentCatalog4229938.Count == 0)
{
// One line per session instead of per tick — a missing capture file is a setup step,
// not a runtime error.
lock (owner.SyncRoot)
owner.PlacedCount = -1;
session.Log.Info($"[GYM] 没有可用点位:{GymEquipmentCatalog4229938.LoadError}");
return;
}
// ★ 2026-10-01 千泊实测守卫:客户端正在切室内 / 正在走传送流程时,SyncGadgetAdd 会被整批吞掉,
// 而场馆每会话只铺一次 ⇒ 一次丢 = 整场空。这里**不下发也不记 Placed**,等下一个 tick 再试。
var now = Environment.TickCount64;
bool teleporting;
long indoorReportMs;
lock (state.SyncRoot)
{
teleporting = state.PendingTeleportId != 0;
indoorReportMs = state.ClientIndoorReportMs;
}
if (cfg.SkipDuringTeleport && teleporting)
{
if (!owner.WarnedBusy)
{
owner.WarnedBusy = true;
session.Log.Info("[GYM] 传送流程未结束,本次不摆放(等下一个 tick 重试)");
}
return;
}
if (cfg.SettleAfterIndoorChangeMs > 0 && indoorReportMs != 0
&& now - indoorReportMs < cfg.SettleAfterIndoorChangeMs)
{
if (!owner.WarnedBusy)
{
owner.WarnedBusy = true;
session.Log.Info($"[GYM] 客户端刚切完室内({now - indoorReportMs}ms 前)," +
$"等 {cfg.SettleAfterIndoorChangeMs}ms 稳定后再摆放");
}
return;
}
if (owner.WarnedBusy)
{
owner.WarnedBusy = false;
session.Log.Info("[GYM] 客户端已稳定,开始摆放");
}
var channel = cfg.Channel switch
{
1 => GadgetSendChannel.Aoi,
2 => GadgetSendChannel.Both,
3 => GadgetSendChannel.AddList,
_ => GadgetSendChannel.Add,
};
foreach (var spot in GymEquipmentCatalog4229938.WithinRadius(px, pz, 0))
{
var radius = spot.Radius + cfg.RadiusBonus;
if (radius > 0f)
{
var dx = spot.X - px;
var dz = spot.Z - pz;
if (dx * dx + dz * dz > radius * radius)
continue;
}
lock (owner.SyncRoot)
{
if (!owner.Placed.Add(spot.Name))
continue;
}
if (spot.Items.Count == 0)
{
session.Log.Info($"[GYM] 场馆 {spot.Name} 还没有器材坐标(先跑 _gym_capture.py)");
continue;
}
session.Log.Info($"[GYM] 进入 {spot.Name}({spot.X:F1},{spot.Z:F1})半径 {radius:F0}m," +
$"放置 {spot.Items.Count} 件器材 channel={channel} logOnly={cfg.LogOnly}");
if (cfg.LogOnly)
continue;
var placed = 0;
if (channel == GadgetSendChannel.AddList)
{
// Batch: one push, and the captured blob uids go straight through.
var specs = spot.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($"[GYM] {spot.Name} 批量放置失败:{batch.Message}");
}
else
{
foreach (var item in spot.Items)
{
// Pass the captured blob uid: it is the instance id the client itself would have
// used, so prefab resolution and any blob-side bookkeeping line up.
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($"[GYM] {spot.Name} {item.Name}({item.PathId}) 放置失败:{result.Message}");
}
}
lock (owner.SyncRoot)
owner.PlacedCount += placed;
session.Log.Info($"[GYM] {spot.Name} 放置完成 {placed}/{spot.Items.Count}");
}
}
}