using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Godot; public partial class GameManager : Node3D { [Export] public Control GameMenu { get; private set; } [Export] public QueueManager QueueManager { get; private set; } [Export] public Node3D SpaceRoot { get; private set; } [Export] public double closeTickInterval = 1; [Export] public double farTickInterval = 10; [Export] public int maxFarThreads = 16; public static bool Loading { get; private set; } = true; public static GameManager Singleton { get; private set; } public static IGenerator Generator { get; private set; } public static Universe GameUniverse { get; private set; } public Player MainPlayer { get; private set; } public List Players { get; private set; } = []; private double closeTickTimer = 0; private double farTickTimer = 0; public bool simulating = false; public bool playerReady = false; private readonly Dictionary spawnedObjects = []; private readonly ConcurrentQueue spawnQueue = []; public override void _Ready() { Singleton = this; Generator = new TestGenerator(); if (Global.IsGameHost) { GameUniverse = Generator.GenerateUniverse(); Loading = false; } } private void OnPlayerReady() { playerReady = true; SpawnClose(); } public override void _ExitTree() { Loading = true; Singleton = null; } public override void _Process(double delta) { if (!playerReady || !Global.IsGameHost) { return; } closeTickTimer += delta; farTickTimer += delta; if (closeTickTimer >= closeTickInterval) { SimulateClose(closeTickTimer); closeTickTimer = 0; } if (simulating) { farTickTimer = 0; } if (farTickTimer >= farTickInterval) { double taskFarTickTimer = farTickTimer; Task.Run(() => { SimulateFar(taskFarTickTimer); }); farTickTimer = 0; } } public Sector GetCurrentSector() { return MainPlayer.PlayerData.CurrentSector; } private void SimulateClose(double delta) { List neighbours = GetCurrentSector().GetNeighbouringSectors(); neighbours.ForEach(sector => { Task.Run(() => { sector.Simulate(delta); }); }); } private void SimulateFar(double delta) { simulating = true; List tasks = []; Sector[,,] sectors = GameUniverse.Sectors; int sizeX = sectors.GetLength(0); int countX = Mathf.Clamp(sizeX / maxFarThreads, 1, int.MaxValue); for (int x = 0; x < sizeX; x += countX) { double taskDelta = delta; int taskStartX = x; int taskCountX = countX; Task clusteredTask = Task.Run(() => { SimulateFarClustered(taskDelta, taskStartX, taskCountX); }); tasks.Add(clusteredTask); } Task.WaitAll([.. tasks]); simulating = false; } private void SimulateFarClustered(double delta, int startX, int countX) { Vector3I currentCoordinates = GetCurrentSector().Coordinates; Sector[,,] sectors = GameUniverse.Sectors; int sizeX = sectors.GetLength(0); int sizeY = sectors.GetLength(1); int sizeZ = sectors.GetLength(2); int startSkipX = Mathf.Clamp(currentCoordinates.X - 1, 0, sizeX); int startSkipY = Mathf.Clamp(currentCoordinates.Y - 1, 0, sizeY); int startSkipZ = Mathf.Clamp(currentCoordinates.Z - 1, 0, sizeZ); int endSkipX = Mathf.Clamp(currentCoordinates.X + 1, 0, sizeX); int endSkipY = Mathf.Clamp(currentCoordinates.Y + 1, 0, sizeY); int endSkipZ = Mathf.Clamp(currentCoordinates.Z + 1, 0, sizeZ); int endX = Mathf.Clamp(startX + countX, 0, sizeX); for (int x = startX; x < endX; x++) { for (int y = 0; y < sizeY; y++) { Thread.Sleep(5); for (int z = 0; z < sizeZ; z++) { if (Helpers.IsBetweenInclusive(x, startSkipX, endSkipX)) { if (Helpers.IsBetweenInclusive(y, startSkipY, endSkipY)) { if (Helpers.IsBetweenInclusive(z, startSkipZ, endSkipZ)) { continue; } } } try { sectors[x, y, z].Simulate(delta); } catch (Exception e) { QueueManager.LogQueue.Enqueue("EXCEPTION: " + e.Message); } } } } } private void SpawnClose() { List neighbours = GetCurrentSector().GetNeighbouringSectors(); neighbours.ForEach(sector => sector.SpawnObjects()); } public Player SpawnPlayer(Character character, bool isMainPlayer = false) { Player player = character.InstantiatePlayer(); player.GameMenu = GameMenu; if (isMainPlayer) { MainPlayer = player; OnPlayerReady(); } Players.Add(player); character.UpdateSector(); SpaceRoot.CallDeferred("add_child", player); return player; } public void DespawnPlayer(Player player) { SpaceRoot.CallDeferred("remove_child", player); player.CallDeferred("queue_free"); } public void Spawn(GameObject gameObject) { spawnQueue.Enqueue(gameObject); CallDeferred(nameof(ProcessSpawnQueue)); } private void ProcessSpawnQueue() { spawnQueue.TryDequeue(out GameObject gameObject); if (gameObject == null) { return; } if (spawnedObjects.ContainsKey(gameObject)) { return; } Node3D instance = gameObject.Instantiate(GetCurrentSector()); spawnedObjects.Add(gameObject, instance); SpaceRoot.CallDeferred("add_child", instance); } public void Despawn(GameObject gameObject) { if (!spawnedObjects.ContainsKey(gameObject)) { return; } Node3D nodeToDespawn = spawnedObjects.GetValueOrDefault(gameObject); spawnedObjects.Remove(gameObject); SpaceRoot.CallDeferred("remove_child", nodeToDespawn); nodeToDespawn.CallDeferred("queue_free"); } public void ApplyOrigin() { Sector current = GetCurrentSector(); foreach (Player player in Players) { player.PlayerData.UpdateSectorOffset(current); player.PlayerData.UpdateNodePosition(); } List nearby = current.GetNeighbouringSectors(); foreach (KeyValuePair spawned in spawnedObjects) { GameObject gameObject = spawned.Key; Node3D node = spawned.Value; if (!nearby.Contains(gameObject.CurrentSector)) { Despawn(gameObject); } else { gameObject.UpdateSectorOffset(current); node.GlobalPosition = gameObject.LocalCoordinates + gameObject.SectorOffset; } } nearby.ForEach(sector => sector.SpawnObjects()); } // These should be in RPCNode and should be queued public void SendUniverseToClient(long id) { RpcId(id, nameof(RpcDownloadUniverse), Generator.GetUniverseSize(), Generator.GetSectorSize()); } // These should be in RPCNode and should be queued [Rpc(MultiplayerApi.RpcMode.Authority)] public void RpcDownloadUniverse(Vector3I universeSize, Vector3 sectorSize) { GameUniverse = Generator.InitializeEmptyUniverse(universeSize, sectorSize); Loading = false; } // These should be in RPCNode and should be queued [Rpc(MultiplayerApi.RpcMode.AnyPeer)] public void RpcSendNearbySectors(Vector3I coordinates, long id = -1) { Sector sector = GameUniverse.GetSector(coordinates); if (sector == null) { return; } List sectors = sector.GetNeighbouringSectors(); sectors.ForEach(sector => SendSectorToClients(sector, id == -1 ? null : id)); } // These should be in RPCNode and should be queued public void SendSectorToClients(Sector sector, long? id = null) { foreach (GameObject gameObject in sector.GameObjects) { if (gameObject is Character) { continue; } Godot.Collections.Dictionary gameObjectData = new() { { "type", gameObject.GetType().Name }, { "sectorCoordinates", sector.Coordinates }, { "coordinates", gameObject.LocalCoordinates }, { "uuid", gameObject.UUID.ToString() }, }; if (id.HasValue) { RpcId(id.Value, nameof(RpcDownloadGameObject), gameObjectData); } else { Rpc(nameof(RpcDownloadGameObject), gameObjectData); } } } // These should be in RPCNode and should be queued [Rpc(MultiplayerApi.RpcMode.Authority)] public void RpcDownloadGameObject(Godot.Collections.Dictionary gameObjectData) { if (!gameObjectData.TryGetValue("type", out var typeData)) { return; } if (!gameObjectData.TryGetValue("sectorCoordinates", out var sectorCoordinatesData)) { return; } if (!gameObjectData.TryGetValue("coordinates", out var coordinatesData)) { return; } if (!gameObjectData.TryGetValue("uuid", out var uuidData)) { return; } string type = (string)typeData; Vector3I sectorCoordinates = (Vector3I)sectorCoordinatesData; Vector3 coordinates = (Vector3)coordinatesData; Guid uuid = Guid.Parse((string)uuidData); Sector sector = GameUniverse.GetSector(sectorCoordinates); if (sector == null) { return; } GameObject gameObject; switch (type) { case "Star": gameObject = new Star(sector, coordinates); break; default: return; } gameObject.UUID = uuid; Spawn(gameObject); sector.AssignObject(gameObject); } }