1using System.Collections.Concurrent;
2using System.Numerics;
3using GlmSharp;
4using Kestrel.Framework.Client.Graphics;
5using Kestrel.Framework.Client.Graphics.Buffers;
6using Kestrel.Framework.Client.Graphics.Camera;
7using Kestrel.Framework.Networking.Packets;
8using Kestrel.Framework.Networking.Packets.C2S;
9using Kestrel.Framework.Utils;
10using KestrelWorld = Kestrel.Framework.World.World;
11using LiteNetLib;
12using Silk.NET.OpenGL;
13using Silk.NET.Windowing;
14using ArchWorld = Arch.Core.World;
15using ArchEntity = Arch.Core.Entity;
16using Kestrel.Framework.Entity.Components;
17using Arch.Core;
18using Arch.Core.Extensions;
19using Kestrel.Framework.Entity;
20
21public enum ClientStatus
22{
23 Connecting,
24 Connected,
25 Disconnected
26}
27
28public class ClientState
29{
30 public ClientStatus status = ClientStatus.Connecting;
31 public Kestrel.Framework.Client.Graphics.Window Window;
32 public Camera Camera;
33 public ArchEntity Player;
34 public string PlayerName;
35 public ArchWorld Entities;
36 public ConcurrentDictionary<Guid, ArchEntity> NetworkableEntities = [];
37 public ConcurrentDictionary<Guid, ArchEntity> ServerIdToEntity = [];
38 public NetPeer NetServer;
39 public KestrelWorld World;
40 public ConcurrentDictionary<Vector3I, ChunkMesh> ChunkMeshes = [];
41 public ChunkMeshManager ChunkMeshManager = new();
42 public int RenderDistance = 12;
43 public HashSet<Vector3I> RequestedChunks = [];
44 public HashSet<Vector3I> RequestedChunksQueue = [];
45 public Profiler Profiler = new();
46
47#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
48 public ClientState(GL gl, IWindow silkWindow)
49#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
50 {
51 Window = new(gl, 800, 600);
52 silkWindow.FramebufferResize += @Window.OnResize;
53
54 Camera = new ThirdPersonCamera(this);
55 }
56
57 public void RequestChunk(Vector3I chunkPos)
58 {
59 if (RequestedChunks.Contains(chunkPos))
60 return;
61 RequestedChunks.Add(chunkPos);
62 RequestedChunksQueue.Add(chunkPos);
63 }
64
65 public void RequestChunksFromQueue()
66 {
67 if (RequestedChunksQueue.Count == 0 || NetServer == null) return;
68
69 if (!Player.Has<Location>())
70 throw new Exception("Player does not have required Location component.");
71
72 Location playerLocation = Player.Get<Location>();
73
74 World.WorldToChunk(
75 (int)playerLocation.X, (int)playerLocation.Y, (int)playerLocation.Z,
76 out var playerChunk, out _);
77
78 var queueSortedByDistance = new List<Vector3I>(RequestedChunksQueue);
79 queueSortedByDistance.Sort((a, b) =>
80 {
81 float aDistance = LocationUtil.HorizontallyWeightedDistance(playerChunk.ToVector3(), a.ToVector3());
82 float bDistance = LocationUtil.HorizontallyWeightedDistance(playerChunk.ToVector3(), b.ToVector3());
83 return aDistance.CompareTo(bDistance);
84 });
85
86
87 int batchCount = Math.Min(8, queueSortedByDistance.Count);
88 NetServer.Send(IPacket.Serialize(new C2SChunkRequest
89 {
90 ChunkCount = batchCount,
91 Chunks = [.. queueSortedByDistance.Take(batchCount)]
92 }), DeliveryMethod.ReliableOrdered);
93
94 foreach (var chunkPos in queueSortedByDistance.Take(batchCount))
95 {
96 RequestedChunksQueue.Remove(chunkPos);
97 }
98 }
99}