using OpenTK.Mathematics; using Voxel.Graphics; using Voxel.Physics; using Voxel.UI; namespace Voxel.Core { public class World { private Dictionary<(int, int), Chunk> _chunks; private (int x, int z) _lastCenter = (0, 0); private int _loadDistance = 8; bool chunkLoadingInitialized = false; private static readonly Dictionary _neighborOffsets = new() { { Orientation.West, (1, 0) }, { Orientation.East, (-1, 0) }, { Orientation.North, (0, 1) }, { Orientation.South, (0, -1) } }; public World() { _chunks = new Dictionary<(int, int), Chunk>(); } public Chunk GetChunk(int chunkX, int chunkZ) { _chunks.TryGetValue((chunkX, chunkZ), out Chunk chunk); return chunk; } public void AddChunk(Chunk chunk) { _chunks[(chunk.X, chunk.Y)] = chunk; UpdateNeighboringChunks(chunk); chunk.UpdateChunkMesh(); Renderer.MarkBuffersDirty(); } public void RemoveChunk(int chunkX, int chunkZ) { if (_chunks.TryGetValue((chunkX, chunkZ), out Chunk chunk)) { var neighbors = GetChunkNeighbors(chunk).ToList(); _chunks.Remove((chunkX, chunkZ)); // update neighbor references to this chunk foreach (var (orientation, neighbor) in neighbors) { var opposite = GetOppositeOrientation(orientation); neighbor.SetNeighbor(opposite, null); neighbor.UpdateChunkMesh(); } } } public new void UpdateChunkLoading(Vector3 playerPosition) { int centerX = (int)Math.Floor(playerPosition.X / Chunk.Size); int centerZ = (int)Math.Floor(playerPosition.Z / Chunk.Size); if ((centerX == _lastCenter.x && centerZ == _lastCenter.z) && chunkLoadingInitialized) return; _lastCenter = (centerX, centerZ); int minX = centerX - _loadDistance; int maxX = centerX + _loadDistance; int minZ = centerZ - _loadDistance; int maxZ = centerZ + _loadDistance; UnloadDistantChunks(minX, maxX, minZ, maxZ); LoadChunksInRange(minX, maxX, minZ, maxZ); chunkLoadingInitialized = true; } private void UnloadDistantChunks(int minX, int maxX, int minZ, int maxZ) { var chunksToRemove = new List<(int, int)>(); foreach (var (chunkX, chunkZ) in _chunks.Keys) { if (chunkX < minX || chunkX > maxX || chunkZ < minZ || chunkZ > maxZ) { chunksToRemove.Add((chunkX, chunkZ)); } } foreach (var chunkPos in chunksToRemove) { RemoveChunk(chunkPos.Item1, chunkPos.Item2); } } private void LoadChunksInRange(int minX, int maxX, int minZ, int maxZ) { for (int x = minX; x <= maxX; x++) { for (int z = minZ; z <= maxZ; z++) { if (!_chunks.ContainsKey((x, z))) { Chunk chunk = new Chunk(x, z); AddChunk(chunk); } } } } public void UpdateNeighboringChunks(Chunk chunk) { for (int i = 0; i < 6; i++) { chunk.SetNeighbor((Orientation)i, null); } foreach (var (orientation, neighbor) in GetChunkNeighbors(chunk)) { Orientation opposite = GetOppositeOrientation(orientation); neighbor.SetNeighbor(opposite, chunk); chunk.SetNeighbor(orientation, neighbor); neighbor.UpdateChunkMesh(); } } private Orientation GetOppositeOrientation(Orientation orientation) { return orientation switch { Orientation.West => Orientation.East, Orientation.East => Orientation.West, Orientation.North => Orientation.South, Orientation.South => Orientation.North, _ => orientation }; } public IEnumerable GetAllChunks() { return _chunks.Values; } public Blocks GetBlock(int worldX, int worldY, int worldZ) { var (chunkX, chunkZ, localX, localZ) = WorldToChunkCoords(worldX, worldZ); Chunk chunk = GetChunk(chunkX, chunkZ); if (chunk == null) return 0; // air if chunk not loaded return chunk.GetBlock(localX, worldY, localZ); } public void SetBlock(int worldX, int worldY, int worldZ, Blocks block, bool updateMesh = true) { var (chunkX, chunkZ, localX, localZ) = WorldToChunkCoords(worldX, worldZ); Chunk chunk = GetChunk(chunkX, chunkZ); if (chunk == null) return; chunk.SetBlock(localX, worldY, localZ, block, updateMesh: updateMesh); // temporary tnt functionality if (block == Blocks.TNT) { Explode(worldX, worldY, worldZ, 4); return; } if (!updateMesh) return; UpdateNeighborsAtBoundary(chunk, localX, worldY, localZ); RebuildDirtyChunks(); } private void UpdateNeighborsAtBoundary(Chunk chunk, int lx, int ly, int lz) { if (lx == Chunk.Size - 1) chunk.GetNeighbor(Orientation.West)?.MarkDirty(); else if (lx == 0) chunk.GetNeighbor(Orientation.East)?.MarkDirty(); if (lz == Chunk.Size - 1) chunk.GetNeighbor(Orientation.North)?.MarkDirty(); else if (lz == 0) chunk.GetNeighbor(Orientation.South)?.MarkDirty(); } public bool IsOnChunkBorder(int worldX, int worldZ) { var (chunkX, chunkZ, localX, localZ) = WorldToChunkCoords(worldX, worldZ); return (localX == Chunk.Size - 1 || localX == 0) || (localZ == Chunk.Size - 1 || localZ == 0); } // temporary tnt functionality public void Explode(int centerX, int centerY, int centerZ, int radius) { int radiusSq = radius * radius; var affectedChunks = new HashSet(); // store chunks that will change // bounding box int minX = centerX - radius; int maxX = centerX + radius; int minY = Math.Max(0, centerY - radius); int maxY = Math.Min(Chunk.Height - 1, centerY + radius); int minZ = centerZ - radius; int maxZ = centerZ + radius; for (int x = minX; x <= maxX; x++) { for (int z = minZ; z <= maxZ; z++) { var (chunkX, chunkZ, localX, localZ) = WorldToChunkCoords(x, z); Chunk chunk = GetChunk(chunkX, chunkZ); if (chunk == null) continue; for (int y = minY; y <= maxY; y++) { int dx = x - centerX; int dy = y - centerY; int dz = z - centerZ; if (dx * dx + dy * dy + dz * dz <= radiusSq) { chunk.SetBlock(localX, y, localZ, Blocks.Air, updateMesh: true); } } } } RebuildDirtyChunks(); } private void RebuildDirtyChunks() { foreach (var chunk in _chunks.Values) { if (chunk.IsDirty) { chunk.UpdateChunkMesh(); Renderer.UpdateChunkBuffer(chunk); chunk.IsDirty = false; } } } public static (int chunkX, int chunkZ, int localX, int localZ) WorldToChunkCoords(int worldX, int worldZ) { int chunkX = worldX >= 0 ? worldX / Chunk.Size : (worldX + 1) / Chunk.Size - 1; int chunkZ = worldZ >= 0 ? worldZ / Chunk.Size : (worldZ + 1) / Chunk.Size - 1; int localX = ((worldX % Chunk.Size) + Chunk.Size) % Chunk.Size; int localZ = ((worldZ % Chunk.Size) + Chunk.Size) % Chunk.Size; return (chunkX, chunkZ, localX, localZ); } List GetEdgeOrientations(int localX, int localZ, int size) { var orientations = new List(); if (localX == size - 1) orientations.Add(Orientation.West); if (localX == 0) orientations.Add(Orientation.East); if (localZ == size - 1) orientations.Add(Orientation.North); if (localZ == 0) orientations.Add(Orientation.South); return orientations; } public IEnumerable<(Orientation orientation, Chunk neighbor)> GetChunkNeighbors(Chunk chunk) { int chunkX = chunk.X; int chunkY = chunk.Y; foreach (var kv in _neighborOffsets) { Chunk neighbor = GetChunk(chunkX + kv.Value.x, chunkY + kv.Value.y); if (neighbor != null) yield return (kv.Key, neighbor); } } public (bool success, Blocks block, int x, int y, int z, Vector3i normal) Raycast(Vector3 origin, Vector3 direction, float maxDistance) { int x = (int)MathF.Floor(origin.X); int y = (int)MathF.Floor(origin.Y); int z = (int)MathF.Floor(origin.Z); int stepX = direction.X > 0 ? 1 : -1; int stepY = direction.Y > 0 ? 1 : -1; int stepZ = direction.Z > 0 ? 1 : -1; float tDeltaX = direction.X != 0 ? MathF.Abs(1 / direction.X) : float.MaxValue; float tDeltaY = direction.Y != 0 ? MathF.Abs(1 / direction.Y) : float.MaxValue; float tDeltaZ = direction.Z != 0 ? MathF.Abs(1 / direction.Z) : float.MaxValue; float tMaxX = direction.X > 0 ? (MathF.Floor(origin.X) + 1 - origin.X) * tDeltaX : (origin.X - MathF.Floor(origin.X)) * tDeltaX; float tMaxY = direction.Y > 0 ? (MathF.Floor(origin.Y) + 1 - origin.Y) * tDeltaY : (origin.Y - MathF.Floor(origin.Y)) * tDeltaY; float tMaxZ = direction.Z > 0 ? (MathF.Floor(origin.Z) + 1 - origin.Z) * tDeltaZ : (origin.Z - MathF.Floor(origin.Z)) * tDeltaZ; float distance = 0f; Vector3i normal = Vector3i.Zero; while (distance <= maxDistance) { Blocks block = GetBlock(x, y, z); if (block != Blocks.Air) return (true, block, x, y, z, normal); if (tMaxX < tMaxY && tMaxX < tMaxZ) { x += stepX; normal = new Vector3i(-stepX, 0, 0); distance = tMaxX; tMaxX += tDeltaX; } else if (tMaxY < tMaxZ) { y += stepY; normal = new Vector3i(0, -stepY, 0); distance = tMaxY; tMaxY += tDeltaY; } else { z += stepZ; normal = new Vector3i(0, 0, -stepZ); distance = tMaxZ; tMaxZ += tDeltaZ; } } return (false, Blocks.Air, 0, 0, 0, Vector3i.Zero); } public List GetColliders(AABB body) { List collisions = new List(); int minX = (int)Math.Floor(body.Min.X); int maxX = (int)Math.Ceiling(body.Max.X) - 1; int minY = (int)Math.Floor(body.Min.Y); int maxY = (int)Math.Ceiling(body.Max.Y) - 1; int minZ = (int)Math.Floor(body.Min.Z); int maxZ = (int)Math.Ceiling(body.Max.Z) - 1; for (int x = minX; x <= maxX; x++) { for (int y = minY; y <= maxY; y++) { for (int z = minZ; z <= maxZ; z++) { if (GetBlock(x, y, z) != Blocks.Air) { collisions.Add(new AABB( new Vector3(x, y, z), new Vector3(x + 1, y + 1, z + 1) )); } } } } return collisions; } } }