using Repoverse.Core.Generation; using Repoverse.Core.Model; using Repoverse.Core.Persistence; using Repoverse.Core.Voxels; namespace Repoverse.Core.Bridge; public sealed record MeshSection(ushort Key, bool Collision, float[][] Vertices, int[] Indices); public sealed record MeshPacket(string Id, IReadOnlyList Sections); public sealed record StreamPacket(IReadOnlyList Unloaded, IReadOnlyList Chunks, int Remaining, int Resident); public sealed record Destination(string Repo, string Description, double X, double Y, double Z, int Floors, int LadderX, int LadderZ, IReadOnlyList Rooms); /// Single-client, serialized bridge session. Core Y-up units remain authoritative. public sealed class WorldSession { private readonly WorldGenerator _generator; private readonly Dictionary _chunks = new(); private readonly HashSet _dirty = new(); private readonly int _radius; public WorldManifest Manifest { get; } public WorldEdits Edits { get; } = new(); public PlayerState Player { get; private set; } public long Tick { get; private set; } public IReadOnlyList Builders => _builders.Values.ToArray(); private readonly Dictionary _builders = new(StringComparer.Ordinal); public IReadOnlyList Destinations { get; } public int Resident => _chunks.Count; public WorldSession(WorldManifest manifest, int radius = 1) { if (manifest.SchemaVersion != WorldManifest.CurrentSchema) throw new InvalidDataException("Unsupported manifest schema."); if (radius is < 0 or > 3) throw new ArgumentOutOfRangeException(nameof(radius)); Manifest = manifest; _radius = radius; _generator = new(manifest); Destinations = manifest.Buildings.Select(b => { var p = BuildingRasterizer.Plan(b); return new Destination(b.Repo.Key, b.Description, p.Entrance.X + 3.5, p.Entrance.Y + 1.5, p.Entrance.Z + .5, b.Floors, p.LadderX, p.CorridorZ + 1, p.Rooms); }).ToArray(); var start = Destinations.FirstOrDefault(); Player = new PlayerState { X = start?.X ?? 10, Y = start?.Y ?? 11, Z = start?.Z ?? 10, Yaw = 180 }; } public static void Validate(PlayerState p) { if (!double.IsFinite(p.X) || !double.IsFinite(p.Y) || !double.IsFinite(p.Z) || !double.IsFinite(p.Yaw) || Math.Abs(p.X) > 1_000_000 || Math.Abs(p.Z) > 1_000_000 || p.Y < -100 || p.Y > 10_000 || !Enum.IsDefined(p.Mode)) throw new ArgumentException("Invalid player state."); } public void SetPlayer(PlayerState player) { Validate(player); Player = player; } private static IEnumerable Neighbours(ChunkCoord c) { yield return c; yield return c with { X = c.X - 1 }; yield return c with { X = c.X + 1 }; yield return c with { Y = c.Y - 1 }; yield return c with { Y = c.Y + 1 }; yield return c with { Z = c.Z - 1 }; yield return c with { Z = c.Z + 1 }; } private static int Distance(ChunkCoord a, ChunkCoord b) => Math.Max(Math.Abs(a.X-b.X), Math.Abs(a.Z-b.Z)); /// Bounded generation and upload, nearest first. Unloads are sent before replacement meshes. public StreamPacket Focus(double x, double z, int budget = 4) { Validate(Player with { X = x, Z = z }); if (budget is < 1 or > 16) throw new ArgumentOutOfRangeException(nameof(budget)); var center = ChunkCoord.FromVoxel((int)Math.Floor(x), WorldConstants.GroundLevel, (int)Math.Floor(z)); var unloaded = new List(); foreach (var c in _chunks.Keys.Where(c => Distance(c, center) > _radius + 1).ToArray()) { _chunks.Remove(c); _dirty.Remove(c); unloaded.Add(c.ToString()); foreach (var n in Neighbours(c)) if (_chunks.ContainsKey(n)) _dirty.Add(n); } var wanted = new List(); for (int dx = -_radius; dx <= _radius; dx++) for (int dz = -_radius; dz <= _radius; dz++) for (int y = 0; y <= WorldGenerator.MaxChunkY; y++) wanted.Add(new(center.X + dx, y, center.Z + dz)); wanted = wanted.OrderBy(c => Distance(c, center)).ThenBy(c => c.Y).ThenBy(c => c.X).ThenBy(c => c.Z).ToList(); foreach (var c in wanted.Where(c => !_chunks.ContainsKey(c)).Take(budget)) { _chunks[c] = _generator.Generate(c, Edits); foreach (var n in Neighbours(c)) if (_chunks.ContainsKey(n)) _dirty.Add(n); } var result = new List(); foreach (var c in _dirty.OrderBy(c => Distance(c, center)).ThenBy(c => c.Y).ThenBy(c => c.X).ThenBy(c => c.Z).Take(budget).ToArray()) { result.Add(Encode(GreedyMesher.Build(_chunks[c], Sample))); _dirty.Remove(c); } Tick++; return new(unloaded, result, wanted.Count(c => !_chunks.ContainsKey(c)) + _dirty.Count, _chunks.Count); } public Voxel? Sample(int x, int y, int z) => _chunks.TryGetValue(ChunkCoord.FromVoxel(x,y,z), out var c) ? c[x-c.Coord.OriginX, y-c.Coord.OriginY, z-c.Coord.OriginZ] : null; public static MeshPacket Encode(ChunkMesh mesh) { var sections = new List(); foreach (var group in mesh.Quads.GroupBy(q => q.MeshKey).OrderBy(g => g.Key)) { var vertices = new List(); var indices = new List(); foreach (var q in group) { int i = vertices.Count; vertices.AddRange(GreedyMesher.Corners(mesh.Coord, q).Select(v => new[] {v.X,v.Y,v.Z})); indices.AddRange([i,i+1,i+2,i,i+2,i+3]); } sections.Add(new(group.Key, !new Voxel(group.Key).IsPassable, vertices.ToArray(), indices.ToArray())); } return new(mesh.Coord.ToString(), sections); } public IReadOnlyList Objects() => Manifest.Buildings.SelectMany(BuildingRasterizer.Objects).ToArray(); public void Edit(int x, int y, int z, ushort packed) { if (y < 1 || y > WorldGenerator.MaxChunkY * WorldConstants.ChunkSize + 31 || Math.Abs((long)x)>1_000_000 || Math.Abs((long)z)>1_000_000 || !Enum.IsDefined(new Voxel(packed).Type)) throw new ArgumentException("Invalid voxel edit."); Edits.Set(x,y,z,new Voxel(packed)); var c = ChunkCoord.FromVoxel(x,y,z); if (_chunks.ContainsKey(c)) _chunks[c] = _generator.Generate(c,Edits); foreach (var n in Neighbours(c)) if (_chunks.ContainsKey(n)) _dirty.Add(n); } public BuilderState Build(BuilderPlan plan) { var voxels=BuilderRasterizer.Rasterize(plan); if(!_builders.ContainsKey(plan.Actor) && _builders.Count>=16)throw new ArgumentException("Builder actor limit reached."); var changed=new HashSet(); foreach(var v in voxels){Edits.Set(v.X,v.Y,v.Z,new Voxel(v.Packed));changed.Add(ChunkCoord.FromVoxel(v.X,v.Y,v.Z));} foreach(var c in changed){if(_chunks.ContainsKey(c))_chunks[c]=_generator.Generate(c,Edits);foreach(var n in Neighbours(c))if(_chunks.ContainsKey(n))_dirty.Add(n);} var last=voxels[^1];var state=new BuilderState(plan.Actor,plan.Id,last.X+.5,last.Y+1,last.Z+.5,voxels.Count);_builders[plan.Actor]=state;return state; } public SaveGame Capture() => SaveGame.Capture(Manifest, Player, Edits, Tick) with {Builders=Builders}; public void Restore(SaveGame save) { if (save.WorldSeed != Manifest.WorldSeed || save.Generator != Manifest.Generator || save.Snapshots.Count != Manifest.Snapshots.Count || save.Snapshots.Any(s => !Manifest.Snapshots.TryGetValue(s.Key,out var v) || v != s.Value)) throw new InvalidDataException("Save belongs to a different world snapshot."); Validate(save.Player); // Validate in a temporary store before changing the live session. var restored = new WorldEdits(); save.RestoreEdits(restored); if(save.Builders.Count>16 || save.Builders.Any(b=>string.IsNullOrWhiteSpace(b.Actor)||b.Actor.Length>64||!double.IsFinite(b.X)||!double.IsFinite(b.Y)||!double.IsFinite(b.Z)) || save.Builders.Select(b=>b.Actor).Distinct().Count()!=save.Builders.Count)throw new InvalidDataException("Invalid saved builders."); Edits.Load(restored.Snapshot()); Player = save.Player; Tick = save.SimulationTick; _builders.Clear();foreach(var b in save.Builders)_builders.Add(b.Actor,b); foreach (var c in _chunks.Keys.ToArray()) { _chunks[c] = _generator.Generate(c,Edits); _dirty.Add(c); } } }