imperfect-space/scripts/GameManager.cs
2026-01-29 09:43:15 -05:00

397 lines
8.8 KiB
C#

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<Player> Players { get; private set; } = [];
private double closeTickTimer = 0;
private double farTickTimer = 0;
public bool simulating = false;
public bool playerReady = false;
private readonly Dictionary<GameObject, Node3D> spawnedObjects = [];
private readonly ConcurrentQueue<GameObject> 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<Sector> neighbours = GetCurrentSector().GetNeighbouringSectors();
neighbours.ForEach(sector =>
{
Task.Run(() =>
{
sector.Simulate(delta);
});
});
}
private void SimulateFar(double delta)
{
simulating = true;
List<Task> 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<Sector> 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<Sector> nearby = current.GetNeighbouringSectors();
foreach (KeyValuePair<GameObject, Node3D> 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<Sector> 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);
}
}