diff --git a/Content.Server/GameObjects/Components/Damage/DamageCommands.cs b/Content.Server/GameObjects/Components/Damage/DamageCommands.cs new file mode 100644 index 0000000000..5b4fe5b43a --- /dev/null +++ b/Content.Server/GameObjects/Components/Damage/DamageCommands.cs @@ -0,0 +1,214 @@ +#nullable enable +using System; +using System.Diagnostics.CodeAnalysis; +using Content.Server.GameObjects.Components.Atmos; +using Content.Shared.GameObjects.Components.Damage; +using Robust.Server.Interfaces.Console; +using Robust.Server.Interfaces.Player; +using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.IoC; + +namespace Content.Server.GameObjects.Components.Damage +{ + public abstract class DamageFlagCommand : IClientCommand + { + public abstract string Command { get; } + public abstract string Description { get; } + public abstract string Help { get; } + + public abstract void Execute(IConsoleShell shell, IPlayerSession? player, string[] args); + + public bool TryGetEntity( + IConsoleShell shell, + IPlayerSession? player, + string[] args, + bool adding, + [NotNullWhen(true)] out IEntity? entity, + out DamageFlag flag, + [NotNullWhen(true)] out IDamageableComponent? damageable) + { + entity = null; + flag = DamageFlag.None; + damageable = null; + + IEntity? parsedEntity; + DamageFlag parsedFlag; + IDamageableComponent? parsedDamageable; + + switch (args.Length) + { + case 1: + { + if (player == null) + { + shell.SendText(player, "An entity needs to be specified when the command isn't used by a player."); + return false; + } + + if (player.AttachedEntity == null) + { + shell.SendText(player, "An entity needs to be specified when you aren't attached to an entity."); + return false; + } + + if (!Enum.TryParse(args[0], true, out parsedFlag)) + { + shell.SendText(player, $"{args[0]} is not a valid damage flag."); + return false; + } + + parsedEntity = player.AttachedEntity; + flag = parsedFlag; + break; + } + case 2: + { + if (!EntityUid.TryParse(args[0], out var id)) + { + shell.SendText(player, $"{args[0]} isn't a valid entity id."); + return false; + } + + var entityManager = IoCManager.Resolve(); + if (!entityManager.TryGetEntity(id, out parsedEntity)) + { + shell.SendText(player, $"No entity found with id {id}."); + return false; + } + + if (!Enum.TryParse(args[1], true, out parsedFlag)) + { + shell.SendText(player, $"{args[1]} is not a valid damage flag."); + return false; + } + + break; + } + default: + shell.SendText(player, Help); + return false; + } + + if (!parsedEntity.TryGetComponent(out parsedDamageable)) + { + shell.SendText(player, $"Entity {parsedEntity.Name} doesn't have a {nameof(IDamageableComponent)}"); + return false; + } + + if (parsedDamageable.HasFlag(parsedFlag) && adding) + { + shell.SendText(player, $"Entity {parsedEntity.Name} already has damage flag {parsedFlag}."); + return false; + } + else if (!parsedDamageable.HasFlag(parsedFlag) && !adding) + { + shell.SendText(player, $"Entity {parsedEntity.Name} doesn't have damage flag {parsedFlag}."); + return false; + } + + entity = parsedEntity; + flag = parsedFlag; + damageable = parsedDamageable; + + return true; + } + } + + public class AddDamageFlagCommand : DamageFlagCommand + { + public override string Command => "adddamageflag"; + public override string Description => "Adds a damage flag to your entity or another."; + public override string Help => $"Usage: {Command} / {Command} "; + + public override void Execute(IConsoleShell shell, IPlayerSession? player, string[] args) + { + if (!TryGetEntity(shell, player, args, true, out var entity, out var flag, out var damageable)) + { + return; + } + + damageable.AddFlag(flag); + shell.SendText(player, $"Added damage flag {flag} to entity {entity.Name}"); + } + } + + public class RemoveDamageFlagCommand : DamageFlagCommand + { + public override string Command => "removedamageflag"; + public override string Description => "Removes a damage flag from your entity or another."; + public override string Help => $"Usage: {Command} / {Command} "; + + public override void Execute(IConsoleShell shell, IPlayerSession? player, string[] args) + { + if (!TryGetEntity(shell, player, args, false, out var entity, out var flag, out var damageable)) + { + return; + } + + damageable.RemoveFlag(flag); + shell.SendText(player, $"Removed damage flag {flag} from entity {entity.Name}"); + } + } + + public class GodModeCommand : IClientCommand + { + public string Command => "godmode"; + public string Description => "Makes your entity or another invulnerable to almost anything. May have irreversible changes."; + public string Help => $"Usage: {Command} / {Command} "; + public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args) + { + IEntity entity; + + switch (args.Length) + { + case 0: + if (player == null) + { + shell.SendText(player, "An entity needs to be specified when the command isn't used by a player."); + return; + } + + if (player.AttachedEntity == null) + { + shell.SendText(player, "An entity needs to be specified when you aren't attached to an entity."); + return; + } + + entity = player.AttachedEntity; + break; + case 1: + if (!EntityUid.TryParse(args[0], out var id)) + { + shell.SendText(player, $"{args[0]} isn't a valid entity id."); + return; + } + + var entityManager = IoCManager.Resolve(); + if (!entityManager.TryGetEntity(id, out var parsedEntity)) + { + shell.SendText(player, $"No entity found with id {id}."); + return; + } + + entity = parsedEntity; + break; + default: + shell.SendText(player, Help); + return; + } + + if (entity.HasComponent()) + { + entity.RemoveComponent(); + } + + if (entity.TryGetComponent(out IDamageableComponent? damageable)) + { + damageable.AddFlag(DamageFlag.Invulnerable); + } + + shell.SendText(player, $"Enabled godmode for entity {entity.Name}"); + } + } +} diff --git a/Content.Shared/GameObjects/Components/Damage/DamageFlag.cs b/Content.Shared/GameObjects/Components/Damage/DamageFlag.cs new file mode 100644 index 0000000000..537fed2391 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Damage/DamageFlag.cs @@ -0,0 +1,13 @@ +using System; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.Components.Damage +{ + [Flags] + [Serializable, NetSerializable] + public enum DamageFlag + { + None = 0, + Invulnerable = 1 << 0 + } +} diff --git a/Content.Shared/GameObjects/Components/Damage/DamageableComponent.cs b/Content.Shared/GameObjects/Components/Damage/DamageableComponent.cs index 8557e738df..7fdfeba0ac 100644 --- a/Content.Shared/GameObjects/Components/Damage/DamageableComponent.cs +++ b/Content.Shared/GameObjects/Components/Damage/DamageableComponent.cs @@ -27,6 +27,8 @@ namespace Content.Shared.GameObjects.Components.Damage private DamageState _currentDamageState; + private DamageFlag _flags; + public event Action? HealthChangedEvent; /// @@ -79,6 +81,8 @@ namespace Content.Shared.GameObjects.Components.Damage { EnterState(value); } + + Dirty(); } } @@ -88,6 +92,36 @@ namespace Content.Shared.GameObjects.Components.Damage public IReadOnlyDictionary DamageTypes => Damage.DamageTypes; + public DamageFlag Flags + { + get => _flags; + private set + { + if (_flags == value) + { + return; + } + + _flags = value; + Dirty(); + } + } + + public void AddFlag(DamageFlag flag) + { + Flags |= flag; + } + + public bool HasFlag(DamageFlag flag) + { + return Flags.HasFlag(flag); + } + + public void RemoveFlag(DamageFlag flag) + { + Flags &= ~flag; + } + public override void ExposeData(ObjectSerializer serializer) { base.ExposeData(serializer); @@ -104,6 +138,35 @@ namespace Content.Shared.GameObjects.Components.Damage t => DeadThreshold = t == -1 ? (int?) null : t, () => DeadThreshold ?? -1); + serializer.DataReadWriteFunction( + "flags", + new List(), + flags => + { + var result = DamageFlag.None; + + foreach (var flag in flags) + { + result |= flag; + } + + Flags = result; + }, + () => + { + var writeFlags = new List(); + + foreach (var flag in (DamageFlag[]) Enum.GetValues(typeof(DamageFlag))) + { + if ((Flags & flag) == flag) + { + writeFlags.Add(flag); + } + } + + return writeFlags; + }); + if (serializer.Reading) { // Doesn't write to file, TODO? @@ -151,6 +214,11 @@ namespace Content.Shared.GameObjects.Components.Damage IEntity? source = null, HealthChangeParams? extraParams = null) { + if (amount > 0 && HasFlag(DamageFlag.Invulnerable)) + { + return false; + } + if (Damage.SupportsDamageType(type)) { var finalDamage = amount; @@ -171,6 +239,11 @@ namespace Content.Shared.GameObjects.Components.Damage IEntity? source = null, HealthChangeParams? extraParams = null) { + if (amount > 0 && HasFlag(DamageFlag.Invulnerable)) + { + return false; + } + if (Damage.SupportsDamageClass(@class)) { var types = @class.ToTypes(); @@ -250,6 +323,11 @@ namespace Content.Shared.GameObjects.Components.Damage public bool SetDamage(DamageType type, int newValue, IEntity? source = null, HealthChangeParams? extraParams = null) { + if (newValue >= TotalDamage && HasFlag(DamageFlag.Invulnerable)) + { + return false; + } + if (Damage.SupportsDamageType(type)) { Damage.SetDamageValue(type, newValue); diff --git a/Content.Shared/GameObjects/Components/Damage/IDamageableComponent.cs b/Content.Shared/GameObjects/Components/Damage/IDamageableComponent.cs index 4290cf4ac2..8c6891953d 100644 --- a/Content.Shared/GameObjects/Components/Damage/IDamageableComponent.cs +++ b/Content.Shared/GameObjects/Components/Damage/IDamageableComponent.cs @@ -45,6 +45,30 @@ namespace Content.Shared.GameObjects.Components.Damage /// IReadOnlyDictionary DamageTypes { get; } + /// + /// The damage flags on this component. + /// + DamageFlag Flags { get; } + + /// + /// Adds a flag to this component. + /// + /// The flag to add. + void AddFlag(DamageFlag flag); + + /// + /// Checks whether or not this component has a specific flag. + /// + /// The flag to check for. + /// True if it has the flag, false otherwise. + bool HasFlag(DamageFlag flag); + + /// + /// Removes a flag from this component. + /// + /// The flag to remove. + void RemoveFlag(DamageFlag flag); + /// /// Gets the amount of damage of a type. /// diff --git a/Resources/Groups/groups.yml b/Resources/Groups/groups.yml index 85d071a009..a2bf42796c 100644 --- a/Resources/Groups/groups.yml +++ b/Resources/Groups/groups.yml @@ -102,6 +102,9 @@ - readyall - factions - signallink + - adddamageflag + - removedamageflag + - godmode CanViewVar: true CanAdminPlace: true CanAdminMenu: true @@ -197,6 +200,9 @@ - readyall - factions - signallink + - adddamageflag + - removedamageflag + - godmode CanViewVar: true CanAdminPlace: true CanScript: true