diff --git a/Content.Client/Decals/DecalPlacementSystem.cs b/Content.Client/Decals/DecalPlacementSystem.cs index daa40ec7f0..45da1c5547 100644 --- a/Content.Client/Decals/DecalPlacementSystem.cs +++ b/Content.Client/Decals/DecalPlacementSystem.cs @@ -183,7 +183,7 @@ public sealed class DecalPlacementSystem : EntitySystem } } -public sealed class PlaceDecalActionEvent : PerformWorldTargetActionEvent +public sealed class PlaceDecalActionEvent : WorldTargetActionEvent { [DataField("decalId", customTypeSerializer:typeof(PrototypeIdSerializer))] public string DecalId = string.Empty; diff --git a/Content.Client/Mapping/MappingSystem.cs b/Content.Client/Mapping/MappingSystem.cs index 3ce74d02d0..0828b59834 100644 --- a/Content.Client/Mapping/MappingSystem.cs +++ b/Content.Client/Mapping/MappingSystem.cs @@ -138,7 +138,7 @@ public sealed partial class MappingSystem : EntitySystem } } -public sealed class StartPlacementActionEvent : PerformActionEvent +public sealed class StartPlacementActionEvent : InstantActionEvent { [DataField("entityType")] public string? EntityType; diff --git a/Content.Server/Atmos/EntitySystems/GasTankSystem.cs b/Content.Server/Atmos/EntitySystems/GasTankSystem.cs index 981b18d917..b8412c14dd 100644 --- a/Content.Server/Atmos/EntitySystems/GasTankSystem.cs +++ b/Content.Server/Atmos/EntitySystems/GasTankSystem.cs @@ -20,7 +20,7 @@ namespace Content.Server.Atmos.EntitySystems { base.Initialize(); SubscribeLocalEvent(BeforeUiOpen); - SubscribeLocalEvent(OnGetActions); + SubscribeLocalEvent(OnGetActions); SubscribeLocalEvent(OnExamined); SubscribeLocalEvent(OnActionToggle); SubscribeLocalEvent(OnDropped); @@ -37,7 +37,7 @@ namespace Content.Server.Atmos.EntitySystems component.DisconnectFromInternals(args.User); } - private void OnGetActions(EntityUid uid, GasTankComponent component, GetActionsEvent args) + private void OnGetActions(EntityUid uid, GasTankComponent component, GetItemActionsEvent args) { args.Actions.Add(component.ToggleAction); } diff --git a/Content.Server/Ghost/Components/GhostComponent.cs b/Content.Server/Ghost/Components/GhostComponent.cs index 7daa0c98ee..e6ea4a4b60 100644 --- a/Content.Server/Ghost/Components/GhostComponent.cs +++ b/Content.Server/Ghost/Components/GhostComponent.cs @@ -29,5 +29,5 @@ namespace Content.Server.Ghost.Components }; } - public sealed class BooActionEvent : PerformActionEvent { } + public sealed class BooActionEvent : InstantActionEvent { } } diff --git a/Content.Server/Guardian/GuardianHostComponent.cs b/Content.Server/Guardian/GuardianHostComponent.cs index 2897606265..e76f0b4c60 100644 --- a/Content.Server/Guardian/GuardianHostComponent.cs +++ b/Content.Server/Guardian/GuardianHostComponent.cs @@ -36,5 +36,5 @@ namespace Content.Server.Guardian }; } - public sealed class GuardianToggleActionEvent : PerformActionEvent { }; + public sealed class GuardianToggleActionEvent : InstantActionEvent { }; } diff --git a/Content.Server/Light/EntitySystems/HandheldLightSystem.cs b/Content.Server/Light/EntitySystems/HandheldLightSystem.cs index e380a14896..4ce723592c 100644 --- a/Content.Server/Light/EntitySystems/HandheldLightSystem.cs +++ b/Content.Server/Light/EntitySystems/HandheldLightSystem.cs @@ -3,6 +3,7 @@ using Content.Server.Light.Components; using Content.Server.Popups; using Content.Server.PowerCell; using Content.Shared.Actions; +using Content.Shared.Actions.ActionTypes; using Content.Shared.Examine; using Content.Shared.Interaction; using Content.Shared.Light.Component; @@ -14,6 +15,7 @@ using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.GameStates; using Robust.Shared.Player; +using Robust.Shared.Prototypes; using Robust.Shared.Utility; namespace Content.Server.Light.EntitySystems @@ -24,6 +26,7 @@ namespace Content.Server.Light.EntitySystems [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly PowerCellSystem _powerCell = default!; [Dependency] private readonly ActionsSystem _actionSystem = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; // TODO: Ideally you'd be able to subscribe to power stuff to get events at certain percentages.. or something? // But for now this will be better anyway. @@ -42,13 +45,20 @@ namespace Content.Server.Light.EntitySystems SubscribeLocalEvent(OnActivate); - SubscribeLocalEvent(OnGetActions); + SubscribeLocalEvent(OnGetActions); SubscribeLocalEvent(OnToggleAction); } - private void OnGetActions(EntityUid uid, HandheldLightComponent component, GetActionsEvent args) + private void OnGetActions(EntityUid uid, HandheldLightComponent component, GetItemActionsEvent args) { - args.Actions.Add(component.ToggleAction); + if (component.ToggleAction == null + && _proto.TryIndex(component.ToggleActionId, out InstantActionPrototype? act)) + { + component.ToggleAction = new(act); + } + + if (component.ToggleAction != null) + args.Actions.Add(component.ToggleAction); } private void OnToggleAction(EntityUid uid, HandheldLightComponent component, ToggleActionEvent args) @@ -169,7 +179,8 @@ namespace Content.Server.Light.EntitySystems if (!component.Activated) return false; component.Activated = false; - _actionSystem.SetToggled(component.ToggleAction, false); + if (component.ToggleAction != null) + _actionSystem.SetToggled(component.ToggleAction, false); _activeLights.Remove(component); component.LastLevel = null; component.Dirty(EntityManager); @@ -202,7 +213,8 @@ namespace Content.Server.Light.EntitySystems } component.Activated = true; - _actionSystem.SetToggled(component.ToggleAction, true); + if (component.ToggleAction != null) + _actionSystem.SetToggled(component.ToggleAction, true); _activeLights.Add(component); component.LastLevel = GetLevel(component); Dirty(component); diff --git a/Content.Server/Light/EntitySystems/UnpoweredFlashlightSystem.cs b/Content.Server/Light/EntitySystems/UnpoweredFlashlightSystem.cs index 682e387da8..fa533f97fe 100644 --- a/Content.Server/Light/EntitySystems/UnpoweredFlashlightSystem.cs +++ b/Content.Server/Light/EntitySystems/UnpoweredFlashlightSystem.cs @@ -19,7 +19,7 @@ namespace Content.Server.Light.EntitySystems base.Initialize(); SubscribeLocalEvent>(AddToggleLightVerbs); - SubscribeLocalEvent(OnGetActions); + SubscribeLocalEvent(OnGetActions); SubscribeLocalEvent(OnToggleAction); } @@ -33,7 +33,7 @@ namespace Content.Server.Light.EntitySystems args.Handled = true; } - private void OnGetActions(EntityUid uid, UnpoweredFlashlightComponent component, GetActionsEvent args) + private void OnGetActions(EntityUid uid, UnpoweredFlashlightComponent component, GetItemActionsEvent args) { args.Actions.Add(component.ToggleAction); } diff --git a/Content.Server/Speech/Components/VocalComponent.cs b/Content.Server/Speech/Components/VocalComponent.cs index b1ea181d7b..0fbfe06e84 100644 --- a/Content.Server/Speech/Components/VocalComponent.cs +++ b/Content.Server/Speech/Components/VocalComponent.cs @@ -2,8 +2,7 @@ using Content.Shared.Actions; using Content.Shared.Actions.ActionTypes; using Content.Shared.Sound; using Robust.Shared.Audio; -using Robust.Shared.Utility; - +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; namespace Content.Server.Speech.Components; @@ -25,19 +24,16 @@ public sealed class VocalComponent : Component [DataField("audioParams")] public AudioParams AudioParams = AudioParams.Default.WithVolume(4f); + [DataField("wilhelmProbability")] + public float WilhelmProbability = 0.01f; + public const float Variation = 0.125f; - // Not using the in-build sound support for actions, given that the sound is modified non-prototype specific factors like gender. - [DataField("action")] - public InstantAction Action = new() - { - UseDelay = TimeSpan.FromSeconds(10), - Icon = new SpriteSpecifier.Texture(new ResourcePath("Interface/Actions/scream.png")), - Name = "action-name-scream", - Description = "AAAAAAAAAAAAAAAAAAAAAAAAA", - Event = new ScreamActionEvent(), - CheckCanInteract = false, // system checks a speech related action blocker. - }; + [DataField("actionId", customTypeSerializer:typeof(PrototypeIdSerializer))] + public string ActionId = "Scream"; + + [DataField("action")] // must be a data-field to properly save cooldown when saving game state. + public InstantAction? ScreamAction = null; } -public sealed class ScreamActionEvent : PerformActionEvent { }; +public sealed class ScreamActionEvent : InstantActionEvent { }; diff --git a/Content.Server/Speech/VocalSystem.cs b/Content.Server/Speech/VocalSystem.cs index 39ae115f4f..c54f441705 100644 --- a/Content.Server/Speech/VocalSystem.cs +++ b/Content.Server/Speech/VocalSystem.cs @@ -1,10 +1,12 @@ using Content.Server.Speech.Components; using Content.Shared.ActionBlocker; using Content.Shared.Actions; +using Content.Shared.Actions.ActionTypes; using Content.Shared.CharacterAppearance; using Content.Shared.CharacterAppearance.Components; using Robust.Shared.Audio; using Robust.Shared.Player; +using Robust.Shared.Prototypes; using Robust.Shared.Random; namespace Content.Server.Speech; @@ -18,6 +20,7 @@ namespace Content.Server.Speech; public sealed class VocalSystem : EntitySystem { [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly SharedActionsSystem _actions = default!; [Dependency] private readonly ActionBlockerSystem _blocker = default!; @@ -32,12 +35,20 @@ public sealed class VocalSystem : EntitySystem private void OnStartup(EntityUid uid, VocalComponent component, ComponentStartup args) { - _actions.AddAction(uid, component.Action, null); + if (component.ScreamAction == null + && _proto.TryIndex(component.ActionId, out InstantActionPrototype? act)) + { + component.ScreamAction = new(act); + } + + if (component.ScreamAction != null) + _actions.AddAction(uid, component.ScreamAction, null); } private void OnShutdown(EntityUid uid, VocalComponent component, ComponentShutdown args) { - _actions.RemoveAction(uid, component.Action); + if (component.ScreamAction != null) + _actions.RemoveAction(uid, component.ScreamAction); } private void OnActionPerform(EntityUid uid, VocalComponent component, ScreamActionEvent args) @@ -60,7 +71,7 @@ public sealed class VocalSystem : EntitySystem if (!TryComp(uid, out HumanoidAppearanceComponent? humanoid)) return false; - if (_random.Prob(.01f)) + if (_random.Prob(component.WilhelmProbability)) { SoundSystem.Play(Filter.Pvs(uid), component.Wilhelm.GetSound(), uid, component.AudioParams); return true; diff --git a/Content.Server/UserInterface/IntrinsicUISystem.cs b/Content.Server/UserInterface/IntrinsicUISystem.cs index bf9a0a6264..dde8522e27 100644 --- a/Content.Server/UserInterface/IntrinsicUISystem.cs +++ b/Content.Server/UserInterface/IntrinsicUISystem.cs @@ -70,7 +70,7 @@ public sealed class IntrinsicUISystem : EntitySystem } [UsedImplicitly] -public sealed class ToggleIntrinsicUIEvent : PerformActionEvent +public sealed class ToggleIntrinsicUIEvent : InstantActionEvent { [ViewVariables] public Enum? Key { get; set; } diff --git a/Content.Server/UserInterface/OpenUiActionEvent.cs b/Content.Server/UserInterface/OpenUiActionEvent.cs index e7ea6174b7..a6942f0e08 100644 --- a/Content.Server/UserInterface/OpenUiActionEvent.cs +++ b/Content.Server/UserInterface/OpenUiActionEvent.cs @@ -4,7 +4,7 @@ using Robust.Shared.Serialization; namespace Content.Server.UserInterface; -public sealed class OpenUiActionEvent : PerformActionEvent, ISerializationHooks +public sealed class OpenUiActionEvent : InstantActionEvent, ISerializationHooks { [ViewVariables] public Enum? Key { get; set; } diff --git a/Content.Shared/Actions/ActionEvents.cs b/Content.Shared/Actions/ActionEvents.cs index 0a28d470b1..716ca1ce2b 100644 --- a/Content.Shared/Actions/ActionEvents.cs +++ b/Content.Shared/Actions/ActionEvents.cs @@ -1,21 +1,44 @@ using Content.Shared.Actions.ActionTypes; +using Content.Shared.Hands; +using Content.Shared.Inventory; +using Content.Shared.Inventory.Events; using Robust.Shared.Map; using Robust.Shared.Serialization; namespace Content.Shared.Actions; -public sealed class GetActionsEvent : EntityEventArgs +/// +/// Event raised directed at items or clothing when they are equipped or held. In order for an item to grant actions some +/// system can subscribe to this event and add actions to the list. +/// +/// +/// Note that a system could also just manually add actions as a result of a or . This exists mostly as a convenience event, while also helping to keep +/// action-granting logic separate from general equipment behavior. +/// +public sealed class GetItemActionsEvent : EntityEventArgs { public SortedSet Actions = new(); + + /// + /// Slot flags for the inventory slot that this item got equipped to. Null if not in a slot (i.e., if equipped to hands). + /// + public SlotFlags? SlotFlags; + + /// + /// If true, the item was equipped to a users hands. + /// + public bool InHands => SlotFlags == null; + + public GetItemActionsEvent(SlotFlags? slotFlags = null) + { + SlotFlags = slotFlags; + } } /// -/// Event used to communicate with the client that the user wishes to perform some action. +/// Event used to communicate with the server that a client wishes to perform some action. /// -/// -/// Basically a wrapper for that the action system will validate before performing -/// (check cooldown, target, enabling-entity) -/// [Serializable, NetSerializable] public sealed class RequestPerformActionEvent : EntityEventArgs { @@ -41,21 +64,56 @@ public sealed class RequestPerformActionEvent : EntityEventArgs } } -[ImplicitDataDefinitionForInheritors] -public abstract class PerformActionEvent : HandledEntityEventArgs +/// +/// This is the type of event that gets raised when an is performed. The field is automatically filled out by the . +/// +/// +/// To define a new action for some system, you need to create an event that inherits from this class. +/// +public abstract class InstantActionEvent : BaseActionEvent { } + +/// +/// This is the type of event that gets raised when an is performed. The and fields will automatically be filled out by the . +/// +/// +/// To define a new action for some system, you need to create an event that inherits from this class. +/// +public abstract class EntityTargetActionEvent : BaseActionEvent { /// - /// The user performing the action + /// The entity that the user targeted. /// - public EntityUid Performer; -} - -public abstract class PerformEntityTargetActionEvent : PerformActionEvent -{ public EntityUid Target; } -public abstract class PerformWorldTargetActionEvent : PerformActionEvent +/// +/// This is the type of event that gets raised when an is performed. The and fields will automatically be filled out by the . +/// +/// +/// To define a new action for some system, you need to create an event that inherits from this class. +/// +public abstract class WorldTargetActionEvent : BaseActionEvent { + /// + /// The coordinates of the location that the user targeted. + /// public MapCoordinates Target; } + +/// +/// Base class for events that are raised when an action gets performed. This should not generally be used outside of the action +/// system. +/// +[ImplicitDataDefinitionForInheritors] +public abstract class BaseActionEvent : HandledEntityEventArgs +{ + /// + /// The user performing the action. + /// + public EntityUid Performer; +} diff --git a/Content.Shared/Actions/ActionTypes/ActionPrototypes.cs b/Content.Shared/Actions/ActionTypes/ActionPrototypes.cs new file mode 100644 index 0000000000..dbf60b64a2 --- /dev/null +++ b/Content.Shared/Actions/ActionTypes/ActionPrototypes.cs @@ -0,0 +1,56 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.Actions.ActionTypes; + +// These are just prototype definitions for actions. Allows actions to be defined once in yaml and re-used elsewhere. +// Note that you still need to create a new instance of each action to properly track the state (cooldown, toggled, +// enabled, etc). The prototypes should not be modified directly. +// +// If ever action states data is separated from the rest of the data, this might not be required +// anymore. + +[Prototype("worldTargetAction")] +public sealed class WorldTargetActionPrototype : WorldTargetAction, IPrototype +{ + [IdDataFieldAttribute] + public string ID { get; } = default!; + + // This is a shitty hack to get around the fact that action-prototypes should not in general be sever-exclusive + // prototypes, but some actions may need to use server-exclusive events, and there is no way to specify on a + // per-prototype basis whether the client should ignore it when validating yaml. + [DataField("serverEvent", serverOnly: true)] + public WorldTargetActionEvent? ServerEvent + { + get => Event; + set => Event = value; + } +} + +[Prototype("entityTargetAction")] +public sealed class EntityTargetActionPrototype : EntityTargetAction, IPrototype +{ + [IdDataFieldAttribute] + public string ID { get; } = default!; + + [DataField("serverEvent", serverOnly: true)] + public EntityTargetActionEvent? ServerEvent + { + get => Event; + set => Event = value; + } +} + +[Prototype("instantAction")] +public sealed class InstantActionPrototype : InstantAction, IPrototype +{ + [IdDataFieldAttribute] + public string ID { get; } = default!; + + [DataField("serverEvent", serverOnly: true)] + public InstantActionEvent? ServerEvent + { + get => Event; + set => Event = value; + } +} + diff --git a/Content.Shared/Actions/ActionTypes/InstantAction.cs b/Content.Shared/Actions/ActionTypes/InstantAction.cs index 28a8b607f4..ec1f6bba05 100644 --- a/Content.Shared/Actions/ActionTypes/InstantAction.cs +++ b/Content.Shared/Actions/ActionTypes/InstantAction.cs @@ -3,10 +3,9 @@ using Robust.Shared.Serialization; namespace Content.Shared.Actions.ActionTypes; /// -/// Instantaneous action with no extra targeting information. Will result in being raised. +/// Instantaneous action with no extra targeting information. Will result in being raised. /// [Serializable, NetSerializable] -[Friend(typeof(SharedActionsSystem))] [Virtual] public class InstantAction : ActionType { @@ -15,7 +14,7 @@ public class InstantAction : ActionType /// [DataField("event")] [NonSerialized] - public PerformActionEvent? Event; + public InstantActionEvent? Event; public InstantAction() { } public InstantAction(InstantAction toClone) diff --git a/Content.Shared/Actions/ActionTypes/TargetedAction.cs b/Content.Shared/Actions/ActionTypes/TargetedAction.cs index a5ecd581d6..0b535cce6d 100644 --- a/Content.Shared/Actions/ActionTypes/TargetedAction.cs +++ b/Content.Shared/Actions/ActionTypes/TargetedAction.cs @@ -67,10 +67,9 @@ public abstract class TargetedAction : ActionType } /// -/// Action that targets some entity. Will result in being raised. +/// Action that targets some entity. Will result in being raised. /// [Serializable, NetSerializable] -[Friend(typeof(SharedActionsSystem))] [Virtual] public class EntityTargetAction : TargetedAction { @@ -79,7 +78,7 @@ public class EntityTargetAction : TargetedAction /// [NonSerialized] [DataField("event")] - public PerformEntityTargetActionEvent? Event; + public EntityTargetActionEvent? Event; [DataField("whitelist")] public EntityWhitelist? Whitelist; @@ -115,10 +114,9 @@ public class EntityTargetAction : TargetedAction } /// -/// Action that targets some map coordinates. Will result in being raised. +/// Action that targets some map coordinates. Will result in being raised. /// [Serializable, NetSerializable] -[Friend(typeof(SharedActionsSystem))] [Virtual] public class WorldTargetAction : TargetedAction { @@ -127,7 +125,7 @@ public class WorldTargetAction : TargetedAction /// [DataField("event")] [NonSerialized] - public PerformWorldTargetActionEvent? Event; + public WorldTargetActionEvent? Event; public WorldTargetAction() { } public WorldTargetAction(WorldTargetAction toClone) diff --git a/Content.Shared/Actions/SharedActionsSystem.cs b/Content.Shared/Actions/SharedActionsSystem.cs index 05a8726777..57a4a98150 100644 --- a/Content.Shared/Actions/SharedActionsSystem.cs +++ b/Content.Shared/Actions/SharedActionsSystem.cs @@ -11,6 +11,7 @@ using Robust.Shared.Containers; using Robust.Shared.GameStates; using Robust.Shared.Map; using Robust.Shared.Player; +using Robust.Shared.Prototypes; using Robust.Shared.Timing; using System.Linq; @@ -99,7 +100,7 @@ public abstract class SharedActionsSystem : EntitySystem #region Execution /// /// When receiving a request to perform an action, this validates whether the action is allowed. If it is, it - /// will raise the relevant + /// will raise the relevant /// private void OnActionRequest(RequestPerformActionEvent ev, EntitySessionEventArgs args) { @@ -124,7 +125,7 @@ public abstract class SharedActionsSystem : EntitySystem if (act.Cooldown.HasValue && act.Cooldown.Value.End > curTime) return; - PerformActionEvent? performEvent = null; + BaseActionEvent? performEvent = null; // Validate request by checking action blockers and the like: var name = Loc.GetString(act.Name); @@ -273,7 +274,7 @@ public abstract class SharedActionsSystem : EntitySystem return _interactionSystem.InRangeUnobstructed(user, coords, range: action.Range); } - protected void PerformAction(ActionsComponent component, ActionType action, PerformActionEvent? actionEvent, TimeSpan curTime) + protected void PerformAction(ActionsComponent component, ActionType action, BaseActionEvent? actionEvent, TimeSpan curTime) { var handled = false; @@ -356,6 +357,13 @@ public abstract class SharedActionsSystem : EntitySystem /// The entity that enables these actions (e.g., flashlight). May be null (innate actions). public virtual void AddAction(EntityUid uid, ActionType action, EntityUid? provider, ActionsComponent? comp = null, bool dirty = true) { + // Because action classes have state data, e.g. cooldowns and uses-remaining, people should not be adding prototypes directly + if (action is IPrototype) + { + Logger.Error("Attempted to directly add a prototype action. You need to clone a prototype in order to use it."); + return; + } + comp ??= EnsureComp(uid); action.Provider = provider; action.AttachedEntity = comp.Owner; @@ -426,7 +434,7 @@ public abstract class SharedActionsSystem : EntitySystem #region EquipHandlers private void OnDidEquip(EntityUid uid, ActionsComponent component, DidEquipEvent args) { - var ev = new GetActionsEvent(); + var ev = new GetItemActionsEvent(args.SlotFlags); RaiseLocalEvent(args.Equipment, ev, false); if (ev.Actions.Count == 0) @@ -437,7 +445,7 @@ public abstract class SharedActionsSystem : EntitySystem private void OnHandEquipped(EntityUid uid, ActionsComponent component, DidEquipHandEvent args) { - var ev = new GetActionsEvent(); + var ev = new GetItemActionsEvent(); RaiseLocalEvent(args.Equipped, ev, false); if (ev.Actions.Count == 0) diff --git a/Content.Shared/Clothing/SharedMagbootsSystem.cs b/Content.Shared/Clothing/SharedMagbootsSystem.cs index e19fb2858d..75e8f2a8ca 100644 --- a/Content.Shared/Clothing/SharedMagbootsSystem.cs +++ b/Content.Shared/Clothing/SharedMagbootsSystem.cs @@ -13,7 +13,7 @@ public abstract class SharedMagbootsSystem : EntitySystem SubscribeLocalEvent>(AddToggleVerb); SubscribeLocalEvent(OnSlipAttempt); - SubscribeLocalEvent(OnGetActions); + SubscribeLocalEvent(OnGetActions); SubscribeLocalEvent(OnToggleAction); } @@ -35,7 +35,7 @@ public abstract class SharedMagbootsSystem : EntitySystem args.Cancel(); } - private void OnGetActions(EntityUid uid, SharedMagbootsComponent component, GetActionsEvent args) + private void OnGetActions(EntityUid uid, SharedMagbootsComponent component, GetItemActionsEvent args) { args.Actions.Add(component.ToggleAction); } diff --git a/Content.Shared/CombatMode/SharedCombatModeComponent.cs b/Content.Shared/CombatMode/SharedCombatModeComponent.cs index c1b47c1378..3fcf93a6c9 100644 --- a/Content.Shared/CombatMode/SharedCombatModeComponent.cs +++ b/Content.Shared/CombatMode/SharedCombatModeComponent.cs @@ -5,7 +5,7 @@ using Content.Shared.Sound; using Content.Shared.Targeting; using Robust.Shared.GameStates; using Robust.Shared.Serialization; -using Robust.Shared.Utility; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; namespace Content.Shared.CombatMode { @@ -27,22 +27,17 @@ namespace Content.Shared.CombatMode [DataField("disarmSuccessSound")] public readonly SoundSpecifier DisarmSuccessSound = new SoundPathSpecifier("/Audio/Effects/thudswoosh.ogg"); - // These are chonky default definitions for combat actions. But its a pain to add a yaml version of this for - // every entity that wants combat mode, especially given that they're currently all identical... so ummm.. yeah. - [DataField("disarmAction")] - public readonly EntityTargetAction DisarmAction = new(); + [DataField("disarmActionId", customTypeSerializer:typeof(PrototypeIdSerializer))] + public readonly string DisarmActionId = "Disarm"; + + [DataField("disarmAction")] // must be a data-field to properly save cooldown when saving game state. + public EntityTargetAction? DisarmAction; + + [DataField("combatToggleActionId", customTypeSerializer: typeof(PrototypeIdSerializer))] + public readonly string CombatToggleActionId = "CombatModeToggle"; [DataField("combatToggleAction")] - public readonly InstantAction CombatToggleAction = new() - { - Icon = new SpriteSpecifier.Texture(new ResourcePath("Interface/Actions/harmOff.png")), - IconOn = new SpriteSpecifier.Texture(new ResourcePath("Interface/Actions/harm.png")), - UserPopup = "action-popup-combat", - PopupToggleSuffix = "-disabling", - Name = "action-name-combat", - Description = "action-description-combat", - Event = new ToggleCombatActionEvent(), - }; + public InstantAction? CombatToggleAction; [ViewVariables(VVAccess.ReadWrite)] public virtual bool IsInCombatMode @@ -52,7 +47,8 @@ namespace Content.Shared.CombatMode { if (_isInCombatMode == value) return; _isInCombatMode = value; - EntitySystem.Get().SetToggled(CombatToggleAction, _isInCombatMode); + if (CombatToggleAction != null) + EntitySystem.Get().SetToggled(CombatToggleAction, _isInCombatMode); Dirty(); // Regenerate physics contacts -> Can probably just selectively check diff --git a/Content.Shared/CombatMode/SharedCombatModeSystem.cs b/Content.Shared/CombatMode/SharedCombatModeSystem.cs index 3092314847..af4b1d91b6 100644 --- a/Content.Shared/CombatMode/SharedCombatModeSystem.cs +++ b/Content.Shared/CombatMode/SharedCombatModeSystem.cs @@ -1,10 +1,13 @@ using Content.Shared.Actions; +using Content.Shared.Actions.ActionTypes; +using Robust.Shared.Prototypes; namespace Content.Shared.CombatMode { public abstract class SharedCombatModeSystem : EntitySystem { [Dependency] private readonly SharedActionsSystem _actionsSystem = default!; + [Dependency] private readonly IPrototypeManager _protoMan = default!; public override void Initialize() { @@ -20,14 +23,32 @@ namespace Content.Shared.CombatMode private void OnStartup(EntityUid uid, SharedCombatModeComponent component, ComponentStartup args) { - _actionsSystem.AddAction(uid, component.CombatToggleAction, null); - _actionsSystem.AddAction(uid, component.DisarmAction, null); + if (component.CombatToggleAction == null + && _protoMan.TryIndex(component.CombatToggleActionId, out InstantActionPrototype? toggleProto)) + { + component.CombatToggleAction = new(toggleProto); + } + + if (component.CombatToggleAction != null) + _actionsSystem.AddAction(uid, component.CombatToggleAction, null); + + if (component.DisarmAction == null + && _protoMan.TryIndex(component.DisarmActionId, out EntityTargetActionPrototype? disarmProto)) + { + component.DisarmAction = new(disarmProto); + } + + if (component.DisarmAction != null) + _actionsSystem.AddAction(uid, component.DisarmAction, null); } private void OnShutdown(EntityUid uid, SharedCombatModeComponent component, ComponentShutdown args) { - _actionsSystem.RemoveAction(uid, component.CombatToggleAction); - _actionsSystem.RemoveAction(uid, component.DisarmAction); + if (component.CombatToggleAction != null) + _actionsSystem.RemoveAction(uid, component.CombatToggleAction); + + if (component.DisarmAction != null) + _actionsSystem.RemoveAction(uid, component.DisarmAction); } private void OnActionPerform(EntityUid uid, SharedCombatModeComponent component, ToggleCombatActionEvent args) @@ -52,6 +73,6 @@ namespace Content.Shared.CombatMode } } - public sealed class ToggleCombatActionEvent : PerformActionEvent { } - public sealed class DisarmActionEvent : PerformEntityTargetActionEvent { } + public sealed class ToggleCombatActionEvent : InstantActionEvent { } + public sealed class DisarmActionEvent : EntityTargetActionEvent { } } diff --git a/Content.Shared/Inventory/Events/EquippedEvents.cs b/Content.Shared/Inventory/Events/EquippedEvents.cs index 4d0859ab30..718b5c9683 100644 --- a/Content.Shared/Inventory/Events/EquippedEvents.cs +++ b/Content.Shared/Inventory/Events/EquippedEvents.cs @@ -1,4 +1,4 @@ -using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects; namespace Content.Shared.Inventory.Events; diff --git a/Content.Shared/Light/Component/SharedHandheldLightComponent.cs b/Content.Shared/Light/Component/SharedHandheldLightComponent.cs index 0dbf940465..c172e63fda 100644 --- a/Content.Shared/Light/Component/SharedHandheldLightComponent.cs +++ b/Content.Shared/Light/Component/SharedHandheldLightComponent.cs @@ -1,14 +1,18 @@ using Content.Shared.Actions.ActionTypes; using Robust.Shared.GameStates; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; namespace Content.Shared.Light.Component { [NetworkedComponent] public abstract class SharedHandheldLightComponent : Robust.Shared.GameObjects.Component { - [DataField("toggleAction", required: true)] - public InstantAction ToggleAction = new(); + [DataField("toggleActionId", customTypeSerializer:typeof(PrototypeIdSerializer))] + public string ToggleActionId = "ToggleLight"; + + [DataField("toggleAction")] + public InstantAction? ToggleAction; public const int StatusLevels = 6; diff --git a/Content.Shared/Toggleable/ToggleActionEvent.cs b/Content.Shared/Toggleable/ToggleActionEvent.cs index 31055b6c3f..6346ab9d61 100644 --- a/Content.Shared/Toggleable/ToggleActionEvent.cs +++ b/Content.Shared/Toggleable/ToggleActionEvent.cs @@ -5,4 +5,4 @@ namespace Content.Shared.Toggleable; /// /// Generic action-event for toggle-able components. /// -public sealed class ToggleActionEvent : PerformActionEvent { } +public sealed class ToggleActionEvent : InstantActionEvent { } diff --git a/Resources/Prototypes/Actions/types.yml b/Resources/Prototypes/Actions/types.yml new file mode 100644 index 0000000000..cd019bdabe --- /dev/null +++ b/Resources/Prototypes/Actions/types.yml @@ -0,0 +1,42 @@ +- type: instantAction + id: Scream + useDelay: 10 + icon: Interface/Actions/scream.png + name: action-name-scream + description: AAAAAAAAAAAAAAAAAAAAAAAAA + serverEvent: !type:ScreamActionEvent + checkCanInteract: false + +- type: instantAction + id: ToggleLight + name: action-name-toggle-light + description: action-description-toggle-light + icon: Objects/Tools/flashlight.rsi/flashlight.png + iconOn: Objects/Tools/flashlight.rsi/flashlight-on.png + event: !type:ToggleActionEvent + +- type: entityTargetAction + id: Disarm + name: action-name-disarm + description: action-description-disarm + icon: Interface/Actions/disarmOff.png + iconOn: Interface/Actions/disarm.png + repeat: true + useDelay: 1.5 + interactOnMiss: true + event: !type:DisarmActionEvent + canTargetSelf: false + whitelist: + components: + - Hands + - StatusEffects + +- type: instantAction + id: CombatModeToggle + name: action-name-combat + description: action-description-combat + icon: Interface/Actions/harmOff.png + iconOn: Interface/Actions/harm.png + userPopup: action-popup-combat + popupToggleSuffix: -disabling + event: !type:ToggleCombatActionEvent diff --git a/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml b/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml index d9abf97fef..f1127e527d 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml @@ -98,12 +98,6 @@ - type: FlashLightVisualizer - type: HandheldLight addPrefix: true - toggleAction: - name: action-name-toggle-light - description: action-description-toggle-light - icon: Objects/Tools/flashlight.rsi/flashlight.png - iconOn: Objects/Tools/flashlight.rsi/flashlight-on.png - event: !type:ToggleActionEvent - type: PowerCellSlot cellSlot: startingItem: PowerCellHardsuitHelmet # self recharging diff --git a/Resources/Prototypes/Entities/Clothing/Head/hardhats.yml b/Resources/Prototypes/Entities/Clothing/Head/hardhats.yml index edbcef7b4c..d1dec87e70 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/hardhats.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/hardhats.yml @@ -21,12 +21,6 @@ - type: FlashLightVisualizer - type: HandheldLight addPrefix: true - toggleAction: - name: action-name-toggle-light - description: action-description-toggle-light - icon: Objects/Tools/flashlight.rsi/flashlight.png - iconOn: Objects/Tools/flashlight.rsi/flashlight-on.png - event: !type:ToggleActionEvent - type: PowerCellSlot cellSlot: startingItem: PowerCellSmallHigh diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 1d889a34ba..b6ebc82bfd 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -705,6 +705,7 @@ # mice are gender neutral who cares maleScream: /Audio/Animals/mouse_squeak.ogg femaleScream: /Audio/Animals/mouse_squeak.ogg + wilhelmProbability: 0.001 # TODO: Remove CombatMode when Prototype Composition is added - type: CombatMode combatToggleAction: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml b/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml index 9a7e8e0a5b..328591e26f 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml @@ -95,20 +95,6 @@ 100: !type:DeadMobState {} - type: HeatResistance - type: CombatMode - disarmAction: - name: action-name-disarm - description: action-description-disarm - icon: Interface/Actions/disarmOff.png - iconOn: Interface/Actions/disarm.png - repeat: true - useDelay: 1.5 - interactOnMiss: true - event: !type:DisarmActionEvent - canTargetSelf: false - whitelist: - components: - - Hands - - StatusEffects - type: Internals - type: StatusEffects allowed: diff --git a/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml b/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml index a3d84d1494..e177170e64 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml @@ -12,20 +12,6 @@ - type: Hands - type: DoAfter - type: CombatMode - disarmAction: - name: action-name-disarm - description: action-description-disarm - icon: Interface/Actions/disarmOff.png - iconOn: Interface/Actions/disarm.png - repeat: true - useDelay: 1.5 - interactOnMiss: true - event: !type:DisarmActionEvent - canTargetSelf: false - whitelist: - components: - - Hands - - StatusEffects - type: Actions - type: PlayerInputMover - type: MovementSpeedModifier diff --git a/Resources/Prototypes/Entities/Mobs/Player/guardian.yml b/Resources/Prototypes/Entities/Mobs/Player/guardian.yml index c840da2d26..ea5394dbab 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/guardian.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/guardian.yml @@ -53,20 +53,6 @@ 0: !type:NormalMobState {} - type: HeatResistance - type: CombatMode - disarmAction: - name: action-name-disarm - description: action-description-disarm - icon: Interface/Actions/disarmOff.png - iconOn: Interface/Actions/disarm.png - repeat: true - useDelay: 1.5 - interactOnMiss: true - event: !type:DisarmActionEvent - canTargetSelf: false - whitelist: - components: - - Hands - - StatusEffects - type: Internals - type: Examiner - type: Speech diff --git a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml index 1e8c26e05c..d71b70da50 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml @@ -228,17 +228,3 @@ normal: onestar_boss dead: onestar_boss_wrecked - type: CombatMode - disarmAction: - name: action-name-disarm - description: action-description-disarm - icon: Interface/Actions/disarmOff.png - iconOn: Interface/Actions/disarm.png - repeat: true - useDelay: 1.5 - interactOnMiss: true - event: !type:DisarmActionEvent - canTargetSelf: false - whitelist: - components: - - Hands - - StatusEffects diff --git a/Resources/Prototypes/Entities/Mobs/Species/human.yml b/Resources/Prototypes/Entities/Mobs/Species/human.yml index 7beef8e08a..d057ad619c 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/human.yml @@ -258,20 +258,6 @@ Burn: sprite: Mobs/Effects/burn_damage.rsi - type: CombatMode - disarmAction: - name: action-name-disarm - description: action-description-disarm - icon: Interface/Actions/disarmOff.png - iconOn: Interface/Actions/disarm.png - repeat: true - useDelay: 1.5 - interactOnMiss: true - event: !type:DisarmActionEvent - canTargetSelf: false - whitelist: - components: - - Hands - - StatusEffects - type: Climbing - type: Cuffable - type: CharacterInfo diff --git a/Resources/Prototypes/Entities/Objects/Misc/fluff_lights.yml b/Resources/Prototypes/Entities/Objects/Misc/fluff_lights.yml index 91dec0beb1..6d8162c14e 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/fluff_lights.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/fluff_lights.yml @@ -6,12 +6,6 @@ components: - type: HandheldLight addPrefix: true - toggleAction: - name: action-name-toggle-light - description: action-description-toggle-light - icon: Objects/Tools/flashlight.rsi/flashlight.png - iconOn: Objects/Tools/flashlight.rsi/flashlight-on.png - event: !type:ToggleActionEvent - type: PowerCellSlot - type: Sprite sprite: Objects/Misc/Lights/lights.rsi diff --git a/Resources/Prototypes/Entities/Objects/Tools/flashlights.yml b/Resources/Prototypes/Entities/Objects/Tools/flashlights.yml index 0583ab1326..f509d799c7 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/flashlights.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/flashlights.yml @@ -17,12 +17,6 @@ right: - state: inhand-right-light shader: unshaded - toggleAction: - name: action-name-toggle-light - description: action-description-toggle-light - icon: Objects/Tools/flashlight.rsi/flashlight.png - iconOn: Objects/Tools/flashlight.rsi/flashlight-on.png - event: !type:ToggleActionEvent - type: PowerCellSlot cellSlot: startingItem: PowerCellSmallHigh diff --git a/Resources/Prototypes/Entities/Objects/Tools/lantern.yml b/Resources/Prototypes/Entities/Objects/Tools/lantern.yml index 67ea2ae2aa..c3b2c1c457 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/lantern.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/lantern.yml @@ -6,12 +6,6 @@ components: - type: HandheldLight addPrefix: true - toggleAction: - name: action-name-toggle-light - description: action-description-toggle-light - icon: Objects/Tools/flashlight.rsi/flashlight.png - iconOn: Objects/Tools/flashlight.rsi/flashlight-on.png - event: !type:ToggleActionEvent - type: Sprite sprite: Objects/Tools/lantern.rsi layers: