using System; using System.Net; using Robust.Client.Configuration; using Robust.Client.GameObjects; using Robust.Client.GameStates; using Robust.Client.Player; using Robust.Client.Utility; using Robust.Shared; using Robust.Shared.Configuration; using Robust.Shared.Enums; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Network; using Robust.Shared.Network.Messages; using Robust.Shared.Player; using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Robust.Client { /// public sealed partial class BaseClient : IBaseClient, IPostInjectInit { [Dependency] private IClientNetManager _net = default!; [Dependency] private IPlayerManager _playMan = default!; [Dependency] private IClientNetConfigurationManager _configManager = default!; [Dependency] private IClientEntityManager _entityManager = default!; [Dependency] private IDiscordRichPresence _discord = default!; [Dependency] private IGameTiming _timing = default!; [Dependency] private IClientGameStateManager _gameStates = default!; [Dependency] private ILogManager _logMan = default!; /// public ushort DefaultPort { get; } = 1212; /// public ClientRunLevel RunLevel { get; private set; } /// public ServerInfo? GameInfo { get; private set; } /// public string? PlayerNameOverride { get; set; } public string? LastDisconnectReason { get; private set; } private (TimeSpan, GameTick) _timeBase; private ISawmill _logger = default!; /// public void Initialize() { _net.Connected += OnConnected; _net.ConnectFailed += OnConnectFailed; _net.Disconnect += OnNetDisconnect; _net.RegisterNetMessage( SyncTimeBase, NetMessageAccept.Handshake | NetMessageAccept.Client); _configManager.OnValueChanged(CVars.NetTickrate, TickRateChanged, invokeImmediately: true); _configManager.OnValueChanged(CVars.GameTimeScale, TimeScaleChanged, invokeImmediately: true); _playMan.Initialize(0); _playMan.PlayerListUpdated += OnPlayerListUpdated; Reset(); } private void OnPlayerListUpdated() { var serverPlayers = _playMan.PlayerCount; if (_net.ServerChannel != null && GameInfo != null && _net.IsConnected) _discord.Update(GameInfo.ServerName, _net.ServerChannel.UserName, GameInfo.ServerMaxPlayers.ToString(), serverPlayers.ToString()); } private void SyncTimeBase(MsgSyncTimeBase message) { _logger.Debug($"Synchronized time base: {message.Tick}: {message.Time}"); if (RunLevel >= ClientRunLevel.Connected) _timing.TimeBase = (message.Time, message.Tick); else _timeBase = (message.Time, message.Tick); } private void TickRateChanged(int tickrate, in CVarChangeInfo info) { if (GameInfo != null) { GameInfo.TickRate = (ushort) tickrate; } _timing.SetTickRateAt((ushort) tickrate, info.TickChanged); _logger.Info($"Tickrate changed to: {tickrate} on tick {_timing.CurTick}"); } private void TimeScaleChanged(float timeScale, in CVarChangeInfo info) { if (!GameTiming.IsTimescaleValid(timeScale)) { _logger.Error($"Invalid time scale set: {timeScale}, ignoring"); return; } _timing.TimeScale = timeScale; _logger.Info($"Tickrate changed to: {timeScale} on tick {_timing.CurTick}"); } /// public void ConnectToServer(DnsEndPoint endPoint) { if (RunLevel == ClientRunLevel.Connecting) { _net.Reset("Client mashing that connect button."); Reset(); } DebugTools.Assert(RunLevel < ClientRunLevel.Connecting); DebugTools.Assert(!_net.IsConnected); OnRunLevelChanged(ClientRunLevel.Connecting); _net.ClientConnect(endPoint.Host, endPoint.Port, PlayerNameOverride ?? _configManager.GetCVar(CVars.PlayerName)); } /// public void DisconnectFromServer(string reason) { _net.ClientDisconnect(reason); } /// public void StartSinglePlayer() { DebugTools.Assert(RunLevel < ClientRunLevel.Connecting); DebugTools.Assert(!_net.IsConnected); var name = PlayerNameOverride ?? _configManager.GetCVar(CVars.PlayerName); _playMan.SetupSinglePlayer(name); OnRunLevelChanged(ClientRunLevel.SinglePlayerGame); _playMan.JoinGame(_playMan.LocalSession!); GameStartedSetup(); } /// public void StopSinglePlayer() { DebugTools.Assert(RunLevel == ClientRunLevel.SinglePlayerGame); DebugTools.Assert(!_net.IsConnected); GameStoppedReset(); } /// public event EventHandler? RunLevelChanged; public event EventHandler? PlayerJoinedServer; public event EventHandler? PlayerJoinedGame; public event EventHandler? PlayerLeaveServer; private void OnConnected(object? sender, NetChannelArgs args) { _configManager.SyncWithServer(); _configManager.ReceivedInitialNwVars += OnReceivedClientData; } private void OnReceivedClientData(object? sender, EventArgs e) { _configManager.ReceivedInitialNwVars -= OnReceivedClientData; var info = GameInfo; var serverName = _configManager.GetCVar(CVars.GameHostName); var maxPlayers = _configManager.GetCVar(CVars.NetMaxConnections); if (info == null) { GameInfo = info = new ServerInfo(serverName, maxPlayers); } else { info.ServerName = serverName; info.ServerMaxPlayers = maxPlayers; } var channel = _net.ServerChannel!; // start up player management _playMan.SetupMultiplayer(channel); _playMan.PlayerStatusChanged += OnStatusChanged; var serverPlayers = _playMan.PlayerCount; _discord.Update(info.ServerName, channel.UserName, info.ServerMaxPlayers.ToString(), serverPlayers.ToString()); } /// /// Player session is fully built, player is an active member of the server. Player is prepared to start /// receiving states when they join the lobby. /// /// Session of the player. private void OnPlayerJoinedServer(ICommonSession session) { DebugTools.Assert(RunLevel < ClientRunLevel.Connected); OnRunLevelChanged(ClientRunLevel.Connected); GameStartedSetup(); PlayerJoinedServer?.Invoke(this, new PlayerEventArgs(session)); } /// /// Player is joining the game /// /// Session of the player. private void OnPlayerJoinedGame(ICommonSession session) { DebugTools.Assert(RunLevel >= ClientRunLevel.Connected); OnRunLevelChanged(ClientRunLevel.InGame); PlayerJoinedGame?.Invoke(this, new PlayerEventArgs(session)); } private void Reset() { _configManager.ReceivedInitialNwVars -= OnReceivedClientData; _playMan.PlayerStatusChanged -= OnStatusChanged; _configManager.ClearReceivedInitialNwVars(); OnRunLevelChanged(ClientRunLevel.Initialize); } private void OnConnectFailed(object? sender, NetConnectFailArgs args) { DebugTools.Assert(RunLevel == ClientRunLevel.Connecting); Reset(); } private void OnNetDisconnect(object? sender, NetDisconnectedArgs args) { DebugTools.Assert(RunLevel > ClientRunLevel.Initialize); // Don't invoke PlayerLeaveServer if PlayerJoinedServer & GameStartedSetup hasn't been called yet. if (RunLevel > ClientRunLevel.Connecting) PlayerLeaveServer?.Invoke(this, new PlayerEventArgs(_playMan.LocalSession)); LastDisconnectReason = args.Reason; GameStoppedReset(); } private void GameStartedSetup() { _entityManager.Startup(); _timing.ResetSimTime(_timeBase); _timing.Paused = false; } private void GameStoppedReset() { _configManager.FlushMessages(); _gameStates.Reset(); _playMan.Shutdown(); _entityManager.Shutdown(); _discord.ClearPresence(); Reset(); } private void OnStatusChanged(object? sender, SessionStatusEventArgs e) { if (e.Session != _playMan.LocalSession) return; // player finished fully connecting to the server. // OldStatus is used here because it can go from connecting-> connected or connecting-> ingame if (e.OldStatus == SessionStatus.Connecting) OnPlayerJoinedServer(e.Session); else if (e.NewStatus == SessionStatus.InGame) OnPlayerJoinedGame(e.Session); } private void OnRunLevelChanged(ClientRunLevel newRunLevel) { _logger.Debug($"Runlevel changed to: {newRunLevel}"); var args = new RunLevelChangedEventArgs(RunLevel, newRunLevel); RunLevel = newRunLevel; RunLevelChanged?.Invoke(this, args); } void IPostInjectInit.PostInject() { _logger = _logMan.GetSawmill("client"); } } /// /// Enumeration of the run levels of the BaseClient. /// /// public enum ClientRunLevel : byte { Error = 0, /// /// The client has not started connecting to a server (on main menu). /// Initialize, /// /// The client started connecting to the server, and is in the process of building the session. /// Connecting, /// /// The client has successfully finished connecting to the server. /// Connected, /// /// The client is now in the game, moving around. /// InGame, /// /// The client is now in singleplayer mode, in-game. /// SinglePlayerGame, } /// /// Helper functions for working with . /// public static class ClientRunLevelExt { /// /// Check if a is /// or . /// public static bool IsInGameLike(this ClientRunLevel runLevel) { return runLevel is ClientRunLevel.InGame or ClientRunLevel.SinglePlayerGame; } } /// /// Event arguments for when something changed with the player. /// public sealed class PlayerEventArgs : EventArgs { /// /// The session that triggered the event. /// private ICommonSession? Session { get; } /// /// Constructs a new instance of the class. /// public PlayerEventArgs(ICommonSession? session) { Session = session; } } /// /// Event arguments for when the RunLevel has changed in the BaseClient. /// public sealed class RunLevelChangedEventArgs : EventArgs { /// /// RunLevel that the BaseClient switched from. /// public ClientRunLevel OldLevel { get; } /// /// RunLevel that the BaseClient switched to. /// public ClientRunLevel NewLevel { get; } /// /// Constructs a new instance of the class. /// public RunLevelChangedEventArgs(ClientRunLevel oldLevel, ClientRunLevel newLevel) { OldLevel = oldLevel; NewLevel = newLevel; } } /// /// Info about the server and player that is sent to the client while connecting. /// public sealed class ServerInfo(string serverName, int serverMaxPlayers) { /// /// Current name of the server. /// public string ServerName { get; set; } = serverName; /// /// Max number of players that are allowed in the server at one time. /// public int ServerMaxPlayers { get; set; } = serverMaxPlayers; public uint TickRate { get; internal set; } } }