813c829420
- New Server/PlayerResolve.TryResolve single-sources the RPC SourceConnection -> NetworkId -> conn->player map resolve (3 sites: ClassSelectReceive, PrepPurchase, DebugCommandReceive); EntityManager reads keep it source-gen-safe from Bursted receivers. - ecb.Dispose() after Playback in 9 Temp-ECB systems (explicit-lifetime hygiene). - The TuningConfig.GetOrDefault(ref state) variant of this tail was REVERTED: state.GetEntityQuery in OnUpdate trips the Entities "creates a query during OnUpdate" diagnostic per system per world (caught in Play smoke) - the SystemAPI.TryGetSingleton idiom is already source-gen-optimal, confirming the original B4 deferral. Verified: 466/466 EditMode green on the final tree, console clean, Play smoke 0 errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
82 lines
4.3 KiB
C#
82 lines
4.3 KiB
C#
using ProjectM.Simulation;
|
|
using Unity.Collections;
|
|
using Unity.Entities;
|
|
using Unity.NetCode;
|
|
|
|
namespace ProjectM.Server
|
|
{
|
|
/// <summary>
|
|
/// Server receiver for <see cref="ClassSelectRequest"/> — the player picks their class at base. Honored ONLY in
|
|
/// Staging (class = a between-runs choice; mid-run it would desync the fight). Resolves sender → player (the
|
|
/// MetaSpend/ReadyToggle idiom), then applies the FULL in-place swap via <see cref="ClassSwapUtil"/> (class seeds +
|
|
/// permanent-meta re-sync) and writes AbilityRef / PlayerClass / AbilityCooldown + <see cref="ClassSwapUtil.HealClamp"/>.
|
|
/// Plain server group, before RunDirectorSystem (the receiver convention); requests are ALWAYS destroyed. NOT
|
|
/// Burst-compiled (a cross-assembly blob+buffer helper on a low-frequency RPC — Burst safety over micro-perf).
|
|
/// </summary>
|
|
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
|
[UpdateInGroup(typeof(SimulationSystemGroup))]
|
|
[UpdateBefore(typeof(RunDirectorSystem))]
|
|
public partial struct ClassSelectReceiveSystem : ISystem
|
|
{
|
|
public void OnCreate(ref SystemState state)
|
|
{
|
|
var b = new EntityQueryBuilder(Allocator.Temp).WithAll<ClassSelectRequest, ReceiveRpcCommandRequest>();
|
|
state.RequireForUpdate(state.GetEntityQuery(b));
|
|
state.RequireForUpdate<RunInfo>();
|
|
}
|
|
|
|
public void OnUpdate(ref SystemState state)
|
|
{
|
|
bool accept = SystemAPI.GetSingleton<RunInfo>().Lifecycle == RunLifecycle.Staging;
|
|
|
|
var playerByConn = new NativeHashMap<int, Entity>(8, Allocator.Temp);
|
|
foreach (var (owner, e) in
|
|
SystemAPI.Query<RefRO<GhostOwner>>().WithAll<PlayerTag, StatModifier>().WithEntityAccess())
|
|
playerByConn[owner.ValueRO.NetworkId] = e;
|
|
|
|
// Meta re-sync inputs (on the director/ledger ghost). dir stays Null if the catalog is absent (guarded).
|
|
Entity dir = Entity.Null;
|
|
bool haveMeta = SystemAPI.TryGetSingleton<MetaUpgradeCatalog>(out var metaCat)
|
|
&& SystemAPI.TryGetSingletonEntity<ResourceLedger>(out dir) && SystemAPI.HasBuffer<MetaTierState>(dir);
|
|
bool haveDb = SystemAPI.TryGetSingleton<AbilityDatabase>(out var abilityDb);
|
|
|
|
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
|
foreach (var (receive, req, reqEntity) in
|
|
SystemAPI.Query<RefRO<ReceiveRpcCommandRequest>, RefRO<ClassSelectRequest>>().WithEntityAccess())
|
|
{
|
|
ecb.DestroyEntity(reqEntity); // ALWAYS consumed
|
|
if (!accept) continue;
|
|
|
|
var conn = receive.ValueRO.SourceConnection;
|
|
if (!PlayerResolve.TryResolve(ref state, playerByConn, conn, out var player))
|
|
continue;
|
|
if (!SystemAPI.HasComponent<AbilityRef>(player)) continue;
|
|
|
|
var mods = SystemAPI.GetBuffer<StatModifier>(player);
|
|
var metaRecord = haveMeta ? SystemAPI.GetBuffer<MetaTierState>(dir) : default;
|
|
ClassSwapUtil.Apply(req.ValueRO.ClassId, mods, haveMeta, metaCat, metaRecord,
|
|
out byte newClass, out byte newAbilityId);
|
|
|
|
SystemAPI.SetComponent(player, new AbilityRef { Id = newAbilityId });
|
|
if (SystemAPI.HasComponent<PlayerClass>(player))
|
|
SystemAPI.SetComponent(player, new PlayerClass { ClassId = newClass });
|
|
if (SystemAPI.HasComponent<AbilityCooldown>(player))
|
|
SystemAPI.SetComponent(player, new AbilityCooldown { NextFireTick = 0 }); // swapped ability fires now
|
|
if (haveDb && SystemAPI.HasComponent<Health>(player) && SystemAPI.HasComponent<CharacterStatsRef>(player))
|
|
{
|
|
byte charId = SystemAPI.GetComponent<CharacterStatsRef>(player).Id;
|
|
if (abilityDb.Value.Value.TryGetCharacter(charId, out var baseChar))
|
|
{
|
|
var hp = SystemAPI.GetComponent<Health>(player);
|
|
ClassSwapUtil.HealClamp(ref hp, baseChar.MaxHealth, mods);
|
|
SystemAPI.SetComponent(player, hp);
|
|
}
|
|
}
|
|
}
|
|
ecb.Playback(state.EntityManager);
|
|
ecb.Dispose();
|
|
playerByConn.Dispose();
|
|
}
|
|
}
|
|
}
|