1using System.Drawing;
2using System.Numerics;
3using Arch.Core.Extensions;
4using GlmSharp;
5using Kestrel.Framework.Client.Graphics;
6using Kestrel.Framework.Client.Graphics.Buffers;
7using Kestrel.Framework.Client.Graphics.Shaders;
8using Kestrel.Framework.Entity.Components;
9using Kestrel.Framework.Networking;
10using Kestrel.Framework.Networking.Packets;
11using Kestrel.Framework.Networking.Packets.C2S;
12using Kestrel.Framework.Networking.Packets.S2C;
13using Kestrel.Framework.Utils;
14using LiteNetLib;
15using LiteNetLib.Utils;
16using Silk.NET.Core.Contexts;
17using Silk.NET.GLFW;
18using Silk.NET.Input;
19using Silk.NET.Maths;
20using Silk.NET.OpenGL;
21using Silk.NET.Windowing;
22using ArchEntity = Arch.Core.Entity;
23using ArchWorld = Arch.Core.World;
24
25namespace Kestrel.Framework.Platform;
26
27public class Client
28{
29#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.
30 private IWindow _window;
31 private IInputContext _input;
32 public ClientState clientState;
33 private GL _gl;
34 private ShaderProgram _shaderProgram;
35 private QuadMesh quad;
36 NetManager networkClient;
37#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.
38 private DateTime lastRequestedChunks = DateTime.Now;
39
40 public void Run()
41 {
42 WindowOptions options = WindowOptions.Default with
43 {
44 Size = new Vector2D<int>(800, 600),
45 Title = "My first Silk.NET application!",
46 };
47
48 _window = Silk.NET.Windowing.Window.Create(options);
49 _window.Load += OnLoad;
50 _window.Update += OnUpdate;
51 _window.Render += OnRender;
52 _window.Run();
53 }
54
55 private unsafe void OnLoad()
56 {
57 _input = _window.CreateInput();
58 for (int i = 0; i < _input.Keyboards.Count; i++)
59 _input.Keyboards[i].KeyDown += KeyDown;
60
61 _gl = _window.CreateOpenGL();
62 _gl.ClearColor(Color.FromArgb(1, 121, 184));
63 _gl.Viewport(0, 0, (uint)_window.Size.X, (uint)_window.Size.Y);
64
65 if (Environment.GetCommandLineArgs().Length != 2)
66 {
67 Console.WriteLine("No name provided");
68 return;
69 }
70
71 clientState = new(_gl, _window)
72 {
73 PlayerName = Environment.GetCommandLineArgs()[1],
74 World = new(),
75 Entities = ArchWorld.Create()
76 };
77
78 for (int i = 0; i < _input.Keyboards.Count; i++)
79 {
80 _input.Mice[i].Cursor.CursorMode = CursorMode.Raw;
81 _input.Mice[i].MouseMove += clientState.Camera.OnMouseMove;
82 }
83
84 VertexShader vs = new(_gl, "./shaders/simple.vs");
85 FragmentShader fs = new(_gl, "./shaders/simple.fs");
86
87 _shaderProgram = new(_gl, vs, fs);
88
89 quad = new QuadMesh(clientState, _shaderProgram);
90
91 _gl.Enable(EnableCap.Blend);
92 _gl.Enable(EnableCap.DepthTest);
93 _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
94 // _gl.PolygonMode(GLEnum.FrontAndBack, GLEnum.Line);
95 // clientState.Window.GL.Enable(GLEnum.CullFace);
96 // clientState.Window.GL.CullFace(GLEnum.Back);
97
98 // Tell GL which triangle winding is considered "front"
99 // clientState.Window.GL.FrontFace(GLEnum.Ccw);
100
101 // Networking
102
103 ComponentRegistry.RegisterComponents();
104
105 EventBasedNetListener listener = new();
106 networkClient = new(listener);
107 networkClient.Start();
108 networkClient.Connect("localhost" /* host IP or name */, 9050 /* port */, "SomeConnectionKey" /* text key or NetDataWriter */);
109 listener.NetworkReceiveEvent += (server, dataReader, deliveryMethod, channel) =>
110 {
111 var packetId = (Packet)dataReader.GetByte();
112 Console.WriteLine("Recieved network packet: {0}", packetId.ToString());
113 switch (packetId)
114 {
115 case Packet.S2CPlayerLoginSuccess:
116 {
117 var packet = new S2CPlayerLoginSuccess();
118 packet.Deserialize(dataReader);
119
120 Console.WriteLine("Found {0} Entities", packet.EntityCount);
121
122 bool foundPlayer = false;
123 foreach (var entity in packet.Entities)
124 {
125 Console.WriteLine("Parsing Entity");
126 ArchEntity archEntity = clientState.Entities.Create(new ServerId(entity.Key));
127
128 // entity.Key is the server ID so we add it to the dictionary
129 clientState.ServerIdToEntity.TryAdd(entity.Key, archEntity);
130
131 foreach (var component in entity.Value)
132 {
133 Console.WriteLine("Component Type: {0} {1}", component.PacketId, component.GetType());
134 if (component is Player player && player.Name == clientState.PlayerName)
135 {
136 clientState.Player = archEntity;
137 foundPlayer = true;
138 }
139 switch (component)
140 {
141 case Player p: clientState.Entities.Add(archEntity, p); break;
142 case Location l: clientState.Entities.Add(archEntity, l); break;
143 case Nametag n: clientState.Entities.Add(archEntity, n); break;
144 case Velocity v: clientState.Entities.Add(archEntity, v); break;
145 default:
146 Console.WriteLine($"Unknown component {component.GetType().Name}");
147 break;
148 }
149 }
150 }
151
152 if (!foundPlayer)
153 {
154 Console.WriteLine("No player was sent from the server, exiting.");
155 Environment.Exit(0);
156 return;
157 }
158
159 Console.WriteLine("Player has {0} components.", clientState.Entities.GetAllComponents(clientState.Player).Length);
160
161 clientState.status = ClientStatus.Connected;
162
163 Location position = clientState.Player.Get<Location>();
164
165 clientState.World.WorldToChunk((int)position.X, (int)position.Y, (int)position.Z, out var chunkPos, out _);
166 position.LastFrameChunkPos = chunkPos;
167
168 foreach (var (x, y, z) in LocationUtil.CoordsNearestFirst(clientState.RenderDistance, chunkPos.X, chunkPos.Y, chunkPos.Z))
169 {
170 clientState.RequestChunk(new(x, y, z));
171 }
172 }
173 break;
174 case Packet.S2CBroadcastEntitySpawn:
175 {
176 var packet = new S2CBroadcastEntitySpawn();
177 packet.Deserialize(dataReader);
178
179 var playerServerId = clientState.Player.Get<ServerId>().Id;
180 if (packet.ServerId == playerServerId)
181 {
182 Console.WriteLine("Ignoring player spawn");
183 return;
184 }
185
186 if (!clientState.ServerIdToEntity.ContainsKey(packet.ServerId))
187 {
188 Console.WriteLine("adding entity");
189 ArchEntity archEntity = clientState.Entities.Create(new ServerId(packet.ServerId));
190
191 // entity.Key is the server ID so we add it to the dictionary
192 clientState.ServerIdToEntity.TryAdd(packet.ServerId, archEntity);
193
194 foreach (var component in packet.Components)
195 {
196 // Should be unreachable but you never know
197 if (component is Player player && player.Name == clientState.PlayerName)
198 {
199 clientState.Player = archEntity;
200 }
201
202 switch (component)
203 {
204 case Location location: clientState.Entities.Add(archEntity, location); break;
205 case Nametag nametag: clientState.Entities.Add(archEntity, nametag); break;
206 case Player playerC: clientState.Entities.Add(archEntity, playerC); break;
207 case Velocity velocity: clientState.Entities.Add(archEntity, velocity); break;
208 case Physics physics: clientState.Entities.Add(archEntity, physics); break;
209 case Collider collider: clientState.Entities.Add(archEntity, collider); break;
210 }
211 }
212 }
213 else
214 {
215 Console.WriteLine("Entity already exists");
216 // If the entity already exists we just ignore it for now we might want to change this later
217 // to check for new components etc etc
218 }
219 }
220 break;
221 case Packet.S2CBroadcastEntityMove:
222 {
223 var packet = new S2CBroadcastEntityMove();
224 packet.Deserialize(dataReader);
225
226 // Don't update the position of the player, might want to change this later but yeah
227 var playerServerId = clientState.Player.Get<ServerId>().Id;
228 if (packet.ServerId == playerServerId)
229 {
230 return;
231 }
232
233 ArchEntity entity = clientState.ServerIdToEntity[packet.ServerId];
234 entity.Get<Location>().Position = new Vector3(packet.Position.X, packet.Position.Y, packet.Position.Z);
235 }
236 break;
237 case Packet.S2CChunkResponse:
238 {
239 var packet = new S2CChunkResponse();
240 packet.Deserialize(dataReader);
241
242 foreach (var packetChunk in packet.Chunks)
243 {
244 var chunk = new World.Chunk(clientState.World, packetChunk.ChunkX, packetChunk.ChunkY, packetChunk.ChunkZ) { Blocks = packetChunk.Blocks, IsEmpty = packetChunk.IsEmpty };
245 clientState.World.SetChunk(packetChunk.ChunkX, packetChunk.ChunkY, packetChunk.ChunkZ, chunk);
246
247 var key = new Vector3I(packetChunk.ChunkX, packetChunk.ChunkY, packetChunk.ChunkZ);
248 clientState.ChunkMeshes.Remove(key, out var _);
249
250 var mesh = new ChunkMesh(clientState, chunk);
251 clientState.ChunkMeshManager.QueueGeneration(mesh);
252 clientState.ChunkMeshes.TryAdd(key, mesh);
253
254 if (clientState.ChunkMeshes.TryGetValue(new Vector3I(packetChunk.ChunkX, packetChunk.ChunkY + 1, packetChunk.ChunkZ), out var topMesh)) clientState.ChunkMeshManager.QueueGeneration(topMesh);
255 if (clientState.ChunkMeshes.TryGetValue(new Vector3I(packetChunk.ChunkX, packetChunk.ChunkY - 1, packetChunk.ChunkZ), out var bottomMesh)) clientState.ChunkMeshManager.QueueGeneration(bottomMesh);
256 if (clientState.ChunkMeshes.TryGetValue(new Vector3I(packetChunk.ChunkX, packetChunk.ChunkY, packetChunk.ChunkZ + 1), out var northMesh)) clientState.ChunkMeshManager.QueueGeneration(northMesh);
257 if (clientState.ChunkMeshes.TryGetValue(new Vector3I(packetChunk.ChunkX, packetChunk.ChunkY, packetChunk.ChunkZ - 1), out var southMesh)) clientState.ChunkMeshManager.QueueGeneration(southMesh);
258 if (clientState.ChunkMeshes.TryGetValue(new Vector3I(packetChunk.ChunkX - 1, packetChunk.ChunkY, packetChunk.ChunkZ), out var westMesh)) clientState.ChunkMeshManager.QueueGeneration(westMesh);
259 if (clientState.ChunkMeshes.TryGetValue(new Vector3I(packetChunk.ChunkX + 1, packetChunk.ChunkY, packetChunk.ChunkZ), out var eastMesh)) clientState.ChunkMeshManager.QueueGeneration(eastMesh);
260 }
261 }
262 break;
263 }
264
265 dataReader.Recycle();
266 };
267
268 listener.PeerConnectedEvent += peer =>
269 {
270 Console.WriteLine("Connected to server!");
271 clientState.NetServer = peer;
272
273 C2SPlayerLoginRequest loginRequest = new(clientState.PlayerName);
274
275 clientState.NetServer.Send(IPacket.Serialize(loginRequest), DeliveryMethod.ReliableOrdered);
276 };
277 }
278
279 private void OnUpdate(double deltaTime)
280 {
281 clientState.Profiler.Tick += 1;
282
283 networkClient.PollEvents();
284
285 if (clientState.status != ClientStatus.Connected)
286 return;
287
288 var _keyboard = _input.Keyboards[0];
289 float cameraSpeed = 150.0f * (float)deltaTime;
290
291 clientState.Entities.Query(new Arch.Core.QueryDescription().WithAll<Location, Player>(), (ArchEntity entity, ref Location location, ref Player player) =>
292 {
293 if (player.Name != clientState.PlayerName)
294 return;
295
296 bool playerMoved = false;
297
298 var actualCameraSpeed = cameraSpeed;
299 if (_keyboard.IsKeyPressed(Key.ShiftLeft))
300 actualCameraSpeed *= 0.2f;
301
302 if (_keyboard.IsKeyPressed(Key.W))
303 {
304 playerMoved = true;
305 location.Position += actualCameraSpeed * clientState.Camera.front.ToVector3();
306 }
307 if (_keyboard.IsKeyPressed(Key.S))
308 {
309 playerMoved = true;
310 location.Position -= actualCameraSpeed * clientState.Camera.front.ToVector3();
311 }
312 if (_keyboard.IsKeyPressed(Key.A))
313 {
314 playerMoved = true;
315
316 location.Position -= glm.Normalized(glm.Cross(clientState.Camera.front, clientState.Camera.up)).ToVector3() * actualCameraSpeed;
317 }
318 if (_keyboard.IsKeyPressed(Key.D))
319 {
320 location.Position += glm.Normalized(glm.Cross(clientState.Camera.front, clientState.Camera.up)).ToVector3() * actualCameraSpeed;
321 playerMoved = true;
322 }
323
324 clientState.World.WorldToChunk((int)location.X, (int)location.Y, (int)location.Z, out var chunkPos, out _);
325 bool hasMovedBetweenChunks = !location.LastFrameChunkPos.Equals(chunkPos);
326
327 if (playerMoved && clientState.NetServer != null && hasMovedBetweenChunks)
328 {
329 clientState.Profiler.Start("Requested chunks distance culling", () =>
330 {
331 List<Vector3I> _requestedChunksCache = [.. clientState.RequestedChunksQueue];
332 foreach (var targetChunk in _requestedChunksCache)
333 {
334 bool condition = LocationUtil.Distance(chunkPos.ToVector3(), targetChunk.ToVector3()) > clientState.RenderDistance * 1.5;
335 if (condition)
336 {
337 clientState.RequestedChunks.Remove(targetChunk);
338 clientState.RequestedChunksQueue.Remove(targetChunk);
339 }
340 }
341 });
342
343
344 // List<KeyValuePair<Vector3I, ChunkMesh>> _chunkMeshes = clientState.ChunkMeshes.ToList();
345 // foreach (var targetChunk in _chunkMeshes)
346 // {
347 // bool condition = LocationUtil.Distance(chunkPos.ToVector3(), targetChunk.Key.ToVector3()) > clientState.RenderDistance * 1.5;
348 // if (condition)
349 // {
350 // clientState.ChunkMeshes.Remove(targetChunk.Key);
351 // }
352 // }
353
354 location.LastFrameChunkPos = chunkPos;
355
356 foreach (var (x, y, z) in LocationUtil.CoordsNearestFirst(clientState.RenderDistance, chunkPos.X, chunkPos.Y, chunkPos.Z))
357 {
358 clientState.RequestChunk(new(x, y, z));
359 }
360
361 Console.WriteLine("Sending player move packet");
362 clientState.NetServer.Send(IPacket.Serialize(new C2SPlayerMove(location.Position)), DeliveryMethod.ReliableUnordered);
363 }
364 });
365
366 clientState.Profiler.Start("Request chunks from queue", () =>
367 {
368 TimeSpan elapsed = DateTime.Now - lastRequestedChunks;
369 if (elapsed.TotalMilliseconds > 500)
370 {
371 lastRequestedChunks = DateTime.Now;
372 clientState.RequestChunksFromQueue();
373 }
374 });
375
376 clientState.Profiler.Start("Generate chunks meshes under limit", () =>
377 {
378 clientState.ChunkMeshManager.GenerateFromQueueUnderTimeLimit(8);
379 });
380 }
381
382 private unsafe void OnRender(double deltaTime)
383 {
384 _gl.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
385
386 if (clientState.status != ClientStatus.Connected)
387 return;
388
389 clientState.Profiler.Start("Rendering", () =>
390 {
391 quad.Draw();
392 });
393 }
394
395 private void KeyDown(IKeyboard keyboard, Key key, int keyCode)
396 {
397 if (key == Key.Escape)
398 {
399 networkClient.Stop();
400 _window.Close();
401 clientState.Profiler.Build();
402 }
403
404 if (key == Key.F11)
405 _window.WindowState = WindowState.Maximized;
406 }
407}