diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs
index d62a80d..4d47b4d 100644
--- a/src/AcDream.App/Rendering/GameWindow.cs
+++ b/src/AcDream.App/Rendering/GameWindow.cs
@@ -1713,13 +1713,18 @@ public sealed class GameWindow : IDisposable
if (_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var pe))
{
pe.Position = result.Position;
- // AC character models face +Y in their default orientation.
- // Our yaw convention has cos(yaw)=+X at yaw=0, so yaw=0
- // means facing +X. Offset by -PI/2 so the model faces the
- // actual walk direction (at yaw=0, model rotation = -PI/2
- // = facing +X instead of the model's default +Y).
pe.Rotation = System.Numerics.Quaternion.CreateFromAxisAngle(
System.Numerics.Vector3.UnitZ, _playerController.Yaw - MathF.PI / 2f);
+
+ // Move the player entity to its current landblock in GpuWorldState
+ // so it doesn't get frustum-culled when the player walks away from
+ // the spawn landblock. Without this, the entity stays in the spawn
+ // landblock's entity list and disappears when that landblock is culled.
+ var pp = _playerController.Position;
+ int plx = _liveCenterX + (int)System.Math.Floor(pp.X / 192f);
+ int ply = _liveCenterY + (int)System.Math.Floor(pp.Y / 192f);
+ uint currentLb = (uint)((plx << 24) | (ply << 16) | 0xFFFF);
+ _worldState.RelocateEntity(pe, currentLb);
}
// Update chase camera.
diff --git a/src/AcDream.App/Streaming/GpuWorldState.cs b/src/AcDream.App/Streaming/GpuWorldState.cs
index 9711e9a..ed2466a 100644
--- a/src/AcDream.App/Streaming/GpuWorldState.cs
+++ b/src/AcDream.App/Streaming/GpuWorldState.cs
@@ -123,7 +123,45 @@ public sealed class GpuWorldState
/// Mark a server-GUID as persistent — this entity survives landblock unloads
/// and gets re-parked as pending for its current canonical landblock.
///
- public void MarkPersistent(uint serverGuid) => _persistentGuids.Add(serverGuid);
+ public void MarkPersistent(uint serverGuid)
+ {
+ _persistentGuids.Add(serverGuid);
+ }
+
+ ///
+ /// Move a persistent entity from its current landblock slot to a new one.
+ /// Called every frame for the player entity so it stays in the landblock
+ /// matching its actual position (not its spawn landblock). Without this,
+ /// the entity stays in the spawn landblock and gets frustum-culled when
+ /// the player walks away.
+ ///
+ public void RelocateEntity(WorldEntity entity, uint newCanonicalLb)
+ {
+ if (entity.ServerGuid == 0) return;
+
+ // Remove from current landblock (find it by scanning)
+ foreach (var kvp in _loaded)
+ {
+ var entities = kvp.Value.Entities;
+ for (int i = 0; i < entities.Count; i++)
+ {
+ if (ReferenceEquals(entities[i], entity))
+ {
+ if (kvp.Key == newCanonicalLb) return; // already in the right place
+
+ // Remove from old
+ var newList = new List(entities.Count - 1);
+ for (int j = 0; j < entities.Count; j++)
+ if (j != i) newList.Add(entities[j]);
+ _loaded[kvp.Key] = new LoadedLandblock(kvp.Value.LandblockId, kvp.Value.Heightmap, newList);
+
+ // Add to new (via AppendLiveEntity which handles pending)
+ AppendLiveEntity(newCanonicalLb, entity);
+ return;
+ }
+ }
+ }
+ }
public void RemoveLandblock(uint landblockId)
{
@@ -136,7 +174,9 @@ public sealed class GpuWorldState
foreach (var entity in lb.Entities)
{
if (entity.ServerGuid != 0 && _persistentGuids.Contains(entity.ServerGuid))
+ {
_persistentRescued.Add(entity);
+ }
}
}