Files
RobustToolbox/Robust.Client/BaseClient.cs
T
83c2a1be11 [Dependency] source generator part 2 (#6550)
* [Dependency] source generator

No more reflection, no more codegen at runtime

Also various changes to Roslyn helpers to make this easier to write.

Requires all types with dependencies to be partial and not have readonly dependency fields. An analyzer enforces this at warning level, the previous injection strategies have remained in the code *for now* as a fallback.

No fallback is available for [field: Dependency] properties, due to a Roslyn bug.

Code Fixes exist. We love Roslyn

* Apply dependencies generator changes to all code

* Release notes

* Preprocessor got hands

* Handle nullable dependencies

These are bad but gotta deal with it.

* Apply suggestions from code review

Co-authored-by: Moony <moony@hellomouse.net>

* Fine, let's not use collection expressions

---------

Co-authored-by: Moony <moony@hellomouse.net>
2026-05-08 12:38:33 +02:00

410 lines
14 KiB
C#

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
{
/// <inheritdoc />
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 IMapManager _mapManager = default!;
[Dependency] private IDiscordRichPresence _discord = default!;
[Dependency] private IGameTiming _timing = default!;
[Dependency] private IClientGameStateManager _gameStates = default!;
[Dependency] private ILogManager _logMan = default!;
/// <inheritdoc />
public ushort DefaultPort { get; } = 1212;
/// <inheritdoc />
public ClientRunLevel RunLevel { get; private set; }
/// <inheritdoc />
public ServerInfo? GameInfo { get; private set; }
/// <inheritdoc />
public string? PlayerNameOverride { get; set; }
public string? LastDisconnectReason { get; private set; }
private (TimeSpan, GameTick) _timeBase;
private ISawmill _logger = default!;
/// <inheritdoc />
public void Initialize()
{
_net.Connected += OnConnected;
_net.ConnectFailed += OnConnectFailed;
_net.Disconnect += OnNetDisconnect;
_net.RegisterNetMessage<MsgSyncTimeBase>(
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}");
}
/// <inheritdoc />
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));
}
/// <inheritdoc />
public void DisconnectFromServer(string reason)
{
_net.ClientDisconnect(reason);
}
/// <inheritdoc />
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();
}
/// <inheritdoc />
public void StopSinglePlayer()
{
DebugTools.Assert(RunLevel == ClientRunLevel.SinglePlayerGame);
DebugTools.Assert(!_net.IsConnected);
GameStoppedReset();
}
/// <inheritdoc />
public event EventHandler<RunLevelChangedEventArgs>? RunLevelChanged;
public event EventHandler<PlayerEventArgs>? PlayerJoinedServer;
public event EventHandler<PlayerEventArgs>? PlayerJoinedGame;
public event EventHandler<PlayerEventArgs>? 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<string>("game.hostname");
if (info == null)
{
GameInfo = info = new ServerInfo(serverName);
}
else
{
info.ServerName = serverName;
}
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());
}
/// <summary>
/// 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.
/// </summary>
/// <param name="session">Session of the player.</param>
private void OnPlayerJoinedServer(ICommonSession session)
{
DebugTools.Assert(RunLevel < ClientRunLevel.Connected);
OnRunLevelChanged(ClientRunLevel.Connected);
GameStartedSetup();
PlayerJoinedServer?.Invoke(this, new PlayerEventArgs(session));
}
/// <summary>
/// Player is joining the game
/// </summary>
/// <param name="session">Session of the player.</param>
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();
_mapManager.Startup();
_timing.ResetSimTime(_timeBase);
_timing.Paused = false;
}
private void GameStoppedReset()
{
_configManager.FlushMessages();
_gameStates.Reset();
_playMan.Shutdown();
_entityManager.Shutdown();
_mapManager.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");
}
}
/// <summary>
/// Enumeration of the run levels of the BaseClient.
/// </summary>
/// <seealso cref="ClientRunLevelExt"/>
public enum ClientRunLevel : byte
{
Error = 0,
/// <summary>
/// The client has not started connecting to a server (on main menu).
/// </summary>
Initialize,
/// <summary>
/// The client started connecting to the server, and is in the process of building the session.
/// </summary>
Connecting,
/// <summary>
/// The client has successfully finished connecting to the server.
/// </summary>
Connected,
/// <summary>
/// The client is now in the game, moving around.
/// </summary>
InGame,
/// <summary>
/// The client is now in singleplayer mode, in-game.
/// </summary>
SinglePlayerGame,
}
/// <summary>
/// Helper functions for working with <see cref="ClientRunLevel"/>.
/// </summary>
public static class ClientRunLevelExt
{
/// <summary>
/// Check if a <see cref="ClientRunLevel"/> is <see cref="ClientRunLevel.InGame"/>
/// or <see cref="ClientRunLevel.SinglePlayerGame"/>.
/// </summary>
public static bool IsInGameLike(this ClientRunLevel runLevel)
{
return runLevel is ClientRunLevel.InGame or ClientRunLevel.SinglePlayerGame;
}
}
/// <summary>
/// Event arguments for when something changed with the player.
/// </summary>
public sealed class PlayerEventArgs : EventArgs
{
/// <summary>
/// The session that triggered the event.
/// </summary>
private ICommonSession? Session { get; }
/// <summary>
/// Constructs a new instance of the class.
/// </summary>
public PlayerEventArgs(ICommonSession? session)
{
Session = session;
}
}
/// <summary>
/// Event arguments for when the RunLevel has changed in the BaseClient.
/// </summary>
public sealed class RunLevelChangedEventArgs : EventArgs
{
/// <summary>
/// RunLevel that the BaseClient switched from.
/// </summary>
public ClientRunLevel OldLevel { get; }
/// <summary>
/// RunLevel that the BaseClient switched to.
/// </summary>
public ClientRunLevel NewLevel { get; }
/// <summary>
/// Constructs a new instance of the class.
/// </summary>
public RunLevelChangedEventArgs(ClientRunLevel oldLevel, ClientRunLevel newLevel)
{
OldLevel = oldLevel;
NewLevel = newLevel;
}
}
/// <summary>
/// Info about the server and player that is sent to the client while connecting.
/// </summary>
public sealed class ServerInfo
{
public ServerInfo(string serverName)
{
ServerName = serverName;
}
/// <summary>
/// Current name of the server.
/// </summary>
public string ServerName { get; set; }
/// <summary>
/// Max number of players that are allowed in the server at one time.
/// </summary>
public int ServerMaxPlayers { get; set; }
public uint TickRate { get; internal set; }
}
}