304ee8c2a7
Portal-gated rooms: new RunLifecycle.RoomExplore loot window; room+nodes persist past the last kill; PortalInteractRequest -> server-only PortalCommand advances (client derives the portal pos). RunDirectorSystem moves teardown off the InRoom edge, relocates the boss-branch/route-gate into the RoomExplore exit, and advances an empty expedition immediately (incl. a boss clear). Class-at-base via a shared ClassSwapUtil (meta-band strip + per-class MetaTierState replay, so the base RPC and the dev SetClass can't drift); ClassSelectRequest Staging-gated. Per-run PREP buffs (PrepPurchaseRequest, PrepCatalog): once-per-run == the row's prep StatModifier band is present, TotalOf-before-Withdraw atomic, stripped on Returning beside the boon band. Health.Max promoted to [GhostField] (the one ghost-hash re-bake) so the boss + floating enemy HP bars read it directly. Feel: melee cone connect-thunk (C3), hit-stop throttle so a horde wipe can't stutter (C4), Spitter cornered-hold. 456/456 EditMode; two adversarial reviews (pre-code 13-confirmed folded; post-impl clean bar the RoomExplore boss-clear dead-time, fixed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
67 lines
3.1 KiB
C#
67 lines
3.1 KiB
C#
using ProjectM.Simulation;
|
|
using Unity.Burst;
|
|
using Unity.Collections;
|
|
using Unity.Entities;
|
|
using Unity.NetCode;
|
|
|
|
namespace ProjectM.Server
|
|
{
|
|
/// <summary>
|
|
/// Server receiver for <see cref="PortalInteractRequest"/> — a participant interacting the room-exit portal during
|
|
/// RoomExplore. Honored ONLY when <c>RunInfo.Lifecycle==RoomExplore</c> and the sender is an EXPEDITION player
|
|
/// (region gate, the RouteSelect idiom). Sets the server-only <see cref="PortalCommand"/> latch IN-PLACE; it does
|
|
/// NOT write RunInfo or tear the room down — RunDirectorSystem (the sole FSM/teardown owner) consumes the latch and
|
|
/// advances. Plain server group, before RunDirectorSystem; requests ALWAYS destroyed; NO CyclePhase edge.
|
|
/// </summary>
|
|
[BurstCompile]
|
|
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
|
[UpdateInGroup(typeof(SimulationSystemGroup))]
|
|
[UpdateBefore(typeof(RunDirectorSystem))]
|
|
public partial struct PortalInteractReceiveSystem : ISystem
|
|
{
|
|
[BurstCompile]
|
|
public void OnCreate(ref SystemState state)
|
|
{
|
|
var b = new EntityQueryBuilder(Allocator.Temp).WithAll<PortalInteractRequest, ReceiveRpcCommandRequest>();
|
|
state.RequireForUpdate(state.GetEntityQuery(b));
|
|
state.RequireForUpdate<RunInfo>();
|
|
state.RequireForUpdate<PortalCommand>();
|
|
}
|
|
|
|
[BurstCompile]
|
|
public void OnUpdate(ref SystemState state)
|
|
{
|
|
var dirEntity = SystemAPI.GetSingletonEntity<RunInfo>();
|
|
bool gateOpen = SystemAPI.GetComponent<RunInfo>(dirEntity).Lifecycle == RunLifecycle.RoomExplore;
|
|
|
|
// Sender region lookup (N3 idiom): a base-bound joiner cannot pull the party out of the room.
|
|
var regionByConn = new NativeHashMap<int, byte>(8, Allocator.Temp);
|
|
foreach (var (owner, region) in
|
|
SystemAPI.Query<RefRO<GhostOwner>, RefRO<RegionTag>>().WithAll<PlayerTag>())
|
|
regionByConn[owner.ValueRO.NetworkId] = region.ValueRO.Region;
|
|
|
|
bool interacted = SystemAPI.GetComponent<PortalCommand>(dirEntity).HasInteract != 0;
|
|
|
|
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
|
foreach (var (receive, requestEntity) in
|
|
SystemAPI.Query<RefRO<ReceiveRpcCommandRequest>>().WithAll<PortalInteractRequest>().WithEntityAccess())
|
|
{
|
|
var conn = receive.ValueRO.SourceConnection;
|
|
bool valid = gateOpen && !interacted
|
|
&& SystemAPI.HasComponent<NetworkId>(conn)
|
|
&& regionByConn.TryGetValue(SystemAPI.GetComponent<NetworkId>(conn).Value, out byte senderRegion)
|
|
&& senderRegion == RegionId.Expedition;
|
|
if (valid)
|
|
{
|
|
SystemAPI.SetComponent(dirEntity, new PortalCommand { HasInteract = 1 });
|
|
interacted = true;
|
|
}
|
|
ecb.DestroyEntity(requestEntity);
|
|
}
|
|
ecb.Playback(state.EntityManager);
|
|
ecb.Dispose();
|
|
regionByConn.Dispose();
|
|
}
|
|
}
|
|
}
|