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