feat(vfx): port retail effect scheduling and delivery

This commit is contained in:
Erik 2026-07-14 09:25:44 +02:00
parent 363e046112
commit 96ddfdf175
28 changed files with 2473 additions and 691 deletions

View file

@ -37,8 +37,8 @@ public static class InteriorEntityIdAllocator
{
/// <summary>Per-landblock counter budget (12 bits). A landblock hydrating
/// more interior entities than this would alias into the next Y-slot —
/// exactly the #190 bug, just at a higher threshold. Callers should log
/// loudly (never silently wrap) if this is ever exceeded.</summary>
/// exactly the #190 bug, just at a higher threshold. <see cref="Allocate"/>
/// fails before that can happen.</summary>
public const uint MaxCounter = 0xFFFu;
/// <summary>The first id in this landblock's reserved range (counter=0).
@ -46,4 +46,18 @@ public static class InteriorEntityIdAllocator
/// within the landblock.</summary>
public static uint Base(uint landblockX, uint landblockY)
=> 0x40000000u | ((landblockX & 0xFFu) << 20) | ((landblockY & 0xFFu) << 12);
/// <summary>
/// Allocates the next canonical interior-static ID and advances
/// <paramref name="counter"/>. The full 12-bit range, including
/// <see cref="MaxCounter"/>, is valid; the next request fails rather than
/// aliasing the following landblock.
/// </summary>
public static uint Allocate(uint landblockX, uint landblockY, ref uint counter)
{
if (counter > MaxCounter)
throw new InvalidDataException(
$"Landblock ({landblockX & 0xFFu:X2},{landblockY & 0xFFu:X2}) exceeds the 4096-entry interior entity id namespace.");
return Base(landblockX, landblockY) + counter++;
}
}

View file

@ -47,22 +47,29 @@ public static class LandblockLoader
// legacy starting-from-1 monotonic Ids — compatible with their assertions
// which check uniqueness within a single landblock.
//
// Latent: if a landblock has >256 stabs (rare), nextId overflows the
// low byte and bleeds into the lbY byte → cross-LB collision. Same
// pattern + same limitation as scenery/interior. Document but don't
// fix in this commit — out of scope for the Tier 1 cache bug fix.
// The low byte reserves 0 for the namespace base and 1..255 for
// entities. Never wrap into the Y byte: a collision here would corrupt
// every renderer/physics/effect table keyed by WorldEntity.Id.
uint stabIdBase = landblockId == 0
? 0u
: 0xC0000000u | ((landblockId >> 24) & 0xFFu) << 16 | ((landblockId >> 16) & 0xFFu) << 8;
uint nextId = stabIdBase == 0 ? 1u : stabIdBase + 1u;
uint AllocateId()
{
if (stabIdBase != 0 && nextId > stabIdBase + 0xFFu)
throw new InvalidDataException(
$"Landblock 0x{landblockId:X8} exceeds the 255-entry static stab id namespace.");
return nextId++;
}
foreach (var stab in info.Objects)
{
if (!IsSupported(stab.Id))
continue;
var stabEntity = new WorldEntity
{
Id = nextId++,
Id = AllocateId(),
SourceGfxObjOrSetupId = stab.Id,
Position = stab.Frame.Origin,
Rotation = stab.Frame.Orientation,
@ -78,7 +85,7 @@ public static class LandblockLoader
continue;
var buildingEntity = new WorldEntity
{
Id = nextId++,
Id = AllocateId(),
SourceGfxObjOrSetupId = building.ModelId,
Position = building.Frame.Origin,
Rotation = building.Frame.Orientation,