From 574512f1bfd212d613868edd7f3d674a9478f3d2 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Thu, 22 Nov 2018 10:37:58 +0100 Subject: [PATCH] Extremely basic game ticker. (#126) You can theoretically restart the round (resetting the map) now, but it breaks very badly if a client is connected due to various edge cases/race conditions. --- .../Storage/ClientStorageComponent.cs | 5 + Content.Server/Content.Server.csproj | 6 +- Content.Server/EntryPoint.cs | 114 ++++----- Content.Server/GameTicking/GamePreset.cs | 10 + .../GameTicking/GamePresets/PresetTraitor.cs | 12 + Content.Server/GameTicking/GameTicker.cs | 238 ++++++++++++++++++ .../Interfaces/GameTicking/IGameTicker.cs | 23 ++ engine | 2 +- 8 files changed, 345 insertions(+), 65 deletions(-) create mode 100644 Content.Server/GameTicking/GamePreset.cs create mode 100644 Content.Server/GameTicking/GamePresets/PresetTraitor.cs create mode 100644 Content.Server/GameTicking/GameTicker.cs create mode 100644 Content.Server/Interfaces/GameTicking/IGameTicker.cs diff --git a/Content.Client/GameObjects/Components/Storage/ClientStorageComponent.cs b/Content.Client/GameObjects/Components/Storage/ClientStorageComponent.cs index 9878fc0767..3019553615 100644 --- a/Content.Client/GameObjects/Components/Storage/ClientStorageComponent.cs +++ b/Content.Client/GameObjects/Components/Storage/ClientStorageComponent.cs @@ -123,6 +123,11 @@ namespace Content.Client.GameObjects.Components.Storage if(!spriteComp.Running) return; + if (spriteComp.BaseRSI == null) + { + return; + } + var baseName = spriteComp.LayerGetState(0).Name; var stateId = open ? $"{baseName}_open" : $"{baseName}_door"; diff --git a/Content.Server/Content.Server.csproj b/Content.Server/Content.Server.csproj index 1e57e86ca7..6fa3c3aa0f 100644 --- a/Content.Server/Content.Server.csproj +++ b/Content.Server/Content.Server.csproj @@ -108,6 +108,9 @@ + + + @@ -116,6 +119,7 @@ + @@ -171,4 +175,4 @@ - + \ No newline at end of file diff --git a/Content.Server/EntryPoint.cs b/Content.Server/EntryPoint.cs index 45d9270e29..ad7002a26f 100644 --- a/Content.Server/EntryPoint.cs +++ b/Content.Server/EntryPoint.cs @@ -34,24 +34,21 @@ using Content.Server.GameObjects.EntitySystems; using Content.Server.Mobs; using Content.Server.Players; using Content.Server.GameObjects.Components.Interactable; +using Content.Server.GameTicking; using Content.Server.Interfaces; +using Content.Server.Interfaces.GameTicking; using Content.Shared.GameObjects.Components.Inventory; using Content.Shared.Interfaces; +using SS14.Shared.Timing; namespace Content.Server { public class EntryPoint : GameServer { - const string PlayerPrototypeName = "HumanMob_Content"; - private IBaseServer _server; private IPlayerManager _players; - private IEntityManager entityManager; private IChatManager chatManager; - - private bool _countdownStarted; - - private GridLocalCoordinates SpawnPoint; + private IGameTicker _gameTicker; /// public override void Init() @@ -60,7 +57,6 @@ namespace Content.Server _server = IoCManager.Resolve(); _players = IoCManager.Resolve(); - entityManager = IoCManager.Resolve(); chatManager = IoCManager.Resolve(); _players.PlayerStatusChanged += HandlePlayerStatusChanged; @@ -129,8 +125,11 @@ namespace Content.Server IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); IoCManager.BuildGraph(); + _gameTicker = IoCManager.Resolve(); + IoCManager.Resolve().Initialize(); } @@ -138,18 +137,7 @@ namespace Content.Server { base.PostInit(); - var timing = IoCManager.Resolve(); - var mapLoader = IoCManager.Resolve(); - var mapMan = IoCManager.Resolve(); - - var newMap = mapMan.CreateMap(); - var grid = mapLoader.LoadBlueprint(newMap, "Maps/stationstation.yml"); - - SpawnPoint = new GridLocalCoordinates(Vector2.Zero, grid); - - var startTime = timing.RealTime; - var timeSpan = timing.RealTime - startTime; - Logger.Info($"Loaded map in {timeSpan.TotalMilliseconds:N2}ms."); + _gameTicker.Initialize(); } /// @@ -163,6 +151,13 @@ namespace Content.Server base.Dispose(disposing); } + public override void Update(AssemblyLoader.UpdateLevel level, float frameTime) + { + base.Update(level, frameTime); + + _gameTicker.Update(new FrameEventArgs(frameTime)); + } + private void HandlePlayerStatusChanged(object sender, SessionStatusEventArgs args) { var session = args.Session; @@ -170,67 +165,60 @@ namespace Content.Server switch (args.NewStatus) { case SessionStatus.Connected: + { + if (session.Data.ContentDataUncast == null) { - if (session.Data.ContentDataUncast == null) - { - session.Data.ContentDataUncast = new PlayerData(session.SessionId); - } - // timer time must be > tick length - Timer.Spawn(0, args.Session.JoinLobby); - - chatManager.DispatchMessage(ChatChannel.Server, "Gamemode: Player joined server!", args.Session.SessionId); + session.Data.ContentDataUncast = new PlayerData(session.SessionId); } + + // timer time must be > tick length + Timer.Spawn(0, args.Session.JoinLobby); + + chatManager.DispatchMessage(ChatChannel.Server, "Gamemode: Player joined server!", + args.Session.SessionId); + } break; case SessionStatus.InLobby: - { - chatManager.DispatchMessage(ChatChannel.Server, "Gamemode: Player joined Lobby!", args.Session.SessionId); - } + { + chatManager.DispatchMessage(ChatChannel.Server, "Gamemode: Player joined Lobby!", + args.Session.SessionId); + } break; case SessionStatus.InGame: + { + //TODO: Check for existing mob and re-attach + var data = session.ContentData(); + if (data.Mind == null) { - //TODO: Check for existing mob and re-attach - var data = session.ContentData(); - if (data.Mind == null) + // No mind yet (new session), make a new one. + data.Mind = new Mind(session.SessionId); + var mob = _gameTicker.SpawnPlayerMob(); + data.Mind.TransferTo(mob); + } + else + { + if (data.Mind.CurrentEntity == null) { - // No mind yet (new session), make a new one. - data.Mind = new Mind(session.SessionId); - var mob = SpawnPlayerMob(); + var mob = _gameTicker.SpawnPlayerMob(); data.Mind.TransferTo(mob); } - else - { - if (data.Mind.CurrentEntity == null) - { - var mob = SpawnPlayerMob(); - data.Mind.TransferTo(mob); - } - session.AttachToEntity(data.Mind.CurrentEntity); - } - chatManager.DispatchMessage(ChatChannel.Server, "Gamemode: Player joined Game!", args.Session.SessionId); + + session.AttachToEntity(data.Mind.CurrentEntity); } + + chatManager.DispatchMessage(ChatChannel.Server, "Gamemode: Player joined Game!", + args.Session.SessionId); + } break; case SessionStatus.Disconnected: - { - chatManager.DispatchMessage(ChatChannel.Server, "Gamemode: Player left!", args.Session.SessionId); - } + { + chatManager.DispatchMessage(ChatChannel.Server, "Gamemode: Player left!", args.Session.SessionId); + } break; } } - - IEntity SpawnPlayerMob() - { - var entity = entityManager.ForceSpawnEntityAt(PlayerPrototypeName, SpawnPoint); - var shoes = entityManager.SpawnEntity("ShoesItem"); - var uniform = entityManager.SpawnEntity("UniformAssistant"); - if (entity.TryGetComponent(out InventoryComponent inventory)) - { - inventory.Equip(EquipmentSlotDefines.Slots.INNERCLOTHING, uniform.GetComponent()); - inventory.Equip(EquipmentSlotDefines.Slots.SHOES, shoes.GetComponent()); - } - return entity; - } } } diff --git a/Content.Server/GameTicking/GamePreset.cs b/Content.Server/GameTicking/GamePreset.cs new file mode 100644 index 0000000000..1da340d5aa --- /dev/null +++ b/Content.Server/GameTicking/GamePreset.cs @@ -0,0 +1,10 @@ +namespace Content.Server.GameTicking +{ + /// + /// A round-start setup preset, such as which antagonists to spawn. + /// + public abstract class GamePreset + { + public abstract void Start(); + } +} diff --git a/Content.Server/GameTicking/GamePresets/PresetTraitor.cs b/Content.Server/GameTicking/GamePresets/PresetTraitor.cs new file mode 100644 index 0000000000..c78fa8f4b3 --- /dev/null +++ b/Content.Server/GameTicking/GamePresets/PresetTraitor.cs @@ -0,0 +1,12 @@ +using SS14.Shared.Log; + +namespace Content.Server.GameTicking.GamePresets +{ + public class PresetTraitor : GamePreset + { + public override void Start() + { + Logger.DebugS("ticker.preset", "Current preset is traitor."); + } + } +} diff --git a/Content.Server/GameTicking/GameTicker.cs b/Content.Server/GameTicking/GameTicker.cs new file mode 100644 index 0000000000..a4886fb5a7 --- /dev/null +++ b/Content.Server/GameTicking/GameTicker.cs @@ -0,0 +1,238 @@ +using System; +using System.Linq; +using Content.Server.GameObjects; +using Content.Server.GameTicking.GamePresets; +using Content.Server.Interfaces.GameTicking; +using Content.Shared.GameObjects.Components.Inventory; +using JetBrains.Annotations; +using SS14.Server.Interfaces; +using SS14.Server.Interfaces.Console; +using SS14.Server.Interfaces.Maps; +using SS14.Server.Interfaces.Player; +using SS14.Shared.Configuration; +using SS14.Shared.Interfaces.Configuration; +using SS14.Shared.Interfaces.GameObjects; +using SS14.Shared.Interfaces.Map; +using SS14.Shared.Interfaces.Timing; +using SS14.Shared.IoC; +using SS14.Shared.Log; +using SS14.Shared.Map; +using SS14.Shared.Maths; +using SS14.Shared.Timing; +using SS14.Shared.Utility; +using SS14.Shared.ViewVariables; + +namespace Content.Server.GameTicking +{ + public class GameTicker : IGameTicker + { + [ViewVariables] + public GameRunLevel RunLevel + { + get => _runLevel; + private set + { + if (_runLevel == value) + { + return; + } + + var old = _runLevel; + _runLevel = value; + + OnRunLevelChanged?.Invoke(new GameRunLevelChangedEventArgs(old, value)); + } + } + + private const string PlayerPrototypeName = "HumanMob_Content"; + private const string MapFile = "Maps/stationstation.yml"; + + private bool _initialized; + [ViewVariables(VVAccess.ReadWrite)] private GridLocalCoordinates _spawnPoint; + +#pragma warning disable 649 + [Dependency] private IEntityManager _entityManager; + [Dependency] private IMapManager _mapManager; + [Dependency] private IMapLoader _mapLoader; + [Dependency] private IGameTiming _gameTiming; + [Dependency] private IConfigurationManager _configurationManager; +#pragma warning restore 649 + + public Action OnRunLevelChanged; + private GameRunLevel _runLevel; + + public void Initialize() + { + DebugTools.Assert(!_initialized); + + _configurationManager.RegisterCVar("game.lobbyenabled", false, CVar.ARCHIVE); + + RestartRound(); + + _initialized = true; + } + + public void Update(FrameEventArgs frameEventArgs) + { + } + + public void RestartRound() + { + Logger.InfoS("ticker", "Restarting round!"); + + RunLevel = GameRunLevel.PreRoundLobby; + _resettingCleanup(); + _preRoundSetup(); + + if (_configurationManager.GetCVar("game.lobbyenabled")) + { + throw new NotImplementedException(); + } + + StartRound(); + } + + public void StartRound() + { + DebugTools.Assert(RunLevel == GameRunLevel.PreRoundLobby); + Logger.InfoS("ticker", "Starting round!"); + + RunLevel = GameRunLevel.InRound; + + // TODO: Allow other presets to be selected. + var preset = new PresetTraitor(); + preset.Start(); + } + + public void EndRound() + { + DebugTools.Assert(RunLevel == GameRunLevel.InRound); + Logger.InfoS("ticker", "Ending round!"); + + RunLevel = GameRunLevel.PostRound; + } + + public IEntity SpawnPlayerMob() + { + var entity = _entityManager.ForceSpawnEntityAt(PlayerPrototypeName, _spawnPoint); + var shoes = _entityManager.SpawnEntity("ShoesItem"); + var uniform = _entityManager.SpawnEntity("UniformAssistant"); + if (entity.TryGetComponent(out InventoryComponent inventory)) + { + inventory.Equip(EquipmentSlotDefines.Slots.INNERCLOTHING, uniform.GetComponent()); + inventory.Equip(EquipmentSlotDefines.Slots.SHOES, shoes.GetComponent()); + } + + return entity; + } + + /// + /// Cleanup that has to run to clear up anything from the previous round. + /// Stuff like wiping the previous map clean. + /// + private void _resettingCleanup() + { + // Delete all entities. + foreach (var entity in _entityManager.GetEntities().ToList()) + { + // TODO: Maybe something less naive here? + // FIXME: Actually, definitely. + entity.Delete(); + } + + // Delete all maps outside of nullspace. + foreach (var map in _mapManager.GetAllMaps().ToList()) + { + // TODO: Maybe something less naive here? + if (map.Index != MapId.Nullspace) + { + _mapManager.DeleteMap(map.Index); + } + } + } + + private void _preRoundSetup() + { + var newMap = _mapManager.CreateMap(); + var startTime = _gameTiming.RealTime; + var grid = _mapLoader.LoadBlueprint(newMap, MapFile); + + _spawnPoint = new GridLocalCoordinates(Vector2.Zero, grid); + + var timeSpan = _gameTiming.RealTime - startTime; + Logger.InfoS("ticker", $"Loaded map in {timeSpan.TotalMilliseconds:N2}ms."); + } + } + + public enum GameRunLevel + { + PreRoundLobby = 0, + InRound, + PostRound + } + + public class GameRunLevelChangedEventArgs : EventArgs + { + public GameRunLevel OldRunLevel { get; } + public GameRunLevel NewRunLevel { get; } + + public GameRunLevelChangedEventArgs(GameRunLevel oldRunLevel, GameRunLevel newRunLevel) + { + OldRunLevel = oldRunLevel; + NewRunLevel = newRunLevel; + } + } + + class StartRoundCommand : IClientCommand + { + public string Command => "startround"; + public string Description => "Ends PreRoundLobby state and starts the round."; + public string Help => String.Empty; + + public void Execute(IConsoleShell shell, IPlayerSession player, string[] args) + { + var ticker = IoCManager.Resolve(); + + if (ticker.RunLevel != GameRunLevel.PreRoundLobby) + { + shell.SendText(player, "This can only be executed while the game is in the pre-round lobby."); + return; + } + + ticker.StartRound(); + } + } + + class EndRoundCommand : IClientCommand + { + public string Command => "endround"; + public string Description => "Ends the round and moves the server to PostRound."; + public string Help => String.Empty; + + public void Execute(IConsoleShell shell, IPlayerSession player, string[] args) + { + var ticker = IoCManager.Resolve(); + + if (ticker.RunLevel != GameRunLevel.InRound) + { + shell.SendText(player, "This can only be executed while the game is in a round."); + return; + } + + ticker.EndRound(); + } + } + + class NewRoundCommand : IClientCommand + { + public string Command => "restartround"; + public string Description => "Moves the server from PostRound to a new PreRoundLobby."; + public string Help => String.Empty; + + public void Execute(IConsoleShell shell, IPlayerSession player, string[] args) + { + var ticker = IoCManager.Resolve(); + ticker.RestartRound(); + } + } +} diff --git a/Content.Server/Interfaces/GameTicking/IGameTicker.cs b/Content.Server/Interfaces/GameTicking/IGameTicker.cs new file mode 100644 index 0000000000..6769b963f5 --- /dev/null +++ b/Content.Server/Interfaces/GameTicking/IGameTicker.cs @@ -0,0 +1,23 @@ +using Content.Server.GameTicking; +using SS14.Shared.Interfaces.GameObjects; +using SS14.Shared.Timing; + +namespace Content.Server.Interfaces.GameTicking +{ + /// + /// The game ticker is responsible for managing the round-by-round system of the game. + /// + public interface IGameTicker + { + GameRunLevel RunLevel { get; } + + void Initialize(); + void Update(FrameEventArgs frameEventArgs); + + void RestartRound(); + void StartRound(); + void EndRound(); + + IEntity SpawnPlayerMob(); + } +} diff --git a/engine b/engine index fcc405e6b6..8320339ab0 160000 --- a/engine +++ b/engine @@ -1 +1 @@ -Subproject commit fcc405e6b6006cabe193a700491ec8cee848a591 +Subproject commit 8320339ab03d47aeb027a29bacaded56d13cd021