From a903ffb1059aa348564b7299d57743e380a1579f Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Wed, 15 May 2019 15:49:02 +0200 Subject: [PATCH 1/9] Allow admins to change round preset. --- Content.Server/Content.Server.csproj | 2 +- .../GameTicking/GamePresets/PresetSandbox.cs | 10 ++++ .../GameTicking/GamePresets/PresetTraitor.cs | 12 ----- Content.Server/GameTicking/GameTicker.cs | 49 ++++++++++++++++++- .../Interfaces/GameTicking/IGameTicker.cs | 2 + Resources/Groups/groups.yml | 1 + 6 files changed, 61 insertions(+), 15 deletions(-) create mode 100644 Content.Server/GameTicking/GamePresets/PresetSandbox.cs delete mode 100644 Content.Server/GameTicking/GamePresets/PresetTraitor.cs diff --git a/Content.Server/Content.Server.csproj b/Content.Server/Content.Server.csproj index 6eb2763531..8495a115da 100644 --- a/Content.Server/Content.Server.csproj +++ b/Content.Server/Content.Server.csproj @@ -142,7 +142,7 @@ - + diff --git a/Content.Server/GameTicking/GamePresets/PresetSandbox.cs b/Content.Server/GameTicking/GamePresets/PresetSandbox.cs new file mode 100644 index 0000000000..fa9d4ae76b --- /dev/null +++ b/Content.Server/GameTicking/GamePresets/PresetSandbox.cs @@ -0,0 +1,10 @@ +namespace Content.Server.GameTicking.GamePresets +{ + public sealed class PresetSandbox : GamePreset + { + public override void Start() + { + // Nothing yet. + } + } +} diff --git a/Content.Server/GameTicking/GamePresets/PresetTraitor.cs b/Content.Server/GameTicking/GamePresets/PresetTraitor.cs deleted file mode 100644 index 787cf8a146..0000000000 --- a/Content.Server/GameTicking/GamePresets/PresetTraitor.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Robust.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 index 5b98912752..34554700ee 100644 --- a/Content.Server/GameTicking/GameTicker.cs +++ b/Content.Server/GameTicking/GameTicker.cs @@ -15,13 +15,13 @@ using Robust.Server.Interfaces.Maps; using Robust.Server.Interfaces.Player; using Robust.Server.Player; using Robust.Shared.Configuration; -using Robust.Shared.Console; using Robust.Shared.Enums; using Robust.Shared.GameObjects; using Robust.Shared.Interfaces.Configuration; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.Map; using Robust.Shared.Interfaces.Network; +using Robust.Shared.Interfaces.Reflection; using Robust.Shared.Interfaces.Timing; using Robust.Shared.IoC; using Robust.Shared.Log; @@ -81,6 +81,8 @@ namespace Content.Server.GameTicking [ViewVariables] private readonly List _gameRules = new List(); + [ViewVariables] private Type _presetType; + #pragma warning disable 649 [Dependency] private IEntityManager _entityManager; [Dependency] private IMapManager _mapManager; @@ -155,7 +157,7 @@ namespace Content.Server.GameTicking RunLevel = GameRunLevel.InRound; // TODO: Allow other presets to be selected. - var preset = _dynamicTypeFactory.CreateInstance(); + var preset = (GamePreset)_dynamicTypeFactory.CreateInstance(_presetType); preset.Start(); foreach (var (playerSession, ready) in _playersInLobby.ToList()) @@ -248,6 +250,15 @@ namespace Content.Server.GameTicking public IEnumerable ActiveGameRules => _gameRules; + public void SetStartPreset(Type type) + { + if (!typeof(GamePreset).IsAssignableFrom(type)) + { + throw new ArgumentException("type must inherit GamePreset"); + } + _presetType = type; + } + private IEntity _spawnPlayerMob() { var entity = _entityManager.ForceSpawnEntityAt(PlayerPrototypeName, _getLateJoinSpawnPoint()); @@ -643,4 +654,38 @@ namespace Content.Server.GameTicking ticker.ToggleReady(player, bool.Parse(args[0])); } } + + class SetGamePresetCommand : IClientCommand + { + public string Command => "setgamepreset"; + public string Description => ""; + public string Help => ""; + + public void Execute(IConsoleShell shell, IPlayerSession player, string[] args) + { + if (args.Length != 1) + { + shell.SendText(player, "Need exactly one argument."); + return; + } + + var ticker = IoCManager.Resolve(); + + Type presetType; + switch (args[0]) + { + case "DeathMatch": + presetType = typeof(PresetDeathMatch); + break; + case "Sandbox": + presetType = typeof(PresetSandbox); + break; + default: + shell.SendText(player, "That is not a valid game preset!"); + return; + } + + ticker.SetStartPreset(presetType); + } + } } diff --git a/Content.Server/Interfaces/GameTicking/IGameTicker.cs b/Content.Server/Interfaces/GameTicking/IGameTicker.cs index ba737643c4..cf879f7c32 100644 --- a/Content.Server/Interfaces/GameTicking/IGameTicker.cs +++ b/Content.Server/Interfaces/GameTicking/IGameTicker.cs @@ -33,5 +33,7 @@ namespace Content.Server.Interfaces.GameTicking T AddGameRule() where T : GameRule, new(); void RemoveGameRule(GameRule rule); IEnumerable ActiveGameRules { get; } + + void SetStartPreset(Type type); } } diff --git a/Resources/Groups/groups.yml b/Resources/Groups/groups.yml index 1df54bd807..1fb9205555 100644 --- a/Resources/Groups/groups.yml +++ b/Resources/Groups/groups.yml @@ -45,4 +45,5 @@ - delete - tp - tpgrid + - setgamepreset CanViewVar: true From 792b219b047349240a55e722250d22cee0b7dd33 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Thu, 16 May 2019 14:09:11 +0200 Subject: [PATCH 2/9] Prevent server crashing without gamepreset override. --- Content.Server/GameTicking/GameTicker.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.Server/GameTicking/GameTicker.cs b/Content.Server/GameTicking/GameTicker.cs index 34554700ee..520331945c 100644 --- a/Content.Server/GameTicking/GameTicker.cs +++ b/Content.Server/GameTicking/GameTicker.cs @@ -157,7 +157,7 @@ namespace Content.Server.GameTicking RunLevel = GameRunLevel.InRound; // TODO: Allow other presets to be selected. - var preset = (GamePreset)_dynamicTypeFactory.CreateInstance(_presetType); + var preset = (GamePreset)_dynamicTypeFactory.CreateInstance(_presetType ?? typeof(PresetSandbox)); preset.Start(); foreach (var (playerSession, ready) in _playersInLobby.ToList()) From 91de6f80b10efc4fbe5f69c7ca571d16bf0eccaf Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Thu, 16 May 2019 14:18:33 +0200 Subject: [PATCH 3/9] Fix player list being blank when re-joining lobby. --- Content.Client/GameTicking/ClientGameTicker.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Content.Client/GameTicking/ClientGameTicker.cs b/Content.Client/GameTicking/ClientGameTicker.cs index e92e565fcc..72a9b3d42d 100644 --- a/Content.Client/GameTicking/ClientGameTicker.cs +++ b/Content.Client/GameTicking/ClientGameTicker.cs @@ -66,6 +66,11 @@ namespace Content.Client.GameTicking return; } + _updatePlayerList(); + } + + private void _updatePlayerList() + { _lobby.OnlinePlayerItemList.Clear(); foreach (var session in _playerManager.Sessions.OrderBy(s => s.Name)) { @@ -208,6 +213,8 @@ namespace Content.Client.GameTicking }; _lobby.LeaveButton.OnPressed += args => _console.ProcessCommand("disconnect"); + + _updatePlayerList(); } private void _joinGame(MsgTickerJoinGame message) From 818ee83440937c178854e45b4890601cc30fa246 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Thu, 16 May 2019 14:28:34 +0200 Subject: [PATCH 4/9] Remove IDisplayManager dependency from SS14Window, content changes. --- Content.Client/EscapeMenuOwner.cs | 5 +---- Content.Client/UserInterface/EscapeMenu.cs | 13 +++++-------- RobustToolbox | 2 +- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/Content.Client/EscapeMenuOwner.cs b/Content.Client/EscapeMenuOwner.cs index 667026cd53..d9e41b1c20 100644 --- a/Content.Client/EscapeMenuOwner.cs +++ b/Content.Client/EscapeMenuOwner.cs @@ -1,12 +1,10 @@ using Content.Client.UserInterface; using Robust.Client.Console; -using Robust.Client.Interfaces.Graphics; using Robust.Client.Interfaces.Input; using Robust.Client.Interfaces.Placement; using Robust.Client.Interfaces.ResourceManagement; using Robust.Client.Interfaces.State; using Robust.Client.State.States; -using Robust.Client.UserInterface.CustomControls; using Robust.Shared.Input; using Robust.Shared.Interfaces.Configuration; using Robust.Shared.Interfaces.Map; @@ -19,7 +17,6 @@ namespace Content.Client { #pragma warning disable 649 [Dependency] private readonly IStateManager _stateManager; - [Dependency] private readonly IDisplayManager _displayManager; [Dependency] private readonly IClientConsole _clientConsole; [Dependency] private readonly ITileDefinitionManager _tileDefinitionManager; [Dependency] private readonly IPlacementManager _placementManager; @@ -41,7 +38,7 @@ namespace Content.Client if (obj.NewState is GameScreen) { // Switched TO GameScreen. - _escapeMenu = new EscapeMenu(_displayManager, _clientConsole, _tileDefinitionManager, _placementManager, + _escapeMenu = new EscapeMenu(_clientConsole, _tileDefinitionManager, _placementManager, _prototypeManager, _resourceCache, _configurationManager) { Visible = false diff --git a/Content.Client/UserInterface/EscapeMenu.cs b/Content.Client/UserInterface/EscapeMenu.cs index 324d9590da..a13bae00e5 100644 --- a/Content.Client/UserInterface/EscapeMenu.cs +++ b/Content.Client/UserInterface/EscapeMenu.cs @@ -18,7 +18,6 @@ namespace Content.Client.UserInterface private readonly IPlacementManager _placementManager; private readonly IPrototypeManager _prototypeManager; private readonly IResourceCache _resourceCache; - private readonly IDisplayManager _displayManager; private readonly IConfigurationManager _configSystem; private BaseButton QuitButton; @@ -27,16 +26,14 @@ namespace Content.Client.UserInterface private BaseButton SpawnTilesButton; private OptionsMenu optionsMenu; - public EscapeMenu(IDisplayManager displayManager, - IClientConsole console, + public EscapeMenu(IClientConsole console, ITileDefinitionManager tileDefinitionManager, IPlacementManager placementManager, IPrototypeManager prototypeManager, IResourceCache resourceCache, - IConfigurationManager configSystem) : base(displayManager) + IConfigurationManager configSystem) { _configSystem = configSystem; - _displayManager = displayManager; _console = console; __tileDefinitionManager = tileDefinitionManager; _placementManager = placementManager; @@ -48,7 +45,7 @@ namespace Content.Client.UserInterface private void PerformLayout() { - optionsMenu = new OptionsMenu(_displayManager, _configSystem) + optionsMenu = new OptionsMenu(_configSystem) { Visible = false }; @@ -97,14 +94,14 @@ namespace Content.Client.UserInterface private void OnSpawnEntitiesButtonClicked(BaseButton.ButtonEventArgs args) { - var window = new EntitySpawnWindow(_displayManager, _placementManager, _prototypeManager, _resourceCache); + var window = new EntitySpawnWindow(_placementManager, _prototypeManager, _resourceCache); window.AddToScreen(); window.OpenToLeft(); } private void OnSpawnTilesButtonClicked(BaseButton.ButtonEventArgs args) { - var window = new TileSpawnWindow(__tileDefinitionManager, _placementManager, _displayManager, _resourceCache); + var window = new TileSpawnWindow(__tileDefinitionManager, _placementManager, _resourceCache); window.AddToScreen(); window.OpenToLeft(); } diff --git a/RobustToolbox b/RobustToolbox index 3bad55a705..10440648bc 160000 --- a/RobustToolbox +++ b/RobustToolbox @@ -1 +1 @@ -Subproject commit 3bad55a705b000e691bdcf50e54948f59fcc51bc +Subproject commit 10440648bc1225cecb5eb09ec2c45d0ec18423c9 From 1a5c4ad83c4624b2594683886a50acd75af2a6a2 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Thu, 16 May 2019 14:41:26 +0200 Subject: [PATCH 5/9] Give badmins access to round commands. --- Resources/Groups/groups.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Resources/Groups/groups.yml b/Resources/Groups/groups.yml index 1fb9205555..2aecb2f4ee 100644 --- a/Resources/Groups/groups.yml +++ b/Resources/Groups/groups.yml @@ -46,4 +46,8 @@ - tp - tpgrid - setgamepreset + - startround + - endround + - restartround + - respawn CanViewVar: true From c197278c6fb40b1b114cfa514f5fa927cd776a83 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Thu, 16 May 2019 15:51:26 +0200 Subject: [PATCH 6/9] Interaction rework. IActivate is now more used. IAttackHand falls back to IActivate. --- Content.Server/EntryPoint.cs | 4 + .../Construction/ConstructorComponent.cs | 2 +- .../Components/Doors/ServerDoorComponent.cs | 5 +- .../Items/Storage/EntityStorageComponent.cs | 5 +- .../Components/Power/ApcComponent.cs | 7 +- .../Components/Research/LatheComponent.cs | 13 +- .../EntitySystems/Click/InteractionSystem.cs | 337 ++++++++++-------- .../EntitySystems/StorageSystem.cs | 2 +- 8 files changed, 200 insertions(+), 175 deletions(-) diff --git a/Content.Server/EntryPoint.cs b/Content.Server/EntryPoint.cs index 98f41d7d90..450519faa9 100644 --- a/Content.Server/EntryPoint.cs +++ b/Content.Server/EntryPoint.cs @@ -88,6 +88,7 @@ namespace Content.Server factory.Register(); factory.Register(); factory.Register(); + factory.RegisterReference(); //Power Components factory.Register(); @@ -127,6 +128,7 @@ namespace Content.Server factory.RegisterReference(); factory.Register(); factory.RegisterReference(); + factory.RegisterReference(); factory.Register(); factory.Register(); @@ -135,6 +137,7 @@ namespace Content.Server factory.Register(); factory.Register(); factory.Register(); + factory.RegisterReference(); factory.Register(); factory.Register(); factory.Register(); @@ -152,6 +155,7 @@ namespace Content.Server factory.RegisterReference(); factory.Register(); + factory.RegisterReference(); factory.Register(); factory.RegisterReference(); diff --git a/Content.Server/GameObjects/Components/Construction/ConstructorComponent.cs b/Content.Server/GameObjects/Components/Construction/ConstructorComponent.cs index 2a27c6ccdb..92562e246b 100644 --- a/Content.Server/GameObjects/Components/Construction/ConstructorComponent.cs +++ b/Content.Server/GameObjects/Components/Construction/ConstructorComponent.cs @@ -44,7 +44,7 @@ namespace Content.Server.GameObjects.Components.Construction var prototype = _prototypeManager.Index(prototypeName); var transform = Owner.Transform; - if (!loc.InRange(_mapManager, transform.GridPosition, InteractionSystem.INTERACTION_RANGE)) + if (!loc.InRange(_mapManager, transform.GridPosition, InteractionSystem.InteractionRange)) { return; } diff --git a/Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs b/Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs index 3100198fd6..6f9957d617 100644 --- a/Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs +++ b/Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs @@ -10,7 +10,7 @@ using Robust.Shared.Timers; namespace Content.Server.GameObjects { - public class ServerDoorComponent : Component, IAttackHand + public class ServerDoorComponent : Component, IActivate { public override string Name => "Door"; @@ -41,7 +41,7 @@ namespace Content.Server.GameObjects base.OnRemove(); } - public bool AttackHand(AttackHandEventArgs eventArgs) + void IActivate.Activate(ActivateEventArgs eventArgs) { if (_state == DoorState.Open) { @@ -51,7 +51,6 @@ namespace Content.Server.GameObjects { Open(); } - return true; } public override void HandleMessage(ComponentMessage message, INetChannel netChannel = null, IComponent component = null) diff --git a/Content.Server/GameObjects/Components/Items/Storage/EntityStorageComponent.cs b/Content.Server/GameObjects/Components/Items/Storage/EntityStorageComponent.cs index 783ecdd774..327c8a089d 100644 --- a/Content.Server/GameObjects/Components/Items/Storage/EntityStorageComponent.cs +++ b/Content.Server/GameObjects/Components/Items/Storage/EntityStorageComponent.cs @@ -14,7 +14,7 @@ using Robust.Server.GameObjects; namespace Content.Server.GameObjects.Components { - public class EntityStorageComponent : Component, IAttackHand, IStorageComponent + public class EntityStorageComponent : Component, IActivate, IStorageComponent { public override string Name => "EntityStorage"; @@ -41,7 +41,7 @@ namespace Content.Server.GameObjects.Components [ViewVariables] public bool Open { get; private set; } - public bool AttackHand(AttackHandEventArgs eventArgs) + void IActivate.Activate(ActivateEventArgs eventArgs) { if (Open) { @@ -51,7 +51,6 @@ namespace Content.Server.GameObjects.Components { OpenStorage(); } - return true; } private void CloseStorage() diff --git a/Content.Server/GameObjects/Components/Power/ApcComponent.cs b/Content.Server/GameObjects/Components/Power/ApcComponent.cs index 51a5130131..62206488fb 100644 --- a/Content.Server/GameObjects/Components/Power/ApcComponent.cs +++ b/Content.Server/GameObjects/Components/Power/ApcComponent.cs @@ -12,7 +12,7 @@ using Robust.Shared.IoC; namespace Content.Server.GameObjects.Components.Power { - public sealed class ApcComponent : SharedApcComponent, IAttackHand + public sealed class ApcComponent : SharedApcComponent, IActivate { PowerStorageComponent Storage; AppearanceComponent Appearance; @@ -106,15 +106,14 @@ namespace Content.Server.GameObjects.Components.Power return net.Lack > 0 ? ApcExternalPowerState.Low : ApcExternalPowerState.Good; } - bool IAttackHand.AttackHand(AttackHandEventArgs eventArgs) + void IActivate.Activate(ActivateEventArgs eventArgs) { if (!eventArgs.User.TryGetComponent(out IActorComponent actor)) { - return false; + return; } _userInterface.Open(actor.playerSession); - return true; } private void _clickSound() diff --git a/Content.Server/GameObjects/Components/Research/LatheComponent.cs b/Content.Server/GameObjects/Components/Research/LatheComponent.cs index 5e55075e53..dff3661d66 100644 --- a/Content.Server/GameObjects/Components/Research/LatheComponent.cs +++ b/Content.Server/GameObjects/Components/Research/LatheComponent.cs @@ -15,7 +15,7 @@ using Robust.Shared.ViewVariables; namespace Content.Server.GameObjects.Components.Research { - public class LatheComponent : SharedLatheComponent, IAttackHand, IAttackBy, IActivate + public class LatheComponent : SharedLatheComponent, IAttackBy, IActivate { public const int VolumePerSheet = 3750; @@ -94,17 +94,6 @@ namespace Content.Server.GameObjects.Components.Research _userInterface.Open(actor.playerSession); return; } - - bool IAttackHand.AttackHand(AttackHandEventArgs eventArgs) - { - - if (!eventArgs.User.TryGetComponent(out IActorComponent actor)) - return false; - - _userInterface.Open(actor.playerSession); - return true; - } - bool IAttackBy.AttackBy(AttackByEventArgs eventArgs) { if (!Owner.TryGetComponent(out MaterialStorageComponent storage) diff --git a/Content.Server/GameObjects/EntitySystems/Click/InteractionSystem.cs b/Content.Server/GameObjects/EntitySystems/Click/InteractionSystem.cs index 9368ee223d..602b359cb6 100644 --- a/Content.Server/GameObjects/EntitySystems/Click/InteractionSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/Click/InteractionSystem.cs @@ -1,19 +1,19 @@ using System; -using Content.Server.Interfaces.GameObjects; -using Robust.Shared.GameObjects; -using Robust.Shared.GameObjects.Systems; -using Robust.Shared.Interfaces.GameObjects; -using System.Collections.Generic; using System.Linq; +using Content.Server.Interfaces.GameObjects; using Content.Shared.Input; -using Robust.Shared.Input; -using Robust.Shared.Log; -using Robust.Shared.Map; +using JetBrains.Annotations; using Robust.Server.GameObjects.EntitySystems; using Robust.Server.Interfaces.Player; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Input; +using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects.Components; using Robust.Shared.Interfaces.Map; using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Map; using Robust.Shared.Players; namespace Content.Server.GameObjects.EntitySystems @@ -26,9 +26,6 @@ namespace Content.Server.GameObjects.EntitySystems /// /// Called when using one object on another /// - /// - /// - /// bool AttackBy(AttackByEventArgs eventArgs); } @@ -47,8 +44,6 @@ namespace Content.Server.GameObjects.EntitySystems /// /// Called when a player directly interacts with an empty hand /// - /// - /// bool AttackHand(AttackHandEventArgs eventArgs); } @@ -65,13 +60,11 @@ namespace Content.Server.GameObjects.EntitySystems /// /// Called when we try to interact with an entity out of range /// - /// - /// - /// /// bool RangedAttackBy(RangedAttackByEventArgs eventArgs); } + [PublicAPI] public class RangedAttackByEventArgs : EventArgs { public IEntity User { get; set; } @@ -88,9 +81,6 @@ namespace Content.Server.GameObjects.EntitySystems /// /// Called when we interact with nothing, or when we interact with an entity out of range that has no behavior /// - /// - /// - /// The entity that was clicked on out of range. May be null if no entity was clicked on.true void AfterAttack(AfterAttackEventArgs eventArgs); } @@ -109,7 +99,6 @@ namespace Content.Server.GameObjects.EntitySystems /// /// Called when we activate an object we are holding to use it /// - /// /// bool UseEntity(UseEntityEventArgs eventArgs); } @@ -127,7 +116,6 @@ namespace Content.Server.GameObjects.EntitySystems /// /// Called when this component is activated by another entity. /// - /// Entity that activated this component. void Activate(ActivateEventArgs eventArgs); } @@ -139,50 +127,66 @@ namespace Content.Server.GameObjects.EntitySystems /// /// Governs interactions during clicking on entities /// - public class InteractionSystem : EntitySystem + [UsedImplicitly] + public sealed class InteractionSystem : EntitySystem { #pragma warning disable 649 [Dependency] private readonly IMapManager _mapManager; #pragma warning restore 649 - public const float INTERACTION_RANGE = 2; - public const float INTERACTION_RANGE_SQUARED = INTERACTION_RANGE * INTERACTION_RANGE; + public const float InteractionRange = 2; + public const float InteractionRangeSquared = InteractionRange * InteractionRange; public override void Initialize() { var inputSys = EntitySystemManager.GetEntitySystem(); - inputSys.BindMap.BindFunction(ContentKeyFunctions.UseItemInHand, new PointerInputCmdHandler(HandleUseItemInHand)); - inputSys.BindMap.BindFunction(ContentKeyFunctions.ActivateItemInWorld, new PointerInputCmdHandler((HandleUseItemInWorld))); + inputSys.BindMap.BindFunction(ContentKeyFunctions.UseItemInHand, + new PointerInputCmdHandler(HandleUseItemInHand)); + inputSys.BindMap.BindFunction(ContentKeyFunctions.ActivateItemInWorld, + new PointerInputCmdHandler((HandleActivateItemInWorld))); } - private void HandleUseItemInWorld(ICommonSession session, GridCoordinates coords, EntityUid uid) + public void HandleActivateItemInWorld(ICommonSession session, GridCoordinates coords, EntityUid uid) { - if(!EntityManager.TryGetEntity(uid, out var used)) + if (!EntityManager.TryGetEntity(uid, out var used)) return; var playerEnt = ((IPlayerSession) session).AttachedEntity; - if(playerEnt == null || !playerEnt.IsValid()) + if (playerEnt == null || !playerEnt.IsValid()) + { return; + } - if (!playerEnt.Transform.GridPosition.InRange(_mapManager, used.Transform.GridPosition, INTERACTION_RANGE)) + if (!playerEnt.Transform.GridPosition.InRange(_mapManager, used.Transform.GridPosition, InteractionRange)) + { return; + } - var activateMsg = new ActivateInWorldMessage(playerEnt, used); + InteractionActivate(playerEnt, used); + } + + private void InteractionActivate(IEntity user, IEntity used) + { + var activateMsg = new ActivateInWorldMessage(user, used); RaiseEvent(activateMsg); - if(activateMsg.Handled) + if (activateMsg.Handled) + { return; + } if (!used.TryGetComponent(out IActivate activateComp)) + { return; + } - activateComp.Activate(new ActivateEventArgs { User = playerEnt }); + activateComp.Activate(new ActivateEventArgs {User = user}); } private void HandleUseItemInHand(ICommonSession session, GridCoordinates coords, EntityUid uid) { // client sanitization - if(!_mapManager.GridExists(coords.GridID)) + if (!_mapManager.GridExists(coords.GridID)) { Logger.InfoS("system.interaction", $"Invalid Coordinates: client={session}, coords={coords}"); return; @@ -190,32 +194,37 @@ namespace Content.Server.GameObjects.EntitySystems if (uid.IsClientSide()) { - Logger.WarningS("system.interaction", $"Client sent interaction with client-side entity. Session={session}, Uid={uid}"); + Logger.WarningS("system.interaction", + $"Client sent interaction with client-side entity. Session={session}, Uid={uid}"); return; } - UserInteraction(((IPlayerSession)session).AttachedEntity, coords, uid); + UserInteraction(((IPlayerSession) session).AttachedEntity, coords, uid); } private void UserInteraction(IEntity player, GridCoordinates coordinates, EntityUid clickedUid) { - //Get entity clicked upon from UID if valid UID, if not assume no entity clicked upon and null + // Get entity clicked upon from UID if valid UID, if not assume no entity clicked upon and null if (!EntityManager.TryGetEntity(clickedUid, out var attacked)) + { attacked = null; + } - //Verify player has a transform component + // Verify player has a transform component if (!player.TryGetComponent(out var playerTransform)) { return; } - //Verify player is on the same map as the entity he clicked on - else if (_mapManager.GetGrid(coordinates.GridID).ParentMap.Index != playerTransform.MapID) + + // Verify player is on the same map as the entity he clicked on + if (_mapManager.GetGrid(coordinates.GridID).ParentMap.Index != playerTransform.MapID) { - Logger.Warning(string.Format("Player named {0} clicked on a map he isn't located on", player.Name)); + Logger.WarningS("system.interaction", + $"Player named {player.Name} clicked on a map he isn't located on"); return; } - //Verify player has a hand, and find what object he is currently holding in his active hand + // Verify player has a hand, and find what object he is currently holding in his active hand if (!player.TryGetComponent(out var hands)) { return; @@ -224,59 +233,64 @@ namespace Content.Server.GameObjects.EntitySystems var item = hands.GetActiveHand?.Owner; if (!ActionBlockerSystem.CanInteract(player)) - return; - //TODO: Check if client should be able to see that object to click on it in the first place, prevent using locaters by firing a laser or something - - - //Clicked on empty space behavior, try using ranged attack - if (attacked == null && item != null) - { - //AFTERATTACK: Check if we clicked on an empty location, if so the only interaction we can do is afterattack - InteractAfterattack(player, item, coordinates); - return; - } - else if (attacked == null) { return; } - //Verify attacked object is on the map if we managed to click on it somehow - if (!attacked.GetComponent().IsMapTransform) - { - Logger.Warning(string.Format("Player named {0} clicked on object {1} that isn't currently on the map somehow", player.Name, attacked.Name)); - return; - } + // TODO: Check if client should be able to see that object to click on it in the first place - //Check if ClickLocation is in object bounds here, if not lets log as warning and see why - if (attacked.TryGetComponent(out BoundingBoxComponent boundingbox)) + // Clicked on empty space behavior, try using ranged attack + if (attacked == null) { - if (!boundingbox.WorldAABB.Contains(coordinates.Position)) + if (item != null) { - Logger.Warning(string.Format("Player {0} clicked {1} outside of its bounding box component somehow", player.Name, attacked.Name)); + // After attack: Check if we clicked on an empty location, if so the only interaction we can do is AfterAttack + InteractAfterAttack(player, item, coordinates); + } + + return; + } + + // Verify attacked object is on the map if we managed to click on it somehow + if (!attacked.Transform.IsMapTransform) + { + Logger.WarningS("system.interaction", + $"Player named {player.Name} clicked on object {attacked.Name} that isn't currently on the map somehow"); + return; + } + + // Check if ClickLocation is in object bounds here, if not lets log as warning and see why + if (attacked.TryGetComponent(out BoundingBoxComponent boundingBox)) + { + if (!boundingBox.WorldAABB.Contains(coordinates.Position)) + { + Logger.WarningS("system.interaction", + $"Player {player.Name} clicked {attacked.Name} outside of its bounding box component somehow"); return; } } - //RANGEDATTACK/AFTERATTACK: Check distance between user and clicked item, if too large parse it in the ranged function - //TODO: have range based upon the item being used? or base it upon some variables of the player himself? - var distance = (playerTransform.WorldPosition - attacked.GetComponent().WorldPosition).LengthSquared; - if (distance > INTERACTION_RANGE_SQUARED) + // RangedAttack/AfterAttack: Check distance between user and clicked item, if too large parse it in the ranged function + // TODO: have range based upon the item being used? or base it upon some variables of the player himself? + var distance = (playerTransform.WorldPosition - attacked.Transform.WorldPosition).LengthSquared; + if (distance > InteractionRangeSquared) { if (item != null) { RangedInteraction(player, item, attacked, coordinates); return; } - return; //Add some form of ranged attackhand here if you need it someday, or perhaps just ways to modify the range of attackhand + + return; // Add some form of ranged AttackHand here if you need it someday, or perhaps just ways to modify the range of AttackHand } - //We are close to the nearby object and the object isn't contained in our active hand - //ATTACKBY/AFTERATTACK: We will either use the item on the nearby object + // We are close to the nearby object and the object isn't contained in our active hand + // AttackBy/AfterAttack: We will either use the item on the nearby object if (item != null) { Interaction(player, item, attacked, coordinates); } - //ATTACKHAND: Since our hand is empty we will use attackhand + // AttackHand/Activate: Since our hand is empty we will use AttackHand/Activate else { Interaction(player, attacked); @@ -284,90 +298,101 @@ namespace Content.Server.GameObjects.EntitySystems } /// - /// We didn't click on any entity, try doing an afterattack on the click location + /// We didn't click on any entity, try doing an AfterAttack on the click location /// - /// - /// - /// - public void InteractAfterattack(IEntity user, IEntity weapon, GridCoordinates clicklocation) + private void InteractAfterAttack(IEntity user, IEntity weapon, GridCoordinates clickLocation) { - var message = new AfterAttackMessage(user, weapon, null, clicklocation); + var message = new AfterAttackMessage(user, weapon, null, clickLocation); RaiseEvent(message); - if(message.Handled) - return; - - List afterattacks = weapon.GetAllComponents().ToList(); - - for (var i = 0; i < afterattacks.Count; i++) + if (message.Handled) { - afterattacks[i].AfterAttack(new AfterAttackEventArgs { User = user, ClickLocation = clicklocation }); + return; + } + + var afterAttacks = weapon.GetAllComponents().ToList(); + var afterAttackEventArgs = new AfterAttackEventArgs {User = user, ClickLocation = clickLocation}; + + foreach (var afterAttack in afterAttacks) + { + afterAttack.AfterAttack(afterAttackEventArgs); } } /// /// Uses a weapon/object on an entity - /// Finds interactable components with the Attackby interface and calls their function + /// Finds components with the AttackBy interface and calls their function /// - /// - /// - /// - public void Interaction(IEntity user, IEntity weapon, IEntity attacked, GridCoordinates clicklocation) + public void Interaction(IEntity user, IEntity weapon, IEntity attacked, GridCoordinates clickLocation) { - var attackMsg = new AttackByMessage(user, weapon, attacked, clicklocation); + var attackMsg = new AttackByMessage(user, weapon, attacked, clickLocation); RaiseEvent(attackMsg); - if(attackMsg.Handled) - return; - - List interactables = attacked.GetAllComponents().ToList(); - - for (var i = 0; i < interactables.Count; i++) + if (attackMsg.Handled) { - if (interactables[i].AttackBy(new AttackByEventArgs { User = user, ClickLocation = clicklocation, AttackWith = weapon })) //If an attackby returns a status completion we finish our attack + return; + } + + var attackBys = attacked.GetAllComponents().ToList(); + var attackByEventArgs = new AttackByEventArgs + { + User = user, ClickLocation = clickLocation, AttackWith = weapon + }; + + foreach (var attackBy in attackBys) + { + if (attackBy.AttackBy(attackByEventArgs)) { + // If an AttackBy returns a status completion we finish our attack return; } } - //Else check damage component to see if we damage if not attackby, and if so can we attack object - - var afterAtkMsg = new AfterAttackMessage(user, weapon, attacked, clicklocation); + var afterAtkMsg = new AfterAttackMessage(user, weapon, attacked, clickLocation); RaiseEvent(afterAtkMsg); if (afterAtkMsg.Handled) - return; - - //If we aren't directly attacking the nearby object, lets see if our item has an after attack we can do - List afterattacks = weapon.GetAllComponents().ToList(); - - for (var i = 0; i < afterattacks.Count; i++) { - afterattacks[i].AfterAttack(new AfterAttackEventArgs { User = user, ClickLocation = clicklocation, Attacked = attacked }); + return; + } + + // If we aren't directly attacking the nearby object, lets see if our item has an after attack we can do + var afterAttacks = weapon.GetAllComponents().ToList(); + var afterAttackEventArgs = new AfterAttackEventArgs + { + User = user, ClickLocation = clickLocation, Attacked = attacked + }; + + foreach (var afterAttack in afterAttacks) + { + afterAttack.AfterAttack(afterAttackEventArgs); } } /// /// Uses an empty hand on an entity - /// Finds interactable components with the Attackhand interface and calls their function + /// Finds components with the AttackHand interface and calls their function /// - /// - /// public void Interaction(IEntity user, IEntity attacked) { var message = new AttackHandMessage(user, attacked); RaiseEvent(message); - if(message.Handled) - return; - - List interactables = attacked.GetAllComponents().ToList(); - - for (var i = 0; i < interactables.Count; i++) + if (message.Handled) { - if (interactables[i].AttackHand(new AttackHandEventArgs { User = user})) //If an attackby returns a status completion we finish our attack + return; + } + + var attackHands = attacked.GetAllComponents().ToList(); + var attackHandEventArgs = new AttackHandEventArgs {User = user}; + + foreach (var attackHand in attackHands) + { + if (attackHand.AttackHand(attackHandEventArgs)) { + // If an AttackHand returns a status completion we finish our attack return; } } - //Else check damage component to see if we damage if not attackby, and if so can we attack object + // Else we run Activate. + InteractionActivate(user, attacked); } /// @@ -388,22 +413,23 @@ namespace Content.Server.GameObjects.EntitySystems /// Activates/Uses an object in control/possession of a user /// If the item has the IUse interface on one of its components we use the object in our hand /// - /// - /// public void UseInteraction(IEntity user, IEntity used) { var useMsg = new UseInHandMessage(user, used); RaiseEvent(useMsg); - if(useMsg.Handled) - return; - - List usables = used.GetAllComponents().ToList(); - - //Try to use item on any components which have the interface - for (var i = 0; i < usables.Count; i++) + if (useMsg.Handled) { - if (usables[i].UseEntity(new UseEntityEventArgs { User = user })) //If an attackby returns a status completion we finish our attack + return; + } + + var uses = used.GetAllComponents().ToList(); + + // Try to use item on any components which have the interface + foreach (var use in uses) + { + if (use.UseEntity(new UseEntityEventArgs {User = user})) { + // If a Use returns a status completion we finish our attack return; } } @@ -413,41 +439,44 @@ namespace Content.Server.GameObjects.EntitySystems /// Will have two behaviors, either "uses" the weapon at range on the entity if it is capable of accepting that action /// Or it will use the weapon itself on the position clicked, regardless of what was there /// - /// - /// - /// public void RangedInteraction(IEntity user, IEntity weapon, IEntity attacked, GridCoordinates clickLocation) { var rangedMsg = new RangedAttackMessage(user, weapon, attacked, clickLocation); RaiseEvent(rangedMsg); - if(rangedMsg.Handled) + if (rangedMsg.Handled) return; - List rangedusables = attacked.GetAllComponents().ToList(); - - //See if we have a ranged attack interaction - for (var i = 0; i < rangedusables.Count; i++) + var rangedAttackBys = attacked.GetAllComponents().ToList(); + var rangedAttackByEventArgs = new RangedAttackByEventArgs { - if (rangedusables[i].RangedAttackBy(new RangedAttackByEventArgs { User = user, Weapon = weapon, ClickLocation = clickLocation })) //If an attackby returns a status completion we finish our attack + User = user, Weapon = weapon, ClickLocation = clickLocation + }; + + // See if we have a ranged attack interaction + foreach (var t in rangedAttackBys) + { + if (t.RangedAttackBy(rangedAttackByEventArgs)) { + // If an AttackBy returns a status completion we finish our attack return; } } - if (weapon != null) + var afterAtkMsg = new AfterAttackMessage(user, weapon, attacked, clickLocation); + RaiseEvent(afterAtkMsg); + if (afterAtkMsg.Handled) + return; + + var afterAttacks = weapon.GetAllComponents().ToList(); + var afterAttackEventArgs = new AfterAttackEventArgs { - var afterAtkMsg = new AfterAttackMessage(user, weapon, attacked, clickLocation); - RaiseEvent(afterAtkMsg); - if (afterAtkMsg.Handled) - return; + User = user, ClickLocation = clickLocation, Attacked = attacked + }; - List afterattacks = weapon.GetAllComponents().ToList(); - - //See if we have a ranged attack interaction - for (var i = 0; i < afterattacks.Count; i++) - { - afterattacks[i].AfterAttack(new AfterAttackEventArgs { User = user, ClickLocation = clickLocation, Attacked = attacked }); - } + //See if we have a ranged attack interaction + foreach (var afterAttack in afterAttacks) + { + afterAttack.AfterAttack(afterAttackEventArgs); } } } @@ -455,6 +484,7 @@ namespace Content.Server.GameObjects.EntitySystems /// /// Raised when being clicked on or "attacked" by a user with an object in their hand /// + [PublicAPI] public class AttackByMessage : EntitySystemMessage { /// @@ -471,7 +501,7 @@ namespace Content.Server.GameObjects.EntitySystems /// Entity that the User attacked with. /// public IEntity ItemInHand { get; } - + /// /// Entity that was attacked. /// @@ -494,6 +524,7 @@ namespace Content.Server.GameObjects.EntitySystems /// /// Raised when being clicked on or "attacked" by a user with an empty hand. /// + [PublicAPI] public class AttackHandMessage : EntitySystemMessage { /// @@ -521,6 +552,7 @@ namespace Content.Server.GameObjects.EntitySystems /// /// Raised when being clicked by objects outside the range of direct use. /// + [PublicAPI] public class RangedAttackMessage : EntitySystemMessage { /// @@ -560,6 +592,7 @@ namespace Content.Server.GameObjects.EntitySystems /// /// Raised when clicking on another object and no attack event was handled. /// + [PublicAPI] public class AfterAttackMessage : EntitySystemMessage { /// @@ -599,6 +632,7 @@ namespace Content.Server.GameObjects.EntitySystems /// /// Raised when using the entity in your hands. /// + [PublicAPI] public class UseInHandMessage : EntitySystemMessage { /// @@ -626,6 +660,7 @@ namespace Content.Server.GameObjects.EntitySystems /// /// Raised when an entity is activated in the world. /// + [PublicAPI] public class ActivateInWorldMessage : EntitySystemMessage { /// diff --git a/Content.Server/GameObjects/EntitySystems/StorageSystem.cs b/Content.Server/GameObjects/EntitySystems/StorageSystem.cs index 1cbc59cd6c..cc54bf3d46 100644 --- a/Content.Server/GameObjects/EntitySystems/StorageSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/StorageSystem.cs @@ -81,7 +81,7 @@ namespace Content.Server.GameObjects.EntitySystems continue; var distanceSquared = (storagePos - attachedEntity.Transform.WorldPosition).LengthSquared; - if (distanceSquared > InteractionSystem.INTERACTION_RANGE_SQUARED) + if (distanceSquared > InteractionSystem.InteractionRangeSquared) { storageComp.UnsubscribeSession(session); } From b64643ecd67ae8c7914c823a5ab34c50e4a68900 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Thu, 16 May 2019 16:25:06 +0200 Subject: [PATCH 7/9] Adds tutorial, remap some buttons to be more in line with SS13. --- Content.Client/Content.Client.csproj | 1 + .../GameTicking/ClientGameTicker.cs | 10 +++ .../UserInterface/TutorialButton.cs | 61 +++++++++++++++++++ Resources/keybinds.yml | 4 +- 4 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 Content.Client/UserInterface/TutorialButton.cs diff --git a/Content.Client/Content.Client.csproj b/Content.Client/Content.Client.csproj index 71024b6d7d..d7632f5255 100644 --- a/Content.Client/Content.Client.csproj +++ b/Content.Client/Content.Client.csproj @@ -127,6 +127,7 @@ + diff --git a/Content.Client/GameTicking/ClientGameTicker.cs b/Content.Client/GameTicking/ClientGameTicker.cs index 72a9b3d42d..1ae9c5d814 100644 --- a/Content.Client/GameTicking/ClientGameTicker.cs +++ b/Content.Client/GameTicking/ClientGameTicker.cs @@ -44,6 +44,7 @@ namespace Content.Client.GameTicking [ViewVariables] private LobbyGui _lobby; [ViewVariables] private bool _gameStarted; [ViewVariables] private DateTime _startTime; + [ViewVariables] private TutorialButton _tutorialButton; public void Initialize() { @@ -170,6 +171,12 @@ namespace Content.Client.GameTicking _gameChat = null; } + if (_tutorialButton != null) + { + _tutorialButton.Dispose(); + _tutorialButton = null; + } + _tickerState = TickerState.InLobby; _lobby = new LobbyGui(_localization, _resourceCache); @@ -242,6 +249,9 @@ namespace Content.Client.GameTicking _gameChat = new ChatBox(); _userInterfaceManager.StateRoot.AddChild(_gameChat); _chatManager.SetChatBox(_gameChat); + _tutorialButton = new TutorialButton(); + _userInterfaceManager.StateRoot.AddChild(_tutorialButton); + _tutorialButton.SetAnchorAndMarginPreset(Control.LayoutPreset.BottomLeft, Control.LayoutPresetMode.MinSize, 50); _gameChat.DefaultChatFormat = "say \"{0}\""; } diff --git a/Content.Client/UserInterface/TutorialButton.cs b/Content.Client/UserInterface/TutorialButton.cs new file mode 100644 index 0000000000..1e72981a6c --- /dev/null +++ b/Content.Client/UserInterface/TutorialButton.cs @@ -0,0 +1,61 @@ +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using Robust.Shared.Utility; + +namespace Content.Client.UserInterface +{ + internal sealed class TutorialButton : Button + { + private const string TutorialContents = @"Hi and welcome to Space Station 14! + +This tutorial will assume that you know a bit about how SS13 plays. +It's mostly intended to lay out the controls and their differences from SS13. + +Just like in any game, WASD is movement. If that does not work, the server probably broke. + +Clicking on things ""interacts"" in some object-defined sense with it, with your active hand. + +X switches hands. Z uses the item in your hand. Q drops items. T focuses chat. C opens your inventory. + +New to SS14: You can press ""E"" to activate objects. This functions similarly to clicking with an empty hand most of the time: opens interfaces, etc. The difference is that it works even without an empty hand. No longer do you need to drop your tools to use a computer! + +You can talk in OOC by prefixing the message with [ or /ooc. + +If you are not on a QWERTY keyboard, the keys mentioned above are bound to the physical location on your keyboard, +not what letter they correspond to. For example on AZERTY movement is ZQSD, drop is A, W is activate in hand. + +If you have any feedback, questions, bug reports, etc..., do not be afraid to tell us! +You can ask on Discord or heck, just write it in OOC, we'll catch it. +"; + + + public TutorialButton() + { + OnPressed += OnOnPressed; + + Text = "Tutorial"; + } + + private void OnOnPressed(ButtonEventArgs obj) + { + _openTutorialWindow(); + } + + private void _openTutorialWindow() + { + var window = new SS14Window {Title = "Tutorial"}; + + var scrollContainer = new ScrollContainer(); + window.Contents.AddChild(scrollContainer); + + var label = new RichTextLabel(); + scrollContainer.AddChild(label); + + var message = new FormattedMessage(); + message.AddText(TutorialContents); + label.SetMessage(message); + + window.AddToScreen(); + } + } +} diff --git a/Resources/keybinds.yml b/Resources/keybinds.yml index 6dce1d0f63..b0b87c8a81 100644 --- a/Resources/keybinds.yml +++ b/Resources/keybinds.yml @@ -45,13 +45,13 @@ binds: key: MouseMiddle type: State - function: SwapHands - key: Tab + key: X type: State - function: Drop key: Q type: State - function: ActivateItemInHand - key: F + key: Z type: State - function: OpenCharacterMenu key: C From 916b1521a0d65da59ee0f16882cc9dada2571779 Mon Sep 17 00:00:00 2001 From: moneyl <8206401+Moneyl@users.noreply.github.com> Date: Sat, 18 May 2019 13:26:58 -0400 Subject: [PATCH 8/9] Fixes server crash when clicking empty hand with full hand --- .../GameObjects/Components/GUI/ServerHandsComponent.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Content.Server/GameObjects/Components/GUI/ServerHandsComponent.cs b/Content.Server/GameObjects/Components/GUI/ServerHandsComponent.cs index 3822f1d39a..d24586a3d0 100644 --- a/Content.Server/GameObjects/Components/GUI/ServerHandsComponent.cs +++ b/Content.Server/GameObjects/Components/GUI/ServerHandsComponent.cs @@ -462,8 +462,8 @@ namespace Content.Server.GameObjects var playerEntity = session.AttachedEntity; var used = GetActiveHand?.Owner; - if (playerEntity == Owner && used != null) - { + if (playerEntity == Owner && used != null && slot.ContainedEntity != null) + { var interactionSystem = _entitySystemManager.GetEntitySystem(); interactionSystem.Interaction(Owner, used, slot.ContainedEntity, GridCoordinates.Nullspace); From 7e30ffe0071881e9cfce7674dbb0662f68138d92 Mon Sep 17 00:00:00 2001 From: moneyl <8206401+Moneyl@users.noreply.github.com> Date: Sat, 18 May 2019 17:10:19 -0400 Subject: [PATCH 9/9] Fixes server crash due to null construction step ConstructionComponent.AttackBy tries calling TryProcessStep on the forward step of the construction stage, and if that fails it tries the backwards step. I many construction prototypes the backwards step is null for all stages and so clicking the construction ghost with an invalid step results in a crash due to the step passed to TryProcessStep being null. --- .../Components/Construction/ConstructionComponent.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Content.Server/GameObjects/Components/Construction/ConstructionComponent.cs b/Content.Server/GameObjects/Components/Construction/ConstructionComponent.cs index 18d8072b38..c6e8d3f656 100644 --- a/Content.Server/GameObjects/Components/Construction/ConstructionComponent.cs +++ b/Content.Server/GameObjects/Components/Construction/ConstructionComponent.cs @@ -93,6 +93,10 @@ namespace Content.Server.GameObjects.Components.Construction bool TryProcessStep(ConstructionStep step, IEntity slapped) { + if (step == null) + { + return false; + } var sound = IoCManager.Resolve().GetEntitySystem(); switch (step)