From d18690caad7d3a23af4c93ac262641205e67b6be Mon Sep 17 00:00:00 2001 From: Cassunshine Date: Wed, 13 Dec 2023 18:55:44 -0500 Subject: [PATCH] -Migrate to using Registries -Migrate to using ContentDatabase instead of hard-coded content --- Client/Network/ClientConnectionContext.cs | 20 ++++ Client/Network/InternetC2SConnection.cs | 29 ++--- Client/Rendering/GameRenderer.cs | 11 +- Client/Rendering/Models/BlockModelManager.cs | 35 ++++-- Client/Rendering/World/ChunkMeshBuilder.cs | 76 ++++++++----- Client/Server/IntegratedServer.cs | 9 ++ Client/VoxelClient.cs | 11 +- Client/World/ClientWorld.cs | 3 +- Common/Collision/PhysicsSim.cs | 2 +- Common/Common.csproj | 4 + Common/Content/ContentDatabase.cs | 33 ++++++ Common/Content/ContentPack.cs | 39 +++++++ Common/Content/MainContentPack.cs | 60 ++++++++++ .../Network/Packets/S2C/Gameplay/ChunkData.cs | 17 +-- .../S2C/Gameplay/Entity/SpawnEntity.cs | 31 ++++++ .../Packets/S2C/Handshake/S2CHandshakeDone.cs | 6 +- Common/Network/Packets/Utils/PacketMap.cs | 53 --------- Common/Network/Packets/Utils/RawIDMap.cs | 23 ---- .../Components/Networking/LNLHostManager.cs | 31 ++---- .../Networking/ServerConnectionContext.cs | 33 ++++-- Common/Server/Components/PlayerManager.cs | 29 ++++- Common/Server/Components/WorldManager.cs | 5 +- Common/Server/VoxelServer.cs | 1 + Common/Server/World/ServerChunk.cs | 13 +++ Common/Server/World/ServerWorld.cs | 30 ++++- Common/Tile/Block.cs | 20 ++-- Common/Tile/Blocks.cs | 54 --------- Common/Util/Registration/BaseRegistry.cs | 13 +++ Common/Util/Registration/Registries.cs | 55 ++++++++++ Common/Util/Registration/Registry.cs | 19 ++++ Common/Util/Registration/SimpleRegistry.cs | 103 ++++++++++++++++++ Common/Util/Registration/TypedRegistry.cs | 67 ++++++++++++ Common/Util/Serialization/VDataWriter.cs | 2 + Common/World/Chunk.cs | 35 ++++-- Common/World/Entity/Entity.cs | 30 ++--- Common/World/Entity/LivingEntity.cs | 11 +- Common/World/Entity/TickedEntity.cs | 11 ++ Common/World/Generation/SimpleGenerator.cs | 9 +- Common/World/LoadedChunkSection.cs | 6 + Common/World/Storage/SimpleStorage.cs | 6 +- Common/World/Tick/TickList.cs | 9 ++ Common/World/{ => Tick}/Tickable.cs | 2 +- Common/World/Views/SnapshotView.cs | 4 +- Common/World/VoxelWorld.cs | 72 +++++++++++- Core/Util/DefferedList.cs | 36 ++++++ Core/Util/SwapList.cs | 83 ++++++++++++++ 46 files changed, 945 insertions(+), 306 deletions(-) create mode 100644 Common/Content/ContentDatabase.cs create mode 100644 Common/Content/ContentPack.cs create mode 100644 Common/Content/MainContentPack.cs create mode 100644 Common/Network/Packets/S2C/Gameplay/Entity/SpawnEntity.cs delete mode 100644 Common/Network/Packets/Utils/PacketMap.cs delete mode 100644 Common/Network/Packets/Utils/RawIDMap.cs create mode 100644 Common/Server/World/ServerChunk.cs delete mode 100644 Common/Tile/Blocks.cs create mode 100644 Common/Util/Registration/BaseRegistry.cs create mode 100644 Common/Util/Registration/Registries.cs create mode 100644 Common/Util/Registration/Registry.cs create mode 100644 Common/Util/Registration/SimpleRegistry.cs create mode 100644 Common/Util/Registration/TypedRegistry.cs create mode 100644 Common/World/Entity/TickedEntity.cs create mode 100644 Common/World/Tick/TickList.cs rename Common/World/{ => Tick}/Tickable.cs (61%) create mode 100644 Core/Util/DefferedList.cs create mode 100644 Core/Util/SwapList.cs diff --git a/Client/Network/ClientConnectionContext.cs b/Client/Network/ClientConnectionContext.cs index 96b8d65..52d84b0 100644 --- a/Client/Network/ClientConnectionContext.cs +++ b/Client/Network/ClientConnectionContext.cs @@ -1,9 +1,12 @@ using System; +using Voxel.Client.Rendering.Models; using Voxel.Client.World; +using Voxel.Client.World.Entity; using Voxel.Common.Network.Packets; using Voxel.Common.Network.Packets.C2S; using Voxel.Common.Network.Packets.S2C; using Voxel.Common.Network.Packets.S2C.Gameplay; +using Voxel.Common.Network.Packets.S2C.Gameplay.Entity; using Voxel.Common.Network.Packets.S2C.Handshake; using Voxel.Common.Network.Packets.Utils; @@ -16,6 +19,7 @@ namespace Voxel.Client.Network; /// public class ClientConnectionContext { public bool isDead => Connection.isDead; + public Guid playerID { get; private set; } public readonly VoxelClient Client; private readonly C2SConnection Connection; @@ -35,6 +39,8 @@ public ClientConnectionContext(VoxelClient client, C2SConnection connection) { GameplayHandler.RegisterHandler(HandleChunkData); GameplayHandler.RegisterHandler(HandleChunkUnload); + GameplayHandler.RegisterHandler(HandleSpawnEntity); + Connection.packetHandler = HandshakeHandler; } @@ -50,7 +56,9 @@ private void HandleSetupWorld(SetupWorld packet) { } private void HandleHandshakeDone(S2CHandshakeDone packet) { + BlockModelManager.BakeRawBlockModels(); Connection.packetHandler = GameplayHandler; + playerID = packet.PlayerID; Console.WriteLine("Client:Server Says Handshake Done"); } @@ -70,6 +78,18 @@ private void HandleChunkUnload(ChunkUnload packet) { packet.Apply(chunk); } + private void HandleSpawnEntity(SpawnEntity packet) { + if (Client.world == null) + return; + + if (packet.ID == playerID) { + var entity = new ControlledClientPlayerEntity(); + entity.ID = packet.ID; + Client.PlayerEntity = entity; + Client.world.AddEntity(entity, packet.position, packet.rotation); + } + } + public void SendPacket(C2SPacket packet) { Connection.DeliverPacket(packet); PacketPool.Return(packet); diff --git a/Client/Network/InternetC2SConnection.cs b/Client/Network/InternetC2SConnection.cs index 005f425..e7fda4c 100644 --- a/Client/Network/InternetC2SConnection.cs +++ b/Client/Network/InternetC2SConnection.cs @@ -2,11 +2,13 @@ using System.Net; using System.Net.Sockets; using LiteNetLib; +using Voxel.Common.Content; using Voxel.Common.Network.Packets; using Voxel.Common.Network.Packets.C2S; using Voxel.Common.Network.Packets.C2S.Handshake; using Voxel.Common.Network.Packets.S2C; using Voxel.Common.Network.Packets.Utils; +using Voxel.Common.Util.Registration; using Voxel.Common.Util.Serialization.Compressed; namespace Voxel.Client.Network; @@ -16,14 +18,14 @@ public class InternetC2SConnection : C2SConnection, INetEventListener { private readonly NetManager NetClient; private NetPeer? peer; - private readonly PacketMap PacketMap = new(); private readonly CompressedVDataReader Reader = new(); private readonly CompressedVDataWriter Writer = new(); + public Registries Registries => ContentDatabase.Instance.Registries; + private bool synced = false; public InternetC2SConnection(string address, int port = 24564) { - PacketMap.FillOutgoingMap(); NetClient = new NetManager(this); OnClosed += () => { @@ -51,30 +53,20 @@ public override void DeliverPacket(Packet toSend) { var writer = Writer; writer.Reset(); - if (!PacketMap.outgoingMap.TryGetValue(toSend.GetType(), out var rawID)) + if (!Registries.PacketTypes.TypeToRaw(toSend.GetType(), out var rawID)) throw new InvalidOperationException($"Cannot send unknown packet {toSend}"); writer.Write(rawID); writer.Write(toSend); peer.Send(writer.currentBytes, 0, DeliveryMethod.ReliableOrdered); - //Console.WriteLine($"Sending {writer.currentBytes.Length} bytes to server"); + //Console.WriteLine($"Sending {writer.currentBytes.Length} bytes to server for packet {toSend}"); } public void OnPeerConnected(NetPeer peer) { this.peer = peer; Console.Out.WriteLine("Client: Connected!"); - - //SYNC MAPS HERE - var writer = Writer; - writer.Reset(); - - PacketMap.WriteOutgoingMap(writer); - peer.Send(writer.currentBytes, 0, DeliveryMethod.ReliableOrdered); - - //After maps have been synced, client handshake is done. - DeliverPacket(new C2SHandshakeDone()); } public void OnNetworkReceive(NetPeer _, NetPacketReader nReader, byte channelNumber, DeliveryMethod deliveryMethod) { @@ -84,14 +76,17 @@ public void OnNetworkReceive(NetPeer _, NetPacketReader nReader, byte channelNum Reader.LoadData(nReader.RawData.AsSpan(nReader.UserDataOffset, nReader.UserDataSize)); if (!synced) { - PacketMap.ReadIncomingMap(Reader); + Registries.ReadSync(Reader); synced = true; - //Console.Out.WriteLine("Client: S2C Map Synced"); + Console.Out.WriteLine("Client: S2C Map Synced"); + + //After maps have been synced, client handshake is done. + DeliverPacket(new C2SHandshakeDone()); return; } var rawID = Reader.ReadUint(); - if (!PacketMap.incomingMap.TryGetValue(rawID, out var packetType)) + if (!Registries.PacketTypes.RawToType(rawID, out var packetType)) return; //Console.WriteLine($"Got packet {packetType.Name} from server"); diff --git a/Client/Rendering/GameRenderer.cs b/Client/Rendering/GameRenderer.cs index 3e78252..9896d5b 100644 --- a/Client/Rendering/GameRenderer.cs +++ b/Client/Rendering/GameRenderer.cs @@ -4,6 +4,7 @@ using Voxel.Client.Rendering.Debug; using Voxel.Client.Rendering.Gui; using Voxel.Client.Rendering.World; +using Voxel.Common.Util; namespace Voxel.Client.Rendering; @@ -78,11 +79,11 @@ public override void Render(double delta) { Framebuffer.Resolve(RenderSystem); BlitRenderer.Blit(Framebuffer.ResolvedMainColor, RenderSystem.GraphicsDevice.MainSwapchain.Framebuffer, true); - - - /*ImGui.Text($"Player Pre-Move Velocity: {(Client.PlayerEntity?.preMoveVelocity.y ?? 0):F3}"); - ImGui.Text($" Player Velocity: {(Client.PlayerEntity?.velocity.y ?? 0):F3}"); - ImGui.Text($"Player Grounded: {Client.PlayerEntity?.isOnFloor ?? false}");*/ + + + ImGui.Text($"Player Position: {(Client.PlayerEntity?.blockPosition ?? ivec3.Zero)}"); + ImGui.Text($"Player Velocity: {(Client.PlayerEntity?.velocity.WorldToBlockPosition() ?? ivec3.Zero)}"); + ImGui.Text($"Player Grounded: {Client.PlayerEntity?.isOnFloor ?? false}"); } diff --git a/Client/Rendering/Models/BlockModelManager.cs b/Client/Rendering/Models/BlockModelManager.cs index c321383..2612453 100644 --- a/Client/Rendering/Models/BlockModelManager.cs +++ b/Client/Rendering/Models/BlockModelManager.cs @@ -5,6 +5,7 @@ using Newtonsoft.Json; using Voxel.Client.Rendering.Texture; using Voxel.Client.Rendering.Utils; +using Voxel.Common.Content; using Voxel.Common.Tile; using Voxel.Core.Assets; @@ -13,13 +14,14 @@ namespace Voxel.Client.Rendering.Models; public static class BlockModelManager { private const float BlueTintAmount = 0.9f; private const string Suffix = ".json"; - + private static readonly string Prefix = "models/block"; - private static readonly Dictionary Models = new(); + private static readonly Dictionary Models = new(); + private static readonly List ModelsByRawID = new(); private static readonly JsonSerializer Serializer = new(); - + private static readonly vec3 LightColor = new(BlueTintAmount, BlueTintAmount, 1); private static readonly vec4 LeftColor = new(ColorFunctions.GetColorMultiplier(0.8f, LightColor), 1); private static readonly vec4 RightColor = new(ColorFunctions.GetColorMultiplier(0.77f, LightColor), 1); @@ -27,8 +29,11 @@ public static class BlockModelManager { private static readonly vec4 BackwardColor = new(ColorFunctions.GetColorMultiplier(0.7f, LightColor), 1); private static readonly vec4 ForwardColor = new(ColorFunctions.GetColorMultiplier(0.67f, LightColor), 1); - public static void RegisterModel(Block block, BlockModel model) => Models[block] = model; - public static bool TryGetModel(Block block, [NotNullWhen(true)] out BlockModel? model) => Models.TryGetValue(block, out model); + public static void RegisterModel(string name, BlockModel model) => Models[name] = model; + public static bool TryGetModel(Block block, [NotNullWhen(true)] out BlockModel? model) { + model = ModelsByRawID[(int)block.id]; + return model != null; + } public static BlockModel GetDefault(Atlas.Sprite sprite) { return new BlockModel.Builder() @@ -113,21 +118,33 @@ public static void Init(AssetReader reader, Atlas atlas) { foreach ((string name, var stream, _) in reader.LoadAll(Prefix, Suffix)) { using var sr = new StreamReader(stream); using var jsonTextReader = new JsonTextReader(sr); - + string texture = Serializer.Deserialize(jsonTextReader)?.Texture ?? ""; int start = Prefix.Length + 1; int end = name.Length - Suffix.Length; string blockName = name[start..end]; - if (Blocks.GetBlock(blockName, out var block) && atlas.TryGetSprite(texture, out var sprite)) - RegisterModel(block, GetDefault(sprite)); + if (atlas.TryGetSprite(texture, out var sprite)) + RegisterModel(blockName, GetDefault(sprite)); } if ( atlas.TryGetSprite("main/grass_top", out var top) && atlas.TryGetSprite("main/grass_side", out var side) && atlas.TryGetSprite("main/dirt", out var bottom) ) - RegisterModel(Blocks.Grass, GetGrass(top, bottom, side)); + RegisterModel("grass", GetGrass(top, bottom, side)); + } + + + public static void BakeRawBlockModels() { + ModelsByRawID.Clear(); + + foreach ((var entry, string? id, uint raw) in ContentDatabase.Instance.Registries.Blocks.Entries()) { + if (Models.TryGetValue(id, out var mdl)) + ModelsByRawID.Add(mdl); + else + ModelsByRawID.Add(null); + } } private class ModelJson { diff --git a/Client/Rendering/World/ChunkMeshBuilder.cs b/Client/Rendering/World/ChunkMeshBuilder.cs index 5310f43..a593bec 100644 --- a/Client/Rendering/World/ChunkMeshBuilder.cs +++ b/Client/Rendering/World/ChunkMeshBuilder.cs @@ -148,53 +148,77 @@ ushort oti(int x, int y, int z) { //Left Face FaceToNeighborIndexes[0] = new[] { oti(-1, 0, 0), - oti(-1, 0, -1), oti(-1, -1, -1), - oti(-1, -1, 0), oti(-1, -1, 1), - oti(-1, 0, 1), oti(-1, 1, 1), - oti(-1, 1, 0), oti(-1, 1, -1), + oti(-1, 0, -1), + oti(-1, -1, -1), + oti(-1, -1, 0), + oti(-1, -1, 1), + oti(-1, 0, 1), + oti(-1, 1, 1), + oti(-1, 1, 0), + oti(-1, 1, -1), }; //Right Face FaceToNeighborIndexes[1] = new[] { oti(1, 0, 0), - oti(1, -1, 0), oti(1, -1, -1), - oti(1, 0, -1), oti(1, 1, -1), - oti(1, 1, 0), oti(1, 1, 1), - oti(1, 0, 1), oti(1, -1, 1), + oti(1, -1, 0), + oti(1, -1, -1), + oti(1, 0, -1), + oti(1, 1, -1), + oti(1, 1, 0), + oti(1, 1, 1), + oti(1, 0, 1), + oti(1, -1, 1), }; //Bottom Face FaceToNeighborIndexes[2] = new[] { oti(0, -1, 0), - oti(-1, -1, 0), oti(-1, -1, -1), - oti(0, -1, -1), oti(1, -1, -1), - oti(1, -1, 0), oti(1, -1, 1), - oti(0, -1, 1), oti(-1, -1, 1), + oti(-1, -1, 0), + oti(-1, -1, -1), + oti(0, -1, -1), + oti(1, -1, -1), + oti(1, -1, 0), + oti(1, -1, 1), + oti(0, -1, 1), + oti(-1, -1, 1), }; //Top Face FaceToNeighborIndexes[3] = new[] { oti(0, 1, 0), - oti(0, 1, -1), oti(-1, 1, -1), - oti(-1, 1, 0), oti(-1, 1, 1), - oti(0, 1, 1), oti(1, 1, 1), - oti(1, 1, 0), oti(1, 1, -1), + oti(0, 1, -1), + oti(-1, 1, -1), + oti(-1, 1, 0), + oti(-1, 1, 1), + oti(0, 1, 1), + oti(1, 1, 1), + oti(1, 1, 0), + oti(1, 1, -1), }; //Backward Face FaceToNeighborIndexes[4] = new[] { oti(0, 0, -1), - oti(0, -1, -1), oti(-1, -1, -1), - oti(-1, 0, -1), oti(-1, 1, -1), - oti(0, 1, -1), oti(1, 1, -1), - oti(1, 0, -1), oti(1, -1, -1), + oti(0, -1, -1), + oti(-1, -1, -1), + oti(-1, 0, -1), + oti(-1, 1, -1), + oti(0, 1, -1), + oti(1, 1, -1), + oti(1, 0, -1), + oti(1, -1, -1), }; //Forward Face FaceToNeighborIndexes[5] = new[] { oti(0, 0, 1), - oti(-1, 0, 1), oti(-1, -1, 1), - oti(0, -1, 1), oti(1, -1, 1), - oti(1, 0, 1), oti(1, 1, 1), - oti(0, 1, 1), oti(-1, 1, 1), + oti(-1, 0, 1), + oti(-1, -1, 1), + oti(0, -1, 1), + oti(1, -1, 1), + oti(1, 0, 1), + oti(1, 1, 1), + oti(0, 1, 1), + oti(-1, 1, 1), }; } } @@ -276,7 +300,7 @@ private void WorkLoop() { var checkBlock = chunkStorages[checkTuple.Item1][checkTuple.Item2]; //Mark if any side of this block is visible. - isVisible |= !checkBlock.IsNotAir; + isVisible |= checkBlock.IsAir; neighbors[n] = checkBlock; } @@ -289,7 +313,7 @@ private void WorkLoop() { faceBlocks[fb] = neighbors[fIndexList[fb]]; //If block directly on face is solid, skip face. - if (faceBlocks[0] != Blocks.Air) + if (!faceBlocks[0].IsAir) continue; float calculateAO(float s1, float corner, float s2) { diff --git a/Client/Server/IntegratedServer.cs b/Client/Server/IntegratedServer.cs index 5ef6e9d..302302e 100644 --- a/Client/Server/IntegratedServer.cs +++ b/Client/Server/IntegratedServer.cs @@ -1,7 +1,16 @@ +using Voxel.Common.Content; using Voxel.Common.Server; namespace Voxel.Client.Server; public class IntegratedServer : VoxelServer { + + public override void Start() { + ContentDatabase.Instance.Clear(); + ContentDatabase.Instance.LoadPack(MainContentPack.Instance); + ContentDatabase.Instance.Finish(); + + base.Start(); + } } diff --git a/Client/VoxelClient.cs b/Client/VoxelClient.cs index c1056cc..a7d6d53 100644 --- a/Client/VoxelClient.cs +++ b/Client/VoxelClient.cs @@ -5,9 +5,8 @@ using Voxel.Client.Rendering; using Voxel.Client.Server; using Voxel.Client.World; -using Voxel.Client.World.Entity; using Voxel.Common.Util; -using Voxel.Common.World.Entity; +using Voxel.Common.Util.Registration; using Voxel.Common.World.Entity.Player; using Voxel.Core; @@ -15,6 +14,7 @@ namespace Voxel.Client; public class VoxelClient : Game { public static VoxelClient Instance { get; private set; } + public GameRenderer GameRenderer { get; set; } /// @@ -32,7 +32,7 @@ public class VoxelClient : Game { /// public ClientWorld? world { get; private set; } - public PlayerEntity? PlayerEntity { get; private set; } + public PlayerEntity? PlayerEntity { get; internal set; } public double timeSinceLastTick; @@ -69,9 +69,6 @@ public void SetupWorld() { world?.Dispose(); world = new ClientWorld(); - - PlayerEntity = new ControlledClientPlayerEntity(); - world.AddEntity(PlayerEntity, new dvec3(0, 15, 0), dvec2.Zero); } public override void OnFrame(double delta, double tickAccumulator) { @@ -88,7 +85,7 @@ public override void OnTick() { useMSAA = !useMSAA; GameRenderer.SetMSAA(useMSAA ? 1u : 8u); } - + connection?.Tick(); world?.Tick(); } diff --git a/Client/World/ClientWorld.cs b/Client/World/ClientWorld.cs index ed640b0..c25f680 100644 --- a/Client/World/ClientWorld.cs +++ b/Client/World/ClientWorld.cs @@ -4,12 +4,11 @@ namespace Voxel.Client.World; public class ClientWorld : VoxelWorld { - + protected override Chunk CreateChunk(ivec3 pos) { var c = new ClientChunk(pos, this); return c; } - public override bool IsChunkLoadedRaw(ivec3 chunkPos) => TryGetChunkRaw(chunkPos, out var c) && c is ClientChunk cc && cc.isFilled; } diff --git a/Common/Collision/PhysicsSim.cs b/Common/Collision/PhysicsSim.cs index 4457327..4dc1307 100644 --- a/Common/Collision/PhysicsSim.cs +++ b/Common/Collision/PhysicsSim.cs @@ -124,7 +124,7 @@ public static bool Raycast(this BlockView world, RaySegment segment, out Raycast while (true) { var blockPos = new dvec3(x, y, z).WorldToBlockPosition(); var block = world.GetBlock(blockPos); - if (block.IsNotAir) { + if (!block.IsAir) { var blockBox = new AABB(blockPos, blockPos + new dvec3(1)); if (blockBox.Raycast(segment, out hit)) diff --git a/Common/Common.csproj b/Common/Common.csproj index 41ca066..da491a2 100644 --- a/Common/Common.csproj +++ b/Common/Common.csproj @@ -23,4 +23,8 @@ + + + + diff --git a/Common/Content/ContentDatabase.cs b/Common/Content/ContentDatabase.cs new file mode 100644 index 0000000..35ff43a --- /dev/null +++ b/Common/Content/ContentDatabase.cs @@ -0,0 +1,33 @@ +using Voxel.Common.Util.Registration; + +namespace Voxel.Common.Content; + +public class ContentDatabase { + public static readonly ContentDatabase Instance = new(); + + public readonly Registries Registries = new(); + + public void LoadPack(ContentPack pack) { + pack.Load(); + + foreach ((string id, var type) in pack.PacketTypes) + Registries.PacketTypes.Register(id, type); + + foreach ((string id, var block) in pack.Blocks) + Registries.Blocks.Register(block, id); + foreach ((string id, var type) in pack.EntityTypes) + Registries.EntityTypes.Register(id, type); + } + + public void Finish() { + Registries.GenerateIDs(); + + //Update blocks with internal IDs + foreach ((var entry, string? id, uint raw) in Registries.Blocks.Entries()) + entry.id = raw; + } + + public void Clear() { + Registries.Clear(); + } +} diff --git a/Common/Content/ContentPack.cs b/Common/Content/ContentPack.cs new file mode 100644 index 0000000..84340de --- /dev/null +++ b/Common/Content/ContentPack.cs @@ -0,0 +1,39 @@ +using Voxel.Common.Tile; +using Voxel.Common.World.Entity; +using Voxel.Common.Network.Packets; + +namespace Voxel.Common.Content; + +/// +/// Holds all of the content related to some part of the game. +/// +/// Ideally in the future will be transmittable across network. +/// +public class ContentPack { + public readonly string ID; + + public readonly Dictionary Blocks = new(); + public readonly Dictionary EntityTypes = new(); + public readonly Dictionary PacketTypes = new(); + + public ContentPack(string id) { + ID = id; + } + + public virtual void Load() { + + } + + public T AddBlock(T b) where T : Block { + Blocks[b.Name] = b; + return b; + } + + public void AddPacketType(string name) where T : Packet { + PacketTypes[name] = typeof(T); + } + + public void AddEntityType(string name) where T : Entity { + EntityTypes[name] = typeof(T); + } +} diff --git a/Common/Content/MainContentPack.cs b/Common/Content/MainContentPack.cs new file mode 100644 index 0000000..0d79731 --- /dev/null +++ b/Common/Content/MainContentPack.cs @@ -0,0 +1,60 @@ +using Voxel.Common.Network.Packets.C2S.Gameplay; +using Voxel.Common.Network.Packets.C2S.Handshake; +using Voxel.Common.Network.Packets.S2C.Gameplay; +using Voxel.Common.Network.Packets.S2C.Gameplay.Entity; +using Voxel.Common.Network.Packets.S2C.Handshake; +using Voxel.Common.Tile; +using Voxel.Common.World.Entity.Player; + +namespace Voxel.Common.Content; + +public class MainContentPack : ContentPack { + + public static readonly MainContentPack Instance = new(); + + public Block Air; + public Block Stone; + public Block Dirt; + public Block Grass; + + private MainContentPack() : base("main") {} + + public override void Load() { + base.Load(); + + LoadBlocks(); + LoadPacketTypes(); + LoadEntityTypes(); + } + + private void LoadBlocks() { + Air = AddBlock(new Block("air", new BlockSettings.Builder { + IsAir = true + })); + Stone = AddBlock(new Block("stone")); + Dirt = AddBlock(new Block("dirt")); + Grass = AddBlock(new Block("grass")); + } + + private void LoadPacketTypes() { + + //C2S + AddPacketType("c2s_player_update"); + AddPacketType("c2s_handshake_done"); + + //S2C + AddPacketType("s2c_handshake_done"); + AddPacketType("s2c_setup_world"); + + AddPacketType("s2c_chunk_data"); + AddPacketType("s2c_chunk_unload"); + + AddPacketType("s2c_entity_transform"); + AddPacketType("s2c_entity_spawn"); + + } + + private void LoadEntityTypes() { + AddEntityType("player"); + } +} diff --git a/Common/Network/Packets/S2C/Gameplay/ChunkData.cs b/Common/Network/Packets/S2C/Gameplay/ChunkData.cs index 55a1493..7cf9b37 100644 --- a/Common/Network/Packets/S2C/Gameplay/ChunkData.cs +++ b/Common/Network/Packets/S2C/Gameplay/ChunkData.cs @@ -1,4 +1,5 @@ using GlmSharp; +using Voxel.Common.Content; using Voxel.Common.Tile; using Voxel.Common.Util.Serialization; using Voxel.Common.World; @@ -18,7 +19,7 @@ public void Init(Chunk chunk) { public override void Write(VDataWriter writer) { writer.Write(position); - + //Console.WriteLine($"S: {position}"); switch (storage) { @@ -45,13 +46,17 @@ public override void Write(VDataWriter writer) { public override void Read(VDataReader reader) { position = reader.ReadIVec3(); - + //Console.WriteLine($"C: {position}"); var type = (Type)reader.ReadInt(); switch (type) { case Type.Single: { - var single = new SingleStorage(Blocks.GetBlock(reader.ReadUint()), null); + var rawID = reader.ReadUint(); + if (!ContentDatabase.Instance.Registries.Blocks.RawToEntry(rawID, out var block)) + throw new InvalidOperationException($"Could not read block from chunk data packet! Id was {rawID}"); + + var single = new SingleStorage(block, null); storage = single; //Console.WriteLine($"Got Single Storage of block {single.Block.Name}"); @@ -61,13 +66,9 @@ public override void Read(VDataReader reader) { var simple = new SimpleStorage(); storage = simple; - for (var i = 0; i < simple.BlockIds.Length; i++) { + for (var i = 0; i < simple.BlockIds.Length; i++) simple.BlockIds[i] = reader.ReadUint(); - if (!Blocks.IsBlockIDValid(simple.BlockIds[i])) - throw new InvalidOperationException($"Block ID {simple.BlockIds[i]} read from packet is invalid"); - } - //Console.WriteLine("Got Simple Storage"); break; } diff --git a/Common/Network/Packets/S2C/Gameplay/Entity/SpawnEntity.cs b/Common/Network/Packets/S2C/Gameplay/Entity/SpawnEntity.cs new file mode 100644 index 0000000..4e7606d --- /dev/null +++ b/Common/Network/Packets/S2C/Gameplay/Entity/SpawnEntity.cs @@ -0,0 +1,31 @@ +using GlmSharp; +using Voxel.Common.Util.Serialization; + +namespace Voxel.Common.Network.Packets.S2C.Gameplay.Entity; + +public class SpawnEntity : EntityPacket { + + public dvec3 position; + public dvec2 rotation; + + public override void Init(World.Entity.Entity entity) { + base.Init(entity); + + position = entity.position; + rotation = entity.rotation; + } + + public override void Write(VDataWriter writer) { + base.Write(writer); + + writer.Write(position); + writer.Write(rotation); + } + + public override void Read(VDataReader reader) { + base.Read(reader); + + position = reader.ReadDVec3(); + rotation = reader.ReadDVec2(); + } +} diff --git a/Common/Network/Packets/S2C/Handshake/S2CHandshakeDone.cs b/Common/Network/Packets/S2C/Handshake/S2CHandshakeDone.cs index e172456..f524d75 100644 --- a/Common/Network/Packets/S2C/Handshake/S2CHandshakeDone.cs +++ b/Common/Network/Packets/S2C/Handshake/S2CHandshakeDone.cs @@ -3,12 +3,12 @@ namespace Voxel.Common.Network.Packets.S2C.Handshake; public class S2CHandshakeDone : S2CPacket { - + public Guid PlayerID; public override void Write(VDataWriter writer) { - + writer.Write(PlayerID); } public override void Read(VDataReader reader) { - + PlayerID = reader.ReadGuid(); } } diff --git a/Common/Network/Packets/Utils/PacketMap.cs b/Common/Network/Packets/Utils/PacketMap.cs deleted file mode 100644 index d4ed0e3..0000000 --- a/Common/Network/Packets/Utils/PacketMap.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Reflection; -using Voxel.Common.Util.Serialization; - -namespace Voxel.Common.Network.Packets.Utils; - -public class PacketMap : RawIDMap where T : Packet { - public void FillOutgoingMap() { - var types = Assembly.GetAssembly(typeof(PacketMap<>))?.ExportedTypes; - - if (types == null) - return; - - var baseType = typeof(T); - var lastId = 0u; - - foreach (var type in types) { - if (!type.IsAssignableTo(baseType)) - continue; - - outgoingMap[type] = lastId++; - - //Console.WriteLine($"Found packet {type.Name} assignable to {baseType.Name}"); - } - } - - - public override void WriteOutgoingMap(VDataWriter writer) { - writer.Write(outgoingMap.Count); - - foreach ((var key, uint value) in outgoingMap) { - writer.Write(key.FullName); - writer.Write(value); - } - } - public override void ReadIncomingMap(VDataReader reader) { - var assembly = Assembly.GetAssembly(typeof(PacketMap<>)); - - var count = reader.ReadInt(); - for (int i = 0; i < count; i++) { - var name = reader.ReadString(); - var value = reader.ReadUint(); - - var type = assembly?.GetType(name); - - if (type == null) { - //TODO - Log error! Unexpected packet. - continue; - } - - incomingMap[value] = type; - } - } -} diff --git a/Common/Network/Packets/Utils/RawIDMap.cs b/Common/Network/Packets/Utils/RawIDMap.cs deleted file mode 100644 index e7af126..0000000 --- a/Common/Network/Packets/Utils/RawIDMap.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Voxel.Common.Util.Serialization; - -namespace Voxel.Common.Network.Packets.Utils; - -/// -/// Maps a RawID to some type and back. -/// -/// Needs to be synced before use. -/// Either map can be shared with other maps if need be. -/// -/// -public abstract class RawIDMap where T : notnull { - public Dictionary outgoingMap = new(); - public Dictionary incomingMap = new(); - - public bool TryConvertIncoming(uint raw, [NotNullWhen(true)] out T? result) => incomingMap.TryGetValue(raw, out result); - public bool TryConvertOutgoing(T instance, out uint result) => outgoingMap.TryGetValue(instance, out result); - - - public abstract void WriteOutgoingMap(VDataWriter writer); - public abstract void ReadIncomingMap(VDataReader reader); -} diff --git a/Common/Server/Components/Networking/LNLHostManager.cs b/Common/Server/Components/Networking/LNLHostManager.cs index 6ef492a..96d11f9 100644 --- a/Common/Server/Components/Networking/LNLHostManager.cs +++ b/Common/Server/Components/Networking/LNLHostManager.cs @@ -2,11 +2,13 @@ using System.Net.Sockets; using LiteNetLib; using LiteNetLib.Utils; +using Voxel.Common.Content; using Voxel.Common.Network; using Voxel.Common.Network.Packets; using Voxel.Common.Network.Packets.C2S; using Voxel.Common.Network.Packets.S2C; using Voxel.Common.Network.Packets.Utils; +using Voxel.Common.Util.Registration; using Voxel.Common.Util.Serialization.Compressed; namespace Voxel.Common.Server.Components.Networking; @@ -23,13 +25,11 @@ public class LNLHostManager : ServerComponent, INetEventListener { private readonly Dictionary ActiveConnections = new(); private readonly Queue DeadConnections = new(); - private readonly PacketMap BasePacketMap = new(); - private readonly CompressedVDataReader Reader = new(); private readonly CompressedVDataWriter Writer = new(); public LNLHostManager(VoxelServer server) : base(server) { - BasePacketMap.FillOutgoingMap(); + } public override void OnServerStart() { @@ -118,18 +118,13 @@ public void OnNetworkLatencyUpdate(NetPeer peer, int latency) { public class LNLS2CConnection : S2CConnection { private readonly LNLHostManager Manager; private readonly NetPeer Peer; - private readonly PacketMap PacketMap; - private bool synced = false; + private Registries Registries => ContentDatabase.Instance.Registries; public LNLS2CConnection(LNLHostManager manager, NetPeer peer) { Manager = manager; Peer = peer; - //Share outgoing map with rest of server. - PacketMap = new(); - PacketMap.outgoingMap = manager.BasePacketMap.outgoingMap; - OnClosed += () => { //Disconnect the peer if it was connected. if (peer.ConnectionState != ConnectionState.Disconnected) @@ -143,17 +138,19 @@ public LNLS2CConnection(LNLHostManager manager, NetPeer peer) { var writer = manager.Writer; writer.Reset(); - PacketMap.WriteOutgoingMap(writer); + Registries.WriteSync(writer); peer.Send(writer.currentBytes, 0, DeliveryMethod.ReliableOrdered); + + Console.WriteLine("Server sending sync packet"); } public override void DeliverPacket(Packet toSend) { var writer = Manager.Writer; writer.Reset(); - if (!PacketMap.outgoingMap.TryGetValue(toSend.GetType(), out var rawID)) - throw new InvalidOperationException($"Cannot send unknown packet {toSend}"); + if (!Registries.PacketTypes.TypeToRaw(toSend.GetType(), out var rawID)) + return; writer.Write(rawID); writer.Write(toSend); @@ -167,19 +164,11 @@ public void HandleNetReceive(NetDataReader nReader) { //Console.WriteLine($"Got {nReader.UserDataSize} bytes from client"); - if (!synced) { - PacketMap.ReadIncomingMap(reader); - synced = true; - - //Console.Out.WriteLine($"Synced packets from client {Peer.Id}"); - return; - } - if (packetHandler == null) return; var rawID = reader.ReadUint(); - if (!PacketMap.incomingMap.TryGetValue(rawID, out var packetType)) + if (!Registries.PacketTypes.RawToType(rawID, out var packetType)) return; //Console.WriteLine($"Got packet {packetType.Name} from client"); diff --git a/Common/Server/Components/Networking/ServerConnectionContext.cs b/Common/Server/Components/Networking/ServerConnectionContext.cs index 18b6d0c..f289127 100644 --- a/Common/Server/Components/Networking/ServerConnectionContext.cs +++ b/Common/Server/Components/Networking/ServerConnectionContext.cs @@ -1,3 +1,4 @@ +using GlmSharp; using Voxel.Common.Network; using Voxel.Common.Network.Packets; using Voxel.Common.Network.Packets.C2S; @@ -20,6 +21,7 @@ namespace Voxel.Common.Server.Components.Networking; /// public class ServerConnectionContext { public bool isDead => Connection.isDead; + public Guid playerID { get; private set; } public readonly S2CConnection Connection; @@ -45,9 +47,11 @@ public ServerConnectionContext(S2CConnection connection) { private void OnHandshakeDone(C2SHandshakeDone pkt) { Connection.packetHandler = GameplayHandler; - + //Notify client that server has finished handshake. - Connection.DeliverPacket(new S2CHandshakeDone()); + var response = PacketPool.GetPacket(); + response.PlayerID = playerID = Guid.NewGuid(); + Connection.DeliverPacket(response); Console.WriteLine("Server:Client Says Handshake Done"); GameplayStart(); @@ -68,20 +72,25 @@ private void OnPlayerUpdated(PlayerUpdated pkt) { public void Close() => Connection.Close(); + /// + /// Sends a packet to the client only if the given position is visible to them. + /// + public void SendPositionedPacket(dvec3 position, S2CPacket packet, bool returnPacket = true) { + if (loadedChunks?.ContainsPosition(position) ?? false) + SendPacket(packet, returnPacket); + } - public void SendPacket(S2CPacket packet) { + public void SendPacket(S2CPacket packet, bool returnPacket = true) { Connection.DeliverPacket(packet); - PacketPool.Return(packet); + + if (returnPacket) + PacketPool.Return(packet); } - public void SetPlayerEntity(PlayerEntity e) - => entity = e; + public void SetPlayerEntity(PlayerEntity e) => entity = e; - public void SetupViewArea(int range) { - if (entity == null) - return; - - loadedChunks = new LoadedChunkSection(entity.world, entity.chunkPosition, range, range); + public void SetupViewArea(VoxelWorld world, ivec3 position, int range) { + loadedChunks = new LoadedChunkSection(world, position, range, range); foreach (var chunk in loadedChunks.Chunks()) { var pkt = PacketPool.GetPacket(); @@ -100,7 +109,7 @@ public void SetupViewArea(int range) { loadedChunks.OnChunkRemovedFromView += c => { var pkt = PacketPool.GetPacket(); pkt.Init(c); - + SendPacket(pkt); }; } diff --git a/Common/Server/Components/PlayerManager.cs b/Common/Server/Components/PlayerManager.cs index e41775d..8ef1c7f 100644 --- a/Common/Server/Components/PlayerManager.cs +++ b/Common/Server/Components/PlayerManager.cs @@ -1,6 +1,10 @@ using GlmSharp; +using Voxel.Common.Network.Packets.S2C; +using Voxel.Common.Network.Packets.S2C.Gameplay.Entity; using Voxel.Common.Network.Packets.S2C.Handshake; +using Voxel.Common.Network.Packets.Utils; using Voxel.Common.Server.Components.Networking; +using Voxel.Common.Util; using Voxel.Common.World.Entity; using Voxel.Common.World.Entity.Player; @@ -31,18 +35,33 @@ private void OnConnectionMade(ServerConnectionContext context) { Console.WriteLine("Server:Creating player entity..."); PlayerEntity pEntity = new PlayerEntity(); - pEntity.ID = Guid.NewGuid(); + pEntity.ID = context.playerID; ContextToPlayer[context] = pEntity; PlayerToContext[pEntity] = context; context.SetPlayerEntity(pEntity); - Server.WorldManager.DefaultWorld.AddEntity(pEntity, new(0, 100, 0), dvec2.Zero); - Console.WriteLine("Server:Sending player to world..."); - context.SendPacket(new SetupWorld()); - context.SetupViewArea(5); + context.SendPacket(PacketPool.GetPacket()); + context.SetupViewArea(Server.WorldManager.DefaultWorld, new dvec3(16, 16, 16).WorldToChunkPosition(), 5); + + Server.WorldManager.DefaultWorld.AddEntity(pEntity, new(16, 16, 16), dvec2.Zero); }; } + + //Sends a packet to all players that can see a given position. + public void SendViewPacket(S2CPacket packet, dvec3 position) { + foreach (var key in ContextToPlayer.Keys) + key.SendPositionedPacket(position, packet, false); + PacketPool.Return(packet); + } + + + //Sends a packet to all players. + public void SendPacket(S2CPacket packet) { + foreach (var key in ContextToPlayer.Keys) + key.SendPacket(packet, false); + PacketPool.Return(packet); + } } diff --git a/Common/Server/Components/WorldManager.cs b/Common/Server/Components/WorldManager.cs index 20246a7..d7fe2fc 100644 --- a/Common/Server/Components/WorldManager.cs +++ b/Common/Server/Components/WorldManager.cs @@ -10,14 +10,15 @@ public class WorldManager : ServerComponent { public WorldManager(VoxelServer server) : base(server) { - Worlds["Overworld"] = DefaultWorld = new ServerWorld(); + Worlds["Overworld"] = DefaultWorld = new ServerWorld(server); } public override void OnServerStart() { } public override void Tick() { - + foreach (var world in Worlds.Values) + world.Tick(); } public override void OnServerStop() { diff --git a/Common/Server/VoxelServer.cs b/Common/Server/VoxelServer.cs index fb3ab30..43aace0 100644 --- a/Common/Server/VoxelServer.cs +++ b/Common/Server/VoxelServer.cs @@ -1,3 +1,4 @@ +using Voxel.Common.Content; using Voxel.Common.Server.Components; using Voxel.Common.Server.Components.Networking; using Voxel.Common.Util; diff --git a/Common/Server/World/ServerChunk.cs b/Common/Server/World/ServerChunk.cs new file mode 100644 index 0000000..3fc5db9 --- /dev/null +++ b/Common/Server/World/ServerChunk.cs @@ -0,0 +1,13 @@ +using GlmSharp; +using Voxel.Common.World; +using Voxel.Common.World.Storage; + +namespace Voxel.Common.Server.World; + +public class ServerChunk : Chunk { + private readonly ServerWorld ServerWorld; + + public ServerChunk(ivec3 chunkPosition, ServerWorld world, ChunkStorage? storage = null) : base(chunkPosition, world, storage) { + ServerWorld = world; + } +} diff --git a/Common/Server/World/ServerWorld.cs b/Common/Server/World/ServerWorld.cs index ed0f64f..ca601b4 100644 --- a/Common/Server/World/ServerWorld.cs +++ b/Common/Server/World/ServerWorld.cs @@ -1,13 +1,37 @@ using GlmSharp; +using Voxel.Common.Network.Packets.S2C.Gameplay.Entity; +using Voxel.Common.Network.Packets.Utils; using Voxel.Common.World; +using Voxel.Common.World.Entity; using Voxel.Common.World.Generation; namespace Voxel.Common.Server.World; public class ServerWorld : VoxelWorld { + public readonly VoxelServer Server; + + public ServerWorld(VoxelServer server) { + Server = server; + } + protected override Chunk CreateChunk(ivec3 pos) { - var baseChunk = base.CreateChunk(pos); - SimpleGenerator.GenerateChunk(baseChunk); - return baseChunk; + var newChunk = new ServerChunk(pos, this); + SimpleGenerator.GenerateChunk(newChunk); + return newChunk; + } + + public override void AddEntity(Entity entity, dvec3 position, dvec2 rotation) { + base.AddEntity(entity, position, rotation); + + var spawnEntityPacket = PacketPool.GetPacket(); + spawnEntityPacket.Init(entity); + + Server.PlayerManager.SendViewPacket(spawnEntityPacket, position); + } + + public override void ProcessEntity(Entity e) { + base.ProcessEntity(e); + + //TODO - sync packet } } diff --git a/Common/Tile/Block.cs b/Common/Tile/Block.cs index 7c53ff2..1ebc346 100644 --- a/Common/Tile/Block.cs +++ b/Common/Tile/Block.cs @@ -2,10 +2,10 @@ namespace Voxel.Common.Tile; public class Block { public readonly string Name; - public readonly BlockSettings Settings; - public bool IsNotAir => Settings.IsNotAir; + public uint id { get; internal set; } - public uint id; + public readonly BlockSettings Settings; + public bool IsAir => Settings.IsAir; public Block(string name, BlockSettings settings) { Name = name; @@ -23,23 +23,23 @@ public Block(string name) : this(name, BlockSettings.Default) {} public class BlockSettings { public static readonly BlockSettings Default = new Builder().Build(); - public readonly bool IsNotAir; - public float GetSolidityFloat => IsNotAir ? 1 : 0; + public readonly bool IsAir; + public float GetSolidityFloat => IsAir ? 0 : 1; - private BlockSettings(bool isNotAir) { - IsNotAir = isNotAir; + private BlockSettings(bool isAir) { + IsAir = isAir; } public class Builder { - public bool IsNotAir = true; + public bool IsAir; public Builder() {} public Builder(BlockSettings settings) { - IsNotAir = settings.IsNotAir; + IsAir = settings.IsAir; } public Builder(Block block) : this(block.Settings) {} - public BlockSettings Build() => new(IsNotAir); + public BlockSettings Build() => new(IsAir); } } diff --git a/Common/Tile/Blocks.cs b/Common/Tile/Blocks.cs deleted file mode 100644 index be0c87d..0000000 --- a/Common/Tile/Blocks.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -namespace Voxel.Common.Tile; - -public static class Blocks { - public static readonly Block[] BlockArray; - - private static readonly Dictionary BlocksByName = new(); - - public static readonly Block Air = new("air", new BlockSettings.Builder { - IsNotAir = false - }); - public static readonly Block Stone = new("stone"); - public static readonly Block Dirt = new("dirt"); - public static readonly Block Grass = new("grass"); - - private static List? blockList; - - static Blocks() { - blockList = new(); - - RegisterBlock(Air); - - RegisterBlock(Stone); - - RegisterBlock(Dirt); - - RegisterBlock(Grass); - - BlockArray = blockList.ToArray(); - blockList = null; - } - - private static T RegisterBlock(T toRegister) where T : Block { - uint id = (uint)blockList!.Count; - toRegister.id = id; - blockList.Add(toRegister); - BlocksByName[toRegister.Name] = toRegister; - return toRegister; - } - - public static Block GetBlock(uint id) - => BlockArray[id]; - public static bool GetBlock(string name, [NotNullWhen(true)] out Block? block) - => BlocksByName.TryGetValue(name, out block); - - public static bool IsBlockIDValid(uint id) => id < BlockArray.Length; - - public static Block? GetBlock(string name) { - GetBlock(name, out var block); - return block; - } -} diff --git a/Common/Util/Registration/BaseRegistry.cs b/Common/Util/Registration/BaseRegistry.cs new file mode 100644 index 0000000..97a1afc --- /dev/null +++ b/Common/Util/Registration/BaseRegistry.cs @@ -0,0 +1,13 @@ +using Voxel.Common.Util.Serialization; + +namespace Voxel.Common.Util.Registration; + +public interface BaseRegistry { + + public void GenerateIDs(); + + public void Write(VDataWriter writer); + public void Read(VDataReader reader); + + public void Clear(); +} diff --git a/Common/Util/Registration/Registries.cs b/Common/Util/Registration/Registries.cs new file mode 100644 index 0000000..c072125 --- /dev/null +++ b/Common/Util/Registration/Registries.cs @@ -0,0 +1,55 @@ +using Voxel.Common.Network.Packets; +using Voxel.Common.Tile; +using Voxel.Common.Util.Serialization; +using Voxel.Common.World.Entity; + +namespace Voxel.Common.Util.Registration; + +public class Registries { + private Dictionary RegistryList = new(); + + public readonly SimpleRegistry Blocks = new(); + public readonly TypedRegistry EntityTypes = new(); + + public readonly TypedRegistry PacketTypes = new(); + + public Registries() { + AddRegistry("packet_type", PacketTypes); + + AddRegistry("block", Blocks); + AddRegistry("entity_type", EntityTypes); + } + + public void AddRegistry(string name, T toAdd) where T : BaseRegistry => RegistryList[name] = toAdd; + + public void WriteSync(VDataWriter writer) { + writer.Write(RegistryList.Count); + + foreach ((string? name, var baseReg) in RegistryList) { + writer.Write(name); + baseReg.Write(writer); + } + } + + public void ReadSync(VDataReader reader) { + var count = reader.ReadInt(); + + for (int i = 0; i < count; i++) { + var name = reader.ReadString(); + + if (!RegistryList.TryGetValue(name, out var reg)) + throw new InvalidOperationException($"Unrecognized registry {name}!"); + + reg.Read(reader); + } + } + + public void Clear() { + foreach (var value in RegistryList.Values) + value.Clear(); + } + public void GenerateIDs() { + foreach (var value in RegistryList.Values) + value.GenerateIDs(); + } +} diff --git a/Common/Util/Registration/Registry.cs b/Common/Util/Registration/Registry.cs new file mode 100644 index 0000000..fc1821b --- /dev/null +++ b/Common/Util/Registration/Registry.cs @@ -0,0 +1,19 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Voxel.Common.Util.Registration; + +public interface Registry : BaseRegistry { + public bool RawToID(uint raw, [NotNullWhen(true)] out string? id); + public bool RawToEntry(uint raw, [NotNullWhen(true)] out T? entry); + + public bool IdToRaw(string id, out uint raw); + public bool IdToEntry(string id, [NotNullWhen(true)] out T? entry); + + public bool EntryToRaw(T entry, out uint raw); + public bool EntryToID(T entry, [NotNullWhen(true)] out string? id); + + public T Register(T toRegister, string id); + + + IEnumerable<(T, string, uint)> Entries(); +} diff --git a/Common/Util/Registration/SimpleRegistry.cs b/Common/Util/Registration/SimpleRegistry.cs new file mode 100644 index 0000000..c27c309 --- /dev/null +++ b/Common/Util/Registration/SimpleRegistry.cs @@ -0,0 +1,103 @@ +using System.Diagnostics.CodeAnalysis; +using Voxel.Common.Util.Serialization; + +namespace Voxel.Common.Util.Registration; + +public class SimpleRegistry : Registry where T : notnull { + private string[] rawToID; + private T[] rawToEntry; + + private readonly Dictionary idToRaw = new(); + private readonly Dictionary idToEntry = new(); + + private readonly Dictionary entryToRaw = new(); + private readonly Dictionary entryToId = new(); + + private readonly Dictionary registeredEntries = new(); + + protected virtual void Put(T entry, string id, uint raw) { + rawToID[raw] = id; + rawToEntry[raw] = entry; + + idToRaw[id] = raw; + idToEntry[id] = entry; + + entryToRaw[entry] = raw; + entryToId[entry] = id; + } + + public T RawToEntryDirect(uint raw) => rawToEntry[raw]; + + public bool RawToID(uint raw, [NotNullWhen(true)] out string? id) { + id = rawToID[raw]; + return true; + } + public bool RawToEntry(uint raw, [NotNullWhen(true)] out T? entry) { + entry = rawToEntry[raw]; + return true; + } + + public bool IdToRaw(string id, out uint raw) => idToRaw.TryGetValue(id, out raw); + public bool IdToEntry(string id, [NotNullWhen(true)] out T? entry) => idToEntry.TryGetValue(id, out entry); + + public bool EntryToRaw(T entry, out uint raw) => entryToRaw.TryGetValue(entry, out raw); + public bool EntryToID(T entry, [NotNullWhen(true)] out string? id) => entryToId.TryGetValue(entry, out id); + + public T Register(T toRegister, string id) { + registeredEntries[id] = toRegister; + return toRegister; + } + public IEnumerable<(T, string, uint)> Entries() { + for (uint raw = 0; raw < rawToID.Length; raw++) + yield return (rawToEntry[raw], rawToID[raw], raw); + } + + public virtual void GenerateIDs() { + var currentID = 0u; + + rawToID = new string[registeredEntries.Count]; + rawToEntry = new T[registeredEntries.Count]; + + foreach ((string? id, var entry) in registeredEntries) + Put(entry, id, currentID++); + } + + public virtual void Write(VDataWriter writer) { + writer.Write(registeredEntries.Count); + + foreach ((string id, uint raw) in idToRaw) { + writer.Write(id); + writer.Write(raw); + } + } + public virtual void Read(VDataReader reader) { + idToRaw.Clear(); + idToEntry.Clear(); + entryToRaw.Clear(); + entryToId.Clear(); + + var count = reader.ReadInt(); + + rawToID = new string[count]; + rawToEntry = new T[count]; + + for (int i = 0; i < count; i++) { + var id = reader.ReadString(); + var raw = reader.ReadUint(); + + if (!registeredEntries.TryGetValue(id, out var entry)) + continue; + + Put(entry, id, raw); + } + } + + + public void Clear() { + idToRaw.Clear(); + idToEntry.Clear(); + entryToRaw.Clear(); + entryToId.Clear(); + registeredEntries.Clear(); + } +} diff --git a/Common/Util/Registration/TypedRegistry.cs b/Common/Util/Registration/TypedRegistry.cs new file mode 100644 index 0000000..2005237 --- /dev/null +++ b/Common/Util/Registration/TypedRegistry.cs @@ -0,0 +1,67 @@ +using System.Reflection; + +namespace Voxel.Common.Util.Registration; + +public class TypedRegistry : SimpleRegistry.TypedEntry> where T : class { + + private readonly Dictionary typeToRaw = new(); + private readonly Dictionary typeToId = new(); + private readonly Dictionary typeToEntry = new(); + + private readonly Dictionary rawToType = new(); + private readonly Dictionary idToType = new(); + private readonly Dictionary entryToType = new(); + + protected override void Put(TypedEntry entry, string id, uint raw) { + base.Put(entry, id, raw); + + typeToRaw[entry.type] = raw; + typeToId[entry.type] = id; + typeToEntry[entry.type] = entry; + + rawToType[raw] = entry.type; + idToType[id] = entry.type; + entryToType[entry] = entry.type; + } + + public void Register(string id) where TReg : T, new() { + var typedEntry = new TypedEntry(); + typedEntry.Setup(); + base.Register(typedEntry, id); + } + + private void TRegisterUniqueNameIdgaf(string id) where TReg : T, new() => Register(id); + + public void Register(string id, Type t) { + var genericmethod = GetType().GetMethod(nameof(TRegisterUniqueNameIdgaf), BindingFlags.Instance | BindingFlags.NonPublic)?.MakeGenericMethod(t); + genericmethod?.Invoke(this, new object?[] { + id + }); + } + + public bool TypeToRaw(Type type, out uint raw) => typeToRaw.TryGetValue(type, out raw); + public bool TypeToId(Type type, out string id) => typeToId.TryGetValue(type, out id); + public bool TypeToEntry(Type type, out TypedEntry entry) => typeToEntry.TryGetValue(type, out entry); + + public bool RawToType(uint raw, out Type type) => rawToType.TryGetValue(raw, out type); + public bool IdToType(string id, out Type type) => idToType.TryGetValue(id, out type); + public bool EntryToType(TypedEntry entry, out Type type) => entryToType.TryGetValue(entry, out type); + + + public class TypedEntry { + public Type type { get; private set; } + public Func factory { get; private set; } + + public void Setup() where TReg : T, new() { + type = typeof(TReg); + SetFactory(); + } + + public void Setup(Type t) { + var genericmethod = GetType().GetMethod(nameof(Setup))?.MakeGenericMethod(t); + genericmethod?.Invoke(this, null); + } + + public void SetFactory() where TReg : T, new() => factory = () => new TReg(); + } +} diff --git a/Common/Util/Serialization/VDataWriter.cs b/Common/Util/Serialization/VDataWriter.cs index 05f8365..9d496a7 100644 --- a/Common/Util/Serialization/VDataWriter.cs +++ b/Common/Util/Serialization/VDataWriter.cs @@ -89,6 +89,8 @@ public void Write(string data) => Write(data, Encoding.UTF8); public void Write(string data, Encoding encoding) { + if (data.Length == 0) + throw new InvalidOperationException("Cannot write empty string"); var len = encoding.GetByteCount(data); Write(len); diff --git a/Common/World/Chunk.cs b/Common/World/Chunk.cs index d8eb428..791be0f 100644 --- a/Common/World/Chunk.cs +++ b/Common/World/Chunk.cs @@ -1,9 +1,10 @@ -using System; -using System.Threading; using GlmSharp; +using Voxel.Common.Content; using Voxel.Common.Tile; using Voxel.Common.Util; using Voxel.Common.World.Storage; +using Voxel.Common.World.Tick; +using Voxel.Core.Util; namespace Voxel.Common.World; @@ -13,7 +14,8 @@ public class Chunk : Tickable, IDisposable { public readonly VoxelWorld World; - private readonly List Tickables = new(); + public readonly TickList TickList = new(); + public readonly List Entities = new(); public ChunkStorage storage { get; private set; } @@ -24,10 +26,10 @@ public class Chunk : Tickable, IDisposable { /// private uint _version; - public bool IsEmpty => storage is SingleStorage ss && ss.Block == Blocks.Air; + public bool IsEmpty => storage is SingleStorage ss && ss.Block == MainContentPack.Instance.Air; public Chunk(ivec3 chunkPosition, VoxelWorld world, ChunkStorage? storage = null) { - this.storage = storage ?? new SingleStorage(Blocks.Air, this); + this.storage = storage ?? new SingleStorage(MainContentPack.Instance.Air, this); ChunkPosition = chunkPosition; WorldPosition = ChunkPosition * PositionExtensions.ChunkSize; @@ -71,12 +73,27 @@ internal void DecrementViewCount() { public void Tick() { - foreach (var tickable in Tickables) - tickable.Tick(); + //Update deffered lists. + TickList.UpdateCollection(); + + //Tick all the tickables in this chunk. + foreach (var tickable in TickList) + ProcessTickable(tickable); + } + + public virtual void ProcessTickable(Tickable t) { + t.Tick(); } - public void AddTickable(Tickable tickable) => Tickables.Add(tickable); - public void RemoveTickable(Tickable tickable) => Tickables.Remove(tickable); + public void AddTickable(Tickable tickable) => TickList.Add(tickable); + public void RemoveTickable(Tickable tickable) => TickList.Remove(tickable); + + internal void AddEntity(T toAdd) where T : Entity.Entity { + Entities.Add(toAdd); + } + internal void RemoveEntity(T toRemove) where T : Entity.Entity { + Entities.Remove(toRemove); + } public void Dispose() { storage.Dispose(); diff --git a/Common/World/Entity/Entity.cs b/Common/World/Entity/Entity.cs index c3a78fd..5450373 100644 --- a/Common/World/Entity/Entity.cs +++ b/Common/World/Entity/Entity.cs @@ -1,12 +1,13 @@ using GlmSharp; using Voxel.Common.Collision; using Voxel.Common.Util; +using Voxel.Common.World.Tick; namespace Voxel.Common.World.Entity; -public abstract class Entity : Tickable { +public abstract class Entity { public VoxelWorld world { get; private set; } - public Chunk chunk { get; private set; } + public Chunk chunk { get; internal set; } public Guid ID = Guid.Empty; @@ -19,12 +20,11 @@ public abstract class Entity : Tickable { /// public dvec2 rotation = dvec2.Zero; - public dvec3 lastPosition { get; private set; } - public dvec2 lastRotation { get; private set; } + public dvec3 lastPosition { get; internal set; } + public dvec2 lastRotation { get; internal set; } public dvec3 velocity { get; set; } - public dvec3 preMoveVelocity { get; private set; } - public bool isOnFloor { get; private set; } + public bool isOnFloor { get; internal set; } /// /// World-space 3d block position of the entity. @@ -46,7 +46,11 @@ public ivec3 blockPosition { public dvec3 eyeOffset => dvec3.UnitY * (eyeHeight - boundingBox.size.y * 0.5); - public void AddToWorld(VoxelWorld newWorld, dvec3 pos, dvec2 rot) { + public Entity() { + + } + + public void AddedToWorld(VoxelWorld newWorld, dvec3 pos, dvec2 rot) { world = newWorld; position = pos; rotation = rot; @@ -55,18 +59,6 @@ public void AddToWorld(VoxelWorld newWorld, dvec3 pos, dvec2 rot) { public virtual void OnAddedToWorld() {} - public virtual void Tick() { - lastPosition = position; - lastRotation = rotation; - - preMoveVelocity = velocity; - velocity = MoveAndSlide(velocity); - position += velocity * Constants.SecondsPerTick; - - //If we're moving down && the new velocity after moving is greater than the velocity before we moved, then we hit a floor. - isOnFloor = preMoveVelocity.y < 0 && velocity.y > preMoveVelocity.y; - } - /// /// Queues an entity to be destroyed at the end of the tick. /// diff --git a/Common/World/Entity/LivingEntity.cs b/Common/World/Entity/LivingEntity.cs index 98118c5..9203c2b 100644 --- a/Common/World/Entity/LivingEntity.cs +++ b/Common/World/Entity/LivingEntity.cs @@ -3,11 +3,18 @@ namespace Voxel.Common.World.Entity; -public abstract class LivingEntity : Entity { +public abstract class LivingEntity : TickedEntity { public override void Tick() { base.Tick(); - + + var preMoveVelocity = velocity; + velocity = MoveAndSlide(velocity); + position += velocity * Constants.SecondsPerTick; + + //If we're moving down && the new velocity after moving is greater than the velocity before we moved, then we hit a floor. + isOnFloor = preMoveVelocity.y < 0 && velocity.y > preMoveVelocity.y; + if (!isOnFloor) { var verticalVelocity = velocity.y; verticalVelocity = Math.Max(-32, verticalVelocity - Constants.GravityPerTick); diff --git a/Common/World/Entity/TickedEntity.cs b/Common/World/Entity/TickedEntity.cs new file mode 100644 index 0000000..efdee04 --- /dev/null +++ b/Common/World/Entity/TickedEntity.cs @@ -0,0 +1,11 @@ +using Voxel.Common.World.Tick; + +namespace Voxel.Common.World.Entity; + +public abstract class TickedEntity : Entity, Tickable { + + public virtual void Tick() { + lastPosition = position; + lastRotation = rotation; + } +} diff --git a/Common/World/Generation/SimpleGenerator.cs b/Common/World/Generation/SimpleGenerator.cs index ed6a02a..d7cb1cd 100644 --- a/Common/World/Generation/SimpleGenerator.cs +++ b/Common/World/Generation/SimpleGenerator.cs @@ -1,5 +1,6 @@ using System.Buffers; using FastNoiseOO; +using Voxel.Common.Content; using Voxel.Common.Tile; using Voxel.Common.Util; using Voxel.Common.World.Storage; @@ -17,7 +18,7 @@ public static void GenerateChunk(Chunk target) { var start = DateTime.Now; float[] noise = Pool.Rent(PositionExtensions.ChunkCapacity); - var storage = new SimpleStorage(Blocks.Air); + var storage = new SimpleStorage(MainContentPack.Instance.Air); var basePosition = target.WorldPosition; @@ -33,11 +34,11 @@ public static void GenerateChunk(Chunk target) { var density = noise[i]; if (density < 0) - storage.SetBlock(Blocks.Stone, i); + storage.SetBlock(MainContentPack.Instance.Stone, i); else if (density < 0.2) - storage.SetBlock(Blocks.Dirt, i); + storage.SetBlock(MainContentPack.Instance.Dirt, i); else if (density < 0.3) - storage.SetBlock(Blocks.Grass, i); + storage.SetBlock(MainContentPack.Instance.Grass, i); } storage.ReduceIfPossible(target, out var newStorage); diff --git a/Common/World/LoadedChunkSection.cs b/Common/World/LoadedChunkSection.cs index bbf7dae..3fb942e 100644 --- a/Common/World/LoadedChunkSection.cs +++ b/Common/World/LoadedChunkSection.cs @@ -77,4 +77,10 @@ public IEnumerable Chunks() { foreach (var view in views.Values) yield return view.Chunk; } + + + public bool ContainsPosition(dvec3 worldPosition) { + var chunkPos = worldPosition.WorldToChunkPosition(); + return views.ContainsKey(chunkPos); + } } diff --git a/Common/World/Storage/SimpleStorage.cs b/Common/World/Storage/SimpleStorage.cs index 38bc2a2..1d58d20 100644 --- a/Common/World/Storage/SimpleStorage.cs +++ b/Common/World/Storage/SimpleStorage.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Voxel.Common.Content; using Voxel.Common.Tile; using Voxel.Common.Util; @@ -32,7 +33,7 @@ private static uint[] GetBlockData() { } internal override void SetBlock(Block toSet, int index) => BlockIds[index] = toSet.id; - internal override Block GetBlock(int index) => Blocks.GetBlock(BlockIds[index]); + internal override Block GetBlock(int index) => ContentDatabase.Instance.Registries.Blocks.RawToEntryDirect(BlockIds[index]); public override ChunkStorage GenerateCopy() { var newStorage = new SimpleStorage(); BlockIds.CopyTo(newStorage.BlockIds.AsSpan()); @@ -56,7 +57,8 @@ public bool ReduceIfPossible(Chunk target, out ChunkStorage newStorage) { } } - newStorage = new SingleStorage(Blocks.GetBlock(startingID), target); + this.Dispose(); + newStorage = new SingleStorage(ContentDatabase.Instance.Registries.Blocks.RawToEntryDirect(startingID), target); return true; } } diff --git a/Common/World/Tick/TickList.cs b/Common/World/Tick/TickList.cs new file mode 100644 index 0000000..4a1ada4 --- /dev/null +++ b/Common/World/Tick/TickList.cs @@ -0,0 +1,9 @@ +using Voxel.Core.Util; + +namespace Voxel.Common.World.Tick; + +public class TickList : DefferedList, Tickable { + public void Tick() { + UpdateCollection(); + } +} diff --git a/Common/World/Tickable.cs b/Common/World/Tick/Tickable.cs similarity index 61% rename from Common/World/Tickable.cs rename to Common/World/Tick/Tickable.cs index 818597b..fc9939b 100644 --- a/Common/World/Tickable.cs +++ b/Common/World/Tick/Tickable.cs @@ -1,4 +1,4 @@ -namespace Voxel.Common.World; +namespace Voxel.Common.World.Tick; public interface Tickable { public void Tick(); diff --git a/Common/World/Views/SnapshotView.cs b/Common/World/Views/SnapshotView.cs index c3eada9..c625241 100644 --- a/Common/World/Views/SnapshotView.cs +++ b/Common/World/Views/SnapshotView.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; using GlmSharp; +using Voxel.Common.Content; using Voxel.Common.Tile; using Voxel.Common.Util; using Voxel.Common.World.Storage; @@ -32,7 +32,7 @@ public Block GetBlock(ivec3 position) { var chunkPos = position.BlockToChunkPosition(); if (!Storages.TryGetValue(chunkPos, out var storage)) - return Blocks.Air; + return MainContentPack.Instance.Air; var localPos = position - chunkPos; return storage[localPos]; diff --git a/Common/World/VoxelWorld.cs b/Common/World/VoxelWorld.cs index 83a6b36..2af011d 100644 --- a/Common/World/VoxelWorld.cs +++ b/Common/World/VoxelWorld.cs @@ -1,9 +1,13 @@ using System.Diagnostics.CodeAnalysis; +using System.Transactions; using GlmSharp; using Voxel.Common.Collision; +using Voxel.Common.Content; using Voxel.Common.Tile; using Voxel.Common.Util; +using Voxel.Common.World.Tick; using Voxel.Common.World.Views; +using Voxel.Core.Util; namespace Voxel.Common.World; @@ -12,7 +16,9 @@ public abstract class VoxelWorld : BlockView, ColliderProvider { private readonly Dictionary Chunks = new(); private readonly List CollisionShapeCache = new(); - public List GlobalTickables = new(); + public readonly TickList GlobalTickables = new(); + private readonly DefferedList WorldEntities = new(); + private readonly Dictionary EntitiesByID = new(); public bool TryGetChunkRaw(ivec3 chunkPos, [NotNullWhen(true)] out Chunk? chunk) => Chunks.TryGetValue(chunkPos, out chunk); public bool TryGetChunk(dvec3 worldPosition, [NotNullWhen(true)] out Chunk? chunk) => TryGetChunkRaw(worldPosition.WorldToChunkPosition(), out chunk); @@ -51,6 +57,10 @@ internal void UnloadChunk(ivec3 chunkPosition) { GlobalTickables.Remove(c); c.storage.Dispose(); + + //Remove entities that were a part of that chunk from the global entity list. + foreach (var entity in c.Entities) + WorldEntities.Remove(entity); } internal ChunkView GetOrCreateChunkView(ivec3 chunkPosition) { @@ -70,7 +80,7 @@ public void SetBlock(ivec3 position, Block block) { public Block GetBlock(ivec3 position) { var chunkPos = position.BlockToChunkPosition(); if (!TryGetChunkRaw(chunkPos, out var chunk)) - return Blocks.Air; + return MainContentPack.Instance.Air; var lPos = position - chunk.WorldPosition; return chunk.GetBlock(lPos); @@ -87,7 +97,7 @@ public List GatherColliders(AABB box) { foreach (var pos in Iteration.Cubic(min, max)) { var chunkPos = pos.BlockToChunkPosition(); - if (!IsChunkLoadedRaw(chunkPos) || GetBlock(pos).IsNotAir) + if (!IsChunkLoadedRaw(chunkPos) || !GetBlock(pos).IsAir) CollisionShapeCache.Add(AABB.FromPosSize(pos + half, dvec3.Ones)); } @@ -95,15 +105,65 @@ public List GatherColliders(AABB box) { } public virtual void AddEntity(Entity.Entity entity, dvec3 position, dvec2 rotation) { - entity.AddToWorld(this, position, rotation); + if (!TryGetChunkRaw(position.WorldToChunkPosition(), out var chunk)) + throw new InvalidOperationException("Cannot add entity to chunk that doesn't exist"); + + //Notify entity that it was added to the world. + entity.chunk = chunk; + entity.AddedToWorld(this, position, rotation); + + //Add entity to list of all loaded entities and add it to the chunk it belongs to. + WorldEntities.Add(entity); + EntitiesByID[entity.ID] = entity; + chunk.AddEntity(entity); + } + + /// + /// Called to remove the entity from the world. + /// + /// + public virtual void RemoveEntity(Entity.Entity entity) { + WorldEntities.Remove(entity); + EntitiesByID.Remove(entity.ID); + entity.chunk.RemoveEntity(entity); - var chunk = GetOrCreateChunk(entity.chunkPosition); - chunk.AddTickable(entity); + Console.WriteLine($"Unloading Entity {entity}"); } + public virtual bool TryGetEntity(Guid id, [NotNullWhen(true)] out Entity.Entity? entity) => EntitiesByID.TryGetValue(id, out entity); + public void Tick() { + //Update chunks and other tickables. + GlobalTickables.UpdateCollection(); foreach (var tickable in GlobalTickables) tickable.Tick(); + + //Update all entities. + WorldEntities.UpdateCollection(); + foreach (var entity in WorldEntities) { + //Process entity. + ProcessEntity(entity); + + //Move entity to new chunk if required. + if (entity.chunk.ChunkPosition != entity.chunkPosition) { + //If new chunk does not exist, unload entity. + if (!TryGetChunkRaw(entity.chunkPosition, out var chunk)) { + RemoveEntity(entity); + return; + } + + //Move entity to new chunk. + entity.chunk.RemoveEntity(entity); + entity.chunk = chunk; + chunk.AddEntity(entity); + + //Console.WriteLine($"Moving entity {entity.ID} to chunk {entity.chunkPosition}"); + } + } + } + + public virtual void ProcessEntity(Entity.Entity e) { + if (e is Tickable t) t.Tick(); } public void Dispose() { diff --git a/Core/Util/DefferedList.cs b/Core/Util/DefferedList.cs new file mode 100644 index 0000000..b648146 --- /dev/null +++ b/Core/Util/DefferedList.cs @@ -0,0 +1,36 @@ +using System.Collections; + +namespace Voxel.Core.Util; + +public class DefferedList : ICollection { + private readonly List RemoveList = new List(); + private readonly List Tickables = new List(); + private readonly List AddList = new List(); + + public int Count => Tickables.Count; + public bool IsReadOnly => false; + + public void UpdateCollection() { + Tickables.AddRange(AddList); + foreach (var t in RemoveList) Tickables.Remove(t); + + AddList.Clear(); + RemoveList.Clear(); + } + + public IEnumerator GetEnumerator() => Tickables.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => Tickables.GetEnumerator(); + public void Add(T item) => AddList.Add(item); + public void Clear() { + RemoveList.Clear(); + AddList.Clear(); + Tickables.Clear(); + } + public bool Contains(T item) => Tickables.Contains(item); + public void CopyTo(T[] array, int arrayIndex) => Tickables.CopyTo(array, arrayIndex); + public bool Remove(T item) { + RemoveList.Add(item); + return true; + } + +} diff --git a/Core/Util/SwapList.cs b/Core/Util/SwapList.cs new file mode 100644 index 0000000..e614bd2 --- /dev/null +++ b/Core/Util/SwapList.cs @@ -0,0 +1,83 @@ +using System.Collections; +using System.Numerics; + +namespace Voxel.Core.Util; + +/// +/// Unordered list-like collection that fills in spaces on the list to prevent large copies. +/// +public class SwapList : IList, IReadOnlyList { + + private T[] data; + + public int Count { get; private set; } + public bool IsReadOnly => false; + + public T this[int index] { + get => data[index]; + set => data[index] = value; + } + + public SwapList(int capacity = 16) { + data = new T[capacity]; + } + + private void ExpandIfNeeded(int by) { + var newCount = Count + by; + + if (newCount >= data.Length) { + var oldData = data; + var newData = new T[BitOperations.RoundUpToPowerOf2((uint)data.Length + 1)]; + + oldData.CopyTo(newData.AsSpan()); + data = newData; + } + } + + public IEnumerator GetEnumerator() { + for (int i = 0; i < Count; i++) + yield return data[i]; + } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public void Add(T item) { + ExpandIfNeeded(1); + + data[Count - 1] = item; + Count++; + } + + public bool Remove(T item) { + var index = Array.IndexOf(data, item); + + if (index == -1) + return false; + RemoveAt(index); + return true; + } + + public bool Contains(T item) => Array.IndexOf(data, item) != -1; + public void CopyTo(T[] array, int arrayIndex) => data.CopyTo(array, arrayIndex); + + public void Clear() { + Array.Fill(data, default); + Count = 0; + } + public int IndexOf(T item) => Array.IndexOf(data, item); + public void Insert(int index, T item) => throw new NotImplementedException(); //tbh just can't be bothered to implement + + + public void RemoveAt(int index) { + if (Count - 1 == index) { + //If element is last element, simply remove it. + data[index] = default; + } else { + //If element is not last element, put the last element in its place. + data[index] = data[Count - 1]; + data[Count - 1] = default; + } + + Count--; + } + +}