Files
Project-M/Assets/_Project/Scripts/Server/World/PortalInteractReceiveSystem.cs
T
kronic 304ee8c2a7 Base<->Expedition ties: portal rooms, class-at-base, prep spend + Health.Max, C3/C4/Spitter (DR-046)
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>
2026-07-06 10:37:24 -07:00

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();
}
}
}