working on multiplayer

This commit is contained in:
2025-06-24 14:45:45 +02:00
parent b1050f627b
commit 5a813f212c
67 changed files with 499 additions and 127 deletions

View File

@@ -29,7 +29,7 @@ namespace RebootKit.Engine.Services.Console {
public string description;
public Action<string[]> action;
}
readonly List<ConsoleCommand> m_ConsoleCommands = new List<ConsoleCommand>();
FileStream m_LogFileStream;
@@ -39,10 +39,16 @@ namespace RebootKit.Engine.Services.Console {
public ConsoleService() {
ConfigVar.StateChanged += OnCVarStateChanged;
m_LogFileStream = new FileStream(Application.persistentDataPath + "/rr_logs.txt", FileMode.Append, FileAccess.Write);
#if UNITY_EDITOR
string logFilePath = Application.persistentDataPath + "/rr_logs_editor.txt";
#else
string logFilePath = Application.persistentDataPath + "/rr_logs.txt";
#endif
m_LogFileStream = new FileStream(logFilePath, FileMode.Append, FileAccess.Write);
m_LogFileWriter = new StreamWriter(m_LogFileStream);
m_LogFileWriter.WriteLine("============================");
m_LogFileWriter.WriteLine("Starting new log");
m_LogFileWriter.WriteLine($" > Game: {Application.productName}");
@@ -52,7 +58,7 @@ namespace RebootKit.Engine.Services.Console {
m_LogFileWriter.Flush();
s_logger.Info("Waking up");
Load();
RegisterCommands();
@@ -146,12 +152,16 @@ namespace RebootKit.Engine.Services.Console {
public static ConsoleCommand[] GenerateCommandsToRegister() {
IEnumerable<MethodInfo> methods = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(assembly => assembly.GetTypes())
.SelectMany(type => type.GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static))
.Where(method => method.GetCustomAttributes(typeof(RCCMD), false).Length > 0);
.SelectMany(type => type.GetMethods(BindingFlags.NonPublic |
BindingFlags.Public |
BindingFlags.Static))
.Where(method => method.GetCustomAttributes(typeof(RCCMD), false)
.Length >
0);
List<ConsoleCommand> commands = new List<ConsoleCommand>();
foreach (MethodInfo method in methods) {
RCCMD attribute = (RCCMD)method.GetCustomAttributes(typeof(RCCMD), false)[0];
RCCMD attribute = (RCCMD) method.GetCustomAttributes(typeof(RCCMD), false)[0];
if (!method.IsStatic) {
s_logger.Error($"Command `{attribute.name}` is not static, skipping");
@@ -162,13 +172,13 @@ namespace RebootKit.Engine.Services.Console {
s_logger.Error($"Command `{attribute.name}` has invalid number of parameters, skipping");
continue;
}
if (method.GetParameters()[0].ParameterType != typeof(string[])) {
s_logger.Error($"Command `{attribute.name}` has invalid parameter type, skipping");
continue;
}
Action<string[]> action = (Action<string[]>)Delegate.CreateDelegate(typeof(Action<string[]>), method);
Action<string[]> action = (Action<string[]>) Delegate.CreateDelegate(typeof(Action<string[]>), method);
commands.Add(new ConsoleCommand {
name = attribute.name,
@@ -176,17 +186,17 @@ namespace RebootKit.Engine.Services.Console {
action = action
});
}
return commands.ToArray();
}
public void RegisterCommands() {
ConsoleCommand[] commands = GenerateCommandsToRegister();
foreach (ConsoleCommand command in commands) {
RegisterCommand(command.name, command.description, command.action);
}
}
bool IsCommandRegistered(string name) {
foreach (ConsoleCommand command in m_ConsoleCommands) {
if (command.name.Equals(name)) {
@@ -221,7 +231,7 @@ namespace RebootKit.Engine.Services.Console {
RR.Console.WriteToOutput(message.ToString());
}
[RCCMD("cvars", "Prints all cvars")]
public static void PrintCVars(string[] args) {
StringBuilder message = new StringBuilder();
@@ -235,7 +245,7 @@ namespace RebootKit.Engine.Services.Console {
void Save() {
string path = Application.persistentDataPath + "/" + RConsts.k_CVarsFilename;
s_logger.Info("Saving cvars to file: " + path);
StringBuilder sb = new StringBuilder();

View File

@@ -1,13 +1,13 @@
using RebootKit.Engine.Foundation;
namespace RebootKit.Engine.Services.Crosshair {
public class CrosshairService : IService {
public CrosshairService() {
}
public void Dispose() {
}
}
using RebootKit.Engine.Foundation;
namespace RebootKit.Engine.Services.Crosshair {
public class CrosshairService : IService {
public CrosshairService() {
}
public void Dispose() {
}
}
}

View File

@@ -1,50 +1,50 @@
using System;
using RebootKit.Engine.Foundation;
using RebootKit.Engine.Services.Console;
using UnityEngine;
using UnityEngine.InputSystem;
namespace RebootKit.Engine.Services.Development {
static class DebugConfig {
[ConfigVar("debug.overlay", 0, "Controls overlay visibility. 0 - hidden, 1 - visible")] public static ConfigVar s_OverlayMode;
}
public class DevToolsService : ServiceMonoBehaviour {
[SerializeField] DebugOverlayView m_DebugOverlayView;
IDisposable m_CVarChangedListener;
void Start() {
ConfigVar.StateChanged += OnCVarChanged;
OnCVarChanged(DebugConfig.s_OverlayMode);
}
void OnDisable() {
Dispose();
}
public override void Dispose() {
ConfigVar.StateChanged -= OnCVarChanged;
}
void Update() {
if (InputSystem.GetDevice<Keyboard>().f3Key.wasReleasedThisFrame) {
DebugConfig.s_OverlayMode.Set(DebugConfig.s_OverlayMode.IndexValue == 1 ? 0 : 1);
}
}
void OnOverlayModeChanged(int mode) {
if (mode == 1) {
m_DebugOverlayView.gameObject.SetActive(true);
} else {
m_DebugOverlayView.gameObject.SetActive(false);
}
}
void OnCVarChanged(ConfigVar cvar) {
if (cvar == DebugConfig.s_OverlayMode) {
OnOverlayModeChanged(cvar.IndexValue);
}
}
}
using System;
using RebootKit.Engine.Foundation;
using RebootKit.Engine.Services.Console;
using UnityEngine;
using UnityEngine.InputSystem;
namespace RebootKit.Engine.Services.Development {
static class DebugConfig {
[ConfigVar("debug.overlay", 0, "Controls overlay visibility. 0 - hidden, 1 - visible")] public static ConfigVar s_OverlayMode;
}
public class DevToolsService : ServiceMonoBehaviour {
[SerializeField] DebugOverlayView m_DebugOverlayView;
IDisposable m_CVarChangedListener;
void Start() {
ConfigVar.StateChanged += OnCVarChanged;
OnCVarChanged(DebugConfig.s_OverlayMode);
}
void OnDisable() {
Dispose();
}
public override void Dispose() {
ConfigVar.StateChanged -= OnCVarChanged;
}
void Update() {
if (InputSystem.GetDevice<Keyboard>().f3Key.wasReleasedThisFrame) {
DebugConfig.s_OverlayMode.Set(DebugConfig.s_OverlayMode.IndexValue == 1 ? 0 : 1);
}
}
void OnOverlayModeChanged(int mode) {
if (mode == 1) {
m_DebugOverlayView.gameObject.SetActive(true);
} else {
m_DebugOverlayView.gameObject.SetActive(false);
}
}
void OnCVarChanged(ConfigVar cvar) {
if (cvar == DebugConfig.s_OverlayMode) {
OnOverlayModeChanged(cvar.IndexValue);
}
}
}
}

View File

@@ -9,5 +9,8 @@ namespace RebootKit.Engine {
public EngineCoreServicesAsset coreServices;
public GameAsset gameAsset;
// @NOTE: Spacewar, change as needed
public uint steamAppID = 480;
}
}

View File

@@ -1,18 +1,18 @@
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using RebootKit.Engine.Foundation;
namespace RebootKit.Engine.Services.GameMode {
public interface IGameMode : IDisposable {
UniTask OnInit(CancellationToken cancellationToken);
void OnStart();
void OnStop();
void OnTick();
}
public abstract class GameModeAsset : FactoryAsset<IGameMode> {
}
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using RebootKit.Engine.Foundation;
namespace RebootKit.Engine.Services.GameMode {
public interface IGameMode : IDisposable {
UniTask OnInit(CancellationToken cancellationToken);
void OnStart();
void OnStop();
void OnTick();
}
public abstract class GameModeAsset : FactoryAsset<IGameMode> {
}
}

View File

@@ -4,19 +4,18 @@ using System.Threading;
using Cysharp.Threading.Tasks;
using R3;
using RebootKit.Engine.Foundation;
using RebootKit.Engine.Multiplayer;
using RebootKit.Engine.Services.Console;
using RebootKit.Engine.Services.GameMode;
using RebootKit.Engine.Services.Input;
using RebootKit.Engine.Services.Simulation;
using RebootKit.Engine.Steam;
using Unity.Collections;
using UnityEngine;
using UnityEngine.AddressableAssets;
using Assert = UnityEngine.Assertions.Assert;
using Logger = RebootKit.Engine.Foundation.Logger;
// RR
// Game
// GameMode
namespace RebootKit.Engine.Main {
public interface IGame : IDisposable {
UniTask InitAsync(CancellationToken cancellationToken);
@@ -33,56 +32,60 @@ namespace RebootKit.Engine.Main {
[ConfigVar("con.write_log", 1, "Enables writing game log to console output")]
static ConfigVar s_writeLogToConsole;
static EngineConfigAsset s_engineConfigAsset;
internal static EngineConfigAsset EngineConfig;
static DisposableBag s_disposableBag;
static DisposableBag s_servicesBag;
static ConsoleService s_consoleService;
static GameModesService s_gameModesService;
static InputService s_inputService;
static WorldService s_worldService;
public static ConsoleService Console => s_consoleService;
public static InputService Input => s_inputService;
public static WorldService World => s_worldService;
public static GameModesService GameModes => s_gameModesService;
public static ConsoleService Console { get; private set; }
public static InputService Input { get; private set; }
public static WorldService World { get; private set; }
public static GameModesService GameModes { get; private set; }
public static Camera MainCamera { get; internal set; }
static IGame s_game;
public static async UniTask InitAsync(EngineConfigAsset configAsset, CancellationToken cancellationToken) {
// Core
internal static async UniTask InitAsync(EngineConfigAsset configAsset, CancellationToken cancellationToken) {
Assert.IsNotNull(configAsset, "Config asset is required");
Assert.IsNotNull(configAsset.gameAsset, "Game asset is required");
s_engineConfigAsset = configAsset;
EngineConfig = configAsset;
s_Logger.Info("Initializing");
s_servicesBag = new DisposableBag();
s_disposableBag = new DisposableBag();
s_Logger.Debug("Registering core services");
s_consoleService = CreateService(s_engineConfigAsset.coreServices.consoleService);
s_inputService = CreateService(s_engineConfigAsset.coreServices.inputService);
s_worldService = CreateService(s_engineConfigAsset.coreServices.worldService);
s_gameModesService = CreateService<GameModesService>();
Console = CreateService(EngineConfig.coreServices.consoleService);
Input = CreateService(EngineConfig.coreServices.inputService);
World = CreateService(EngineConfig.coreServices.worldService);
GameModes = CreateService<GameModesService>();
await InitializeAssetsAsync(cancellationToken);
await SteamManager.InitializeAsync(cancellationToken);
if (SteamManager.IsInitialized) {
s_networkTransport = SteamManager.NetworkTransport;
}
s_Logger.Debug("Creating game");
s_game = s_engineConfigAsset.gameAsset.CreateGame();
s_game = EngineConfig.gameAsset.CreateGame();
await s_game.InitAsync(cancellationToken);
}
public static void Shutdown() {
internal static void Shutdown() {
SteamManager.Shutdown();
s_Logger.Info("Shutting down");
s_servicesBag.Dispose();
s_disposableBag.Dispose();
}
public static void Run() {
internal static void Run() {
s_game.Run();
#if UNITY_EDITOR
@@ -145,12 +148,13 @@ namespace RebootKit.Engine.Main {
// Game API
public static void StartGameMode(GameModeAsset gameMode, WorldConfig world) {
if (gameMode is null) {
throw new ArgumentNullException(nameof(gameMode));
if (!IsClient() || !IsHost()) {
s_Logger.Error("Cannot start game mode: you must be connected to a server and be the host");
return;
}
s_Logger.Info($"Starting game mode: {gameMode.name} in world: {world.name}");
s_gameModesService.Start(gameMode, world);
GameModes.Start(gameMode, world);
}
public static TGame Game<TGame>() where TGame : IGame {
@@ -182,17 +186,17 @@ namespace RebootKit.Engine.Main {
public static void Log(string message) {
Debug.Log(message);
s_consoleService?.WriteToOutput(message);
Console?.WriteToOutput(message);
}
public static void LogWarning(string message) {
Debug.LogWarning(message);
s_consoleService?.WriteToOutput(message);
Console?.WriteToOutput(message);
}
public static void LogError(string message) {
Debug.LogError(message);
s_consoleService?.WriteToOutput(message);
Console?.WriteToOutput(message);
}
// CVar API
@@ -228,5 +232,48 @@ namespace RebootKit.Engine.Main {
ConfigVarsContainer.Register(cvar);
return cvar;
}
// Network API
static GameLobby s_gameLobby;
static INetworkTransport s_networkTransport;
public static bool IsHost() {
return s_networkTransport.IsServer();
}
public static bool IsClient() {
return s_networkTransport.IsClient();
}
public static int GetPing() {
return -1;
}
public static void HostServer(bool offline = false) {
s_networkTransport.StartServer();
}
public static void ConnectToLobby() {
s_networkTransport.Connect(Steamworks.SteamNetworkingSockets.Identity.SteamId);
}
public static void Disconnect() {
s_networkTransport.Disconnect();
}
internal static void OnConnected(GameLobby lobby) {
}
internal static void OnDisconnected() {
}
internal static void OnServerDataReceived(byte[] data) {
s_Logger.Debug($"[SERVER] Data received: {data.Length} bytes");
}
internal static void OnClientDataReceived(byte[] data) {
s_Logger.Debug($"[CLIENT] Data received: {data.Length} bytes");
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 83d5dc53310c40caab8b7732b9cecbdc
timeCreated: 1750615656

View File

@@ -0,0 +1,7 @@
namespace RebootKit.Engine.Multiplayer {
public class GameLobby {
public string GameModeID { get; private set; }
public string WorldID { get; private set; }
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: cf39e9785e4e4ad0a7ebb8b19c882b2a
timeCreated: 1750628631

View File

@@ -0,0 +1,24 @@
using System;
namespace RebootKit.Engine.Multiplayer {
public enum SendMode {
Reliable,
Unreliable
}
public interface INetworkTransport {
void Initialize();
void Shutdown();
bool IsServer();
bool IsClient();
bool StartServer();
void StopServer();
bool Connect(ulong serverID);
void Disconnect();
void Send(ulong clientID, ArraySegment<byte> data, SendMode mode);
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 702059b29fda4edc80caf8b22cd8c0d7
timeCreated: 1750759626

View File

@@ -0,0 +1,4 @@
namespace RebootKit.Engine.Multiplayer {
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 34d66ac5d1c443e8992a84edd5eb796e
timeCreated: 1750628495

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: ac31ad3431a84354ab7c73a84039be3f
timeCreated: 1741791385

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b72bb6331ad3466d9c3da0c63c300d1a
timeCreated: 1750601845

View File

@@ -0,0 +1,50 @@
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using R3;
using RebootKit.Engine.Main;
using RebootKit.Engine.Multiplayer;
using Steamworks;
using Logger = RebootKit.Engine.Foundation.Logger;
namespace RebootKit.Engine.Steam {
static class SteamManager {
static readonly Logger s_Logger = new Logger(nameof(SteamManager));
public static bool IsInitialized { get; private set; } = false;
public static INetworkTransport NetworkTransport { get; private set; } = new SteamNetworkTransport();
internal static async UniTask InitializeAsync(CancellationToken cancellationToken = default) {
s_Logger.Info("Initializing Steam Manager...");
IsInitialized = false;
try {
SteamClient.Init(RR.EngineConfig.steamAppID, true);
} catch (Exception ex) {
s_Logger.Error($"Failed to initialize Steam Client: {ex.Message}");
return;
}
NetworkTransport.Initialize();
IsInitialized = true;
await UniTask.Yield(cancellationToken);
}
internal static void Shutdown() {
if (!IsInitialized) {
s_Logger.Error("Steam Manager is not initialized. Skipping operation.");
return;
}
s_Logger.Info("Shutting down Steam Manager...");
NetworkTransport.Shutdown();
SteamClient.Shutdown();
IsInitialized = false;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 692a7bd3511b4e79b29944f1439556a0
timeCreated: 1750601849

View File

@@ -0,0 +1,209 @@
using System;
using R3;
using RebootKit.Engine.Foundation;
using RebootKit.Engine.Main;
using RebootKit.Engine.Multiplayer;
using Steamworks;
using Steamworks.Data;
using Logger = RebootKit.Engine.Foundation.Logger;
namespace RebootKit.Engine.Steam {
class SteamNetworkTransport : INetworkTransport {
static readonly Logger s_Logger = new Logger(nameof(SteamNetworkTransport));
const int k_DefaultPort = 420;
ServerCallbacks m_SocketManager;
ClientCallbacks m_ConnectionManager;
IDisposable m_TickDisposable;
SteamId m_HostSteamID;
public void Initialize() {
m_TickDisposable = Observable.EveryUpdate()
.Subscribe(_ => Tick());
SteamNetworkingUtils.DebugLevel = NetDebugOutput.Debug;
SteamNetworkingSockets.OnConnectionStatusChanged += OnConnectionStatusChanged;
SteamNetworkingUtils.OnDebugOutput += OnSteamNetworkDebugOutput;
SteamNetworkingUtils.InitRelayNetworkAccess();
}
public void Shutdown() {
Disconnect();
SteamNetworkingUtils.OnDebugOutput -= OnSteamNetworkDebugOutput;
SteamNetworkingSockets.OnConnectionStatusChanged -= OnConnectionStatusChanged;
m_TickDisposable.Dispose();
}
public bool IsServer() {
return m_SocketManager != null;
}
public bool IsClient() {
return m_ConnectionManager != null;
}
public bool StartServer() {
Disconnect();
s_Logger.Info("Creating server...");
try {
m_HostSteamID = SteamNetworkingSockets.Identity.SteamId;
m_SocketManager = SteamNetworkingSockets.CreateRelaySocket<ServerCallbacks>(k_DefaultPort);
m_ConnectionManager = SteamNetworkingSockets.ConnectRelay<ClientCallbacks>(m_HostSteamID, k_DefaultPort);
} catch (Exception e) {
s_Logger.Error($"Failed to create server: {e.Message}");
m_SocketManager = null;
m_ConnectionManager = null;
return false;
}
return true;
}
public void StopServer() {
Disconnect();
}
public bool Connect(ulong serverID) {
if (IsServer()) {
s_Logger.Error("Cannot connect to a server while running as a server.");
return false;
}
Disconnect();
s_Logger.Info("Connecting to server with steam ID: " + serverID);
try {
m_ConnectionManager = SteamNetworkingSockets.ConnectRelay<ClientCallbacks>(serverID, k_DefaultPort);
m_HostSteamID = serverID;
} catch (Exception e) {
s_Logger.Error($"Failed to connect to server with ID {serverID}: {e.Message}");
m_ConnectionManager = null;
return false;
}
return true;
}
public void Disconnect() {
if (m_ConnectionManager != null) {
s_Logger.Info("Disconnecting from the server...");
m_ConnectionManager.Close();
m_ConnectionManager = null;
}
if (m_SocketManager != null) {
s_Logger.Info("Shutting down the server...");
m_SocketManager.Close();
m_SocketManager = null;
}
}
public void Send(ulong clientID, ArraySegment<byte> data, SendMode mode) {
if (clientID == 0) {
clientID = m_HostSteamID;
}
}
void OnSteamNetworkDebugOutput(NetDebugOutput level, string message) {
LogLevel logLevel = level switch {
NetDebugOutput.Debug => LogLevel.Debug,
NetDebugOutput.Msg => LogLevel.Info,
NetDebugOutput.Warning => LogLevel.Warning,
NetDebugOutput.Error => LogLevel.Error,
_ => LogLevel.Info
};
s_Logger.Log(logLevel, message);
}
void Tick() {
m_SocketManager?.Receive();
m_ConnectionManager?.Receive();
}
void OnConnectionStatusChanged(Connection connection, ConnectionInfo info) {
s_Logger.Info($"OnConnectionStatusChanged: {connection.Id} - {info.Identity} - Status: {info.State}");
}
class ServerCallbacks : SocketManager {
public override void OnConnecting(Connection connection, ConnectionInfo data) {
base.OnConnecting(connection, data);
connection.Accept();
s_Logger.Info($"OnConnecting: {connection.Id} - {data.Identity}");
}
public override void OnConnected(Connection connection, ConnectionInfo data) {
base.OnConnected(connection, data);
s_Logger.Info($"OnConnected: {connection.Id} - {data.Identity}");
connection.SendMessage(new byte[] {
0xBE,
0xFE,
0x00,
0x00
}, 0, 4, 0);
}
public override void OnDisconnected(Connection connection, ConnectionInfo data) {
base.OnDisconnected(connection, data);
s_Logger.Info($"OnDisconnected: {connection.Id} - {data.Identity}");
}
public override void OnMessage(Connection connection,
NetIdentity identity,
IntPtr data,
int size,
long messageNum,
long recvTime,
int channel) {
base.OnMessage(connection, identity, data, size, messageNum, recvTime, channel);
byte[] buffer = new byte[size];
System.Runtime.InteropServices.Marshal.Copy(data, buffer, 0, size);
RR.OnServerDataReceived(buffer);
s_Logger.Info($"OnMessage: {connection.Id} - {identity} - Size: {size} - Channel: {channel}");
}
}
class ClientCallbacks : ConnectionManager {
public override void OnConnected(ConnectionInfo info) {
base.OnConnected(info);
s_Logger.Info("ConnectionOnConnected");
}
public override void OnConnecting(ConnectionInfo info) {
base.OnConnecting(info);
s_Logger.Info("ConnectionOnConnecting");
}
public override void OnDisconnected(ConnectionInfo info) {
base.OnDisconnected(info);
s_Logger.Info("ConnectionOnDisconnected");
}
public override void OnMessage(IntPtr data, int size, long messageNum, long recvTime, int channel) {
byte[] buffer = new byte[size];
System.Runtime.InteropServices.Marshal.Copy(data, buffer, 0, size);
RR.OnClientDataReceived(buffer);
s_Logger.Info($"OnMessage: Size: {size} - Channel: {channel}");
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 71b7467a62944868972778b3326a153a
timeCreated: 1750615670