From 3503cb52d28eb44a9c3a1a18b13d1a82e8110d66 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 10 Oct 2025 13:45:48 -0600 Subject: [PATCH] Refactor Crayons to use shared charges system and autonetworking. Adds auto recharging crayon. (#40575) * Added special crayon with infinite charges for borg usage. * Use battery system to manage charges. * Reverted extra changes * Set charge on init * removed init assignment * Added comments to crayoncomponent * tweaked comments * Working with the new charges component, but at what cost? * Remvoed extra field * Apply suggestion from @slarticodefast Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Apply suggestion from @slarticodefast Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Apply suggestion from @slarticodefast Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Apply suggestion from @slarticodefast Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Fix renamed variables and descriptions in comments * Variable naming, comment cleanup and autonetworking. * Fix for test case, modified on init * Cleaned up/merged charges logic * review --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> --- Content.Client/Crayon/CrayonComponent.cs | 14 --- Content.Client/Crayon/CrayonSystem.cs | 49 +++----- Content.Server/Crayon/CrayonComponent.cs | 28 ----- Content.Server/Crayon/CrayonSystem.cs | 48 +++---- Content.Shared/Crayon/CrayonComponent.cs | 119 ++++++++++++++++++ .../Crayon/SharedCrayonComponent.cs | 113 ----------------- .../Entities/Objects/Fun/crayons.yml | 17 ++- 7 files changed, 169 insertions(+), 219 deletions(-) delete mode 100644 Content.Client/Crayon/CrayonComponent.cs delete mode 100644 Content.Server/Crayon/CrayonComponent.cs create mode 100644 Content.Shared/Crayon/CrayonComponent.cs delete mode 100644 Content.Shared/Crayon/SharedCrayonComponent.cs diff --git a/Content.Client/Crayon/CrayonComponent.cs b/Content.Client/Crayon/CrayonComponent.cs deleted file mode 100644 index 5729c616c2..0000000000 --- a/Content.Client/Crayon/CrayonComponent.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Content.Shared.Crayon; -using Robust.Shared.GameObjects; -using Robust.Shared.ViewVariables; - -namespace Content.Client.Crayon -{ - [RegisterComponent] - public sealed partial class CrayonComponent : SharedCrayonComponent - { - [ViewVariables(VVAccess.ReadWrite)] public bool UIUpdateNeeded; - [ViewVariables] public int Charges { get; set; } - [ViewVariables] public int Capacity { get; set; } - } -} diff --git a/Content.Client/Crayon/CrayonSystem.cs b/Content.Client/Crayon/CrayonSystem.cs index dc03979481..e990263bf3 100644 --- a/Content.Client/Crayon/CrayonSystem.cs +++ b/Content.Client/Crayon/CrayonSystem.cs @@ -1,67 +1,52 @@ using Content.Client.Items; using Content.Client.Message; using Content.Client.Stylesheets; +using Content.Shared.Charges.Components; +using Content.Shared.Charges.Systems; using Content.Shared.Crayon; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; -using Robust.Shared.GameObjects; -using Robust.Shared.GameStates; -using Robust.Shared.Localization; using Robust.Shared.Timing; namespace Content.Client.Crayon; public sealed class CrayonSystem : SharedCrayonSystem { - // Didn't do in shared because I don't think most of the server stuff can be predicted. + [Dependency] private readonly SharedChargesSystem _charges = default!; + [Dependency] private readonly EntityManager _entityManager = default!; + public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnCrayonHandleState); - Subs.ItemStatus(ent => new StatusControl(ent)); - } - private static void OnCrayonHandleState(EntityUid uid, CrayonComponent component, ref ComponentHandleState args) - { - if (args.Current is not CrayonComponentState state) return; - - component.Color = state.Color; - component.SelectedState = state.State; - component.Charges = state.Charges; - component.Capacity = state.Capacity; - - component.UIUpdateNeeded = true; + Subs.ItemStatus(ent => new StatusControl(ent, _charges, _entityManager)); } private sealed class StatusControl : Control { - private readonly CrayonComponent _parent; + private readonly Entity _crayon; + private readonly SharedChargesSystem _charges; private readonly RichTextLabel _label; + private readonly int _capacity; - public StatusControl(CrayonComponent parent) + public StatusControl(Entity crayon, SharedChargesSystem charges, EntityManager entityManage) { - _parent = parent; + _crayon = crayon; + _charges = charges; + _capacity = entityManage.GetComponent(_crayon.Owner).MaxCharges; _label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } }; AddChild(_label); - - parent.UIUpdateNeeded = true; } protected override void FrameUpdate(FrameEventArgs args) { base.FrameUpdate(args); - if (!_parent.UIUpdateNeeded) - { - return; - } - - _parent.UIUpdateNeeded = false; _label.SetMarkup(Robust.Shared.Localization.Loc.GetString("crayon-drawing-label", - ("color",_parent.Color), - ("state",_parent.SelectedState), - ("charges", _parent.Charges), - ("capacity",_parent.Capacity))); + ("color",_crayon.Comp.Color), + ("state",_crayon.Comp.SelectedState), + ("charges", _charges.GetCurrentCharges(_crayon.Owner)), + ("capacity", _capacity))); } } } diff --git a/Content.Server/Crayon/CrayonComponent.cs b/Content.Server/Crayon/CrayonComponent.cs deleted file mode 100644 index df20681938..0000000000 --- a/Content.Server/Crayon/CrayonComponent.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Content.Server.UserInterface; -using Content.Shared.Crayon; -using Robust.Server.GameObjects; -using Robust.Shared.Audio; - -namespace Content.Server.Crayon -{ - [RegisterComponent] - public sealed partial class CrayonComponent : SharedCrayonComponent - { - [DataField("useSound")] public SoundSpecifier? UseSound; - - [ViewVariables(VVAccess.ReadWrite)] - [DataField("selectableColor")] - public bool SelectableColor { get; set; } - - [ViewVariables(VVAccess.ReadWrite)] - public int Charges { get; set; } - - [ViewVariables(VVAccess.ReadWrite)] - [DataField("capacity")] - public int Capacity { get; set; } = 30; - - [ViewVariables(VVAccess.ReadWrite)] - [DataField("deleteEmpty")] - public bool DeleteEmpty = true; - } -} diff --git a/Content.Server/Crayon/CrayonSystem.cs b/Content.Server/Crayon/CrayonSystem.cs index f3abc2bf7a..07b580fba5 100644 --- a/Content.Server/Crayon/CrayonSystem.cs +++ b/Content.Server/Crayon/CrayonSystem.cs @@ -3,6 +3,7 @@ using System.Numerics; using Content.Server.Administration.Logs; using Content.Server.Decals; using Content.Server.Popups; +using Content.Shared.Charges.Systems; using Content.Shared.Crayon; using Content.Shared.Database; using Content.Shared.Decals; @@ -12,7 +13,6 @@ using Content.Shared.Nutrition.EntitySystems; using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; -using Robust.Shared.GameStates; using Robust.Shared.Prototypes; namespace Content.Server.Crayon; @@ -24,23 +24,27 @@ public sealed class CrayonSystem : SharedCrayonSystem [Dependency] private readonly DecalSystem _decals = default!; [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedChargesSystem _charges = default!; [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnCrayonInit); + + SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnCrayonBoundUI); SubscribeLocalEvent(OnCrayonBoundUIColor); SubscribeLocalEvent(OnCrayonUse, before: new[] { typeof(FoodSystem) }); SubscribeLocalEvent(OnCrayonAfterInteract, after: new[] { typeof(FoodSystem) }); SubscribeLocalEvent(OnCrayonDropped); - SubscribeLocalEvent(OnCrayonGetState); } - private static void OnCrayonGetState(EntityUid uid, CrayonComponent component, ref ComponentGetState args) + private void OnMapInit(Entity ent, ref MapInitEvent args) { - args.State = new CrayonComponentState(component.Color, component.SelectedState, component.Charges, component.Capacity); + // Get the first one from the catalog and set it as default + var decal = _prototypeManager.EnumeratePrototypes().FirstOrDefault(x => x.Tags.Contains("crayon")); + ent.Comp.SelectedState = decal?.ID ?? string.Empty; + Dirty(ent); } private void OnCrayonAfterInteract(EntityUid uid, CrayonComponent component, AfterInteractEvent args) @@ -48,7 +52,7 @@ public sealed class CrayonSystem : SharedCrayonSystem if (args.Handled || !args.CanReach) return; - if (component.Charges <= 0) + if (_charges.IsEmpty(uid)) { if (component.DeleteEmpty) UseUpCrayon(uid, args.User); @@ -72,17 +76,15 @@ public sealed class CrayonSystem : SharedCrayonSystem if (component.UseSound != null) _audio.PlayPvs(component.UseSound, uid, AudioParams.Default.WithVariation(0.125f)); - // Decrease "Ammo" - component.Charges--; - Dirty(uid, component); + _charges.TryUseCharge(uid); _adminLogger.Add(LogType.CrayonDraw, LogImpact.Low, $"{ToPrettyString(args.User):user} drew a {component.Color:color} {component.SelectedState}"); args.Handled = true; - if (component.DeleteEmpty && component.Charges <= 0) + if (component.DeleteEmpty && _charges.IsEmpty(uid)) UseUpCrayon(uid, args.User); else - _uiSystem.ServerSendUiMessage(uid, SharedCrayonComponent.CrayonUiKey.Key, new CrayonUsedMessage(component.SelectedState)); + _uiSystem.ServerSendUiMessage(uid, CrayonUiKey.Key, new CrayonUsedMessage(component.SelectedState)); } private void OnCrayonUse(EntityUid uid, CrayonComponent component, UseInHandEvent args) @@ -91,14 +93,12 @@ public sealed class CrayonSystem : SharedCrayonSystem if (args.Handled) return; - if (!_uiSystem.HasUi(uid, SharedCrayonComponent.CrayonUiKey.Key)) - { + if (!_uiSystem.HasUi(uid, CrayonUiKey.Key)) return; - } - _uiSystem.TryToggleUi(uid, SharedCrayonComponent.CrayonUiKey.Key, args.User); + _uiSystem.TryToggleUi(uid, CrayonUiKey.Key, args.User); - _uiSystem.SetUiState(uid, SharedCrayonComponent.CrayonUiKey.Key, new CrayonBoundUserInterfaceState(component.SelectedState, component.SelectableColor, component.Color)); + _uiSystem.SetUiState(uid, CrayonUiKey.Key, new CrayonBoundUserInterfaceState(component.SelectedState, component.SelectableColor, component.Color)); args.Handled = true; } @@ -109,35 +109,23 @@ public sealed class CrayonSystem : SharedCrayonSystem return; component.SelectedState = args.State; - Dirty(uid, component); } private void OnCrayonBoundUIColor(EntityUid uid, CrayonComponent component, CrayonColorMessage args) { - // you still need to ensure that the given color is a valid color + // Ensure that the given color can be changed or already matches if (!component.SelectableColor || args.Color == component.Color) return; component.Color = args.Color; Dirty(uid, component); - - } - - private void OnCrayonInit(EntityUid uid, CrayonComponent component, ComponentInit args) - { - component.Charges = component.Capacity; - - // Get the first one from the catalog and set it as default - var decal = _prototypeManager.EnumeratePrototypes().FirstOrDefault(x => x.Tags.Contains("crayon")); - component.SelectedState = decal?.ID ?? string.Empty; - Dirty(uid, component); } private void OnCrayonDropped(EntityUid uid, CrayonComponent component, DroppedEvent args) { // TODO: Use the existing event. - _uiSystem.CloseUi(uid, SharedCrayonComponent.CrayonUiKey.Key, args.User); + _uiSystem.CloseUi(uid, CrayonUiKey.Key, args.User); } private void UseUpCrayon(EntityUid uid, EntityUid user) diff --git a/Content.Shared/Crayon/CrayonComponent.cs b/Content.Shared/Crayon/CrayonComponent.cs new file mode 100644 index 0000000000..c772b43d00 --- /dev/null +++ b/Content.Shared/Crayon/CrayonComponent.cs @@ -0,0 +1,119 @@ +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared.Crayon; + +/// +/// Component holding the state of a crayon-like component +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(SharedCrayonSystem))] +public sealed partial class CrayonComponent : Component +{ + /// + /// The ID of currently selected decal prototype that will be placed when the crayon is used. + /// + [DataField, AutoNetworkedField] + public string SelectedState; + + /// + /// Color with which the crayon will draw. + /// + [DataField, AutoNetworkedField] + public Color Color; + + /// + /// Play a sound when drawing if specified. + /// + [DataField] + public SoundSpecifier? UseSound; + + /// + /// Can the color can be changed? + /// + [DataField, AutoNetworkedField] + public bool SelectableColor; + + /// + /// Should the crayon be deleted when all charges are consumed? + /// + [DataField, AutoNetworkedField] + public bool DeleteEmpty = true; +} + +/// +/// Opens the crayon window for decal and color selection. +/// +[Serializable, NetSerializable] +public enum CrayonUiKey : byte +{ + Key, +} + +/// +/// Used by the client to notify the server about the selected decal ID +/// +[Serializable, NetSerializable] +public sealed class CrayonSelectMessage : BoundUserInterfaceMessage +{ + public readonly string State; + public CrayonSelectMessage(string selected) + { + State = selected; + } +} + +/// +/// Sets the color of the crayon, used by Rainbow Crayon +/// +[Serializable, NetSerializable] +public sealed class CrayonColorMessage : BoundUserInterfaceMessage +{ + public readonly Color Color; + public CrayonColorMessage(Color color) + { + Color = color; + } +} + +/// +/// Server to CLIENT. Notifies the BUI that a decal with given ID has been drawn. +/// Allows the client UI to advance forward in the client-only ephemeral queue, +/// preventing the crayon from becoming a magic text storage device. +/// +[Serializable, NetSerializable] +public sealed class CrayonUsedMessage : BoundUserInterfaceMessage +{ + public readonly string DrawnDecal; + + public CrayonUsedMessage(string drawn) + { + DrawnDecal = drawn; + } +} + +/// +/// The state of the crayon UI as sent by the server +/// +/// +/// TODO: Delete this and use component states and predict the UI. +/// This info is already networked on its own. +/// +[Serializable, NetSerializable] +public sealed class CrayonBoundUserInterfaceState : BoundUserInterfaceState +{ + public string Selected; + /// + /// Can the color can be changed + /// + public bool SelectableColor; + public Color Color; + + public CrayonBoundUserInterfaceState(string selected, bool selectableColor, Color color) + { + Selected = selected; + SelectableColor = selectableColor; + Color = color; + } +} diff --git a/Content.Shared/Crayon/SharedCrayonComponent.cs b/Content.Shared/Crayon/SharedCrayonComponent.cs deleted file mode 100644 index a9c21988ea..0000000000 --- a/Content.Shared/Crayon/SharedCrayonComponent.cs +++ /dev/null @@ -1,113 +0,0 @@ -using Robust.Shared.GameStates; -using Robust.Shared.Serialization; - -namespace Content.Shared.Crayon -{ - - /// - /// Component holding the state of a crayon-like component - /// - [NetworkedComponent, ComponentProtoName("Crayon"), Access(typeof(SharedCrayonSystem))] - public abstract partial class SharedCrayonComponent : Component - { - /// - /// The ID of currently selected decal prototype that will be placed when the crayon is used - /// - public string SelectedState { get; set; } = string.Empty; - - /// - /// Color with which the crayon will draw - /// - [DataField("color")] - public Color Color; - - [Serializable, NetSerializable] - public enum CrayonUiKey : byte - { - Key, - } - } - - /// - /// Used by the client to notify the server about the selected decal ID - /// - [Serializable, NetSerializable] - public sealed class CrayonSelectMessage : BoundUserInterfaceMessage - { - public readonly string State; - public CrayonSelectMessage(string selected) - { - State = selected; - } - } - - /// - /// Sets the color of the crayon, used by Rainbow Crayon - /// - [Serializable, NetSerializable] - public sealed class CrayonColorMessage : BoundUserInterfaceMessage - { - public readonly Color Color; - public CrayonColorMessage(Color color) - { - Color = color; - } - } - - /// - /// Server to CLIENT. Notifies the BUI that a decal with given ID has been drawn. - /// Allows the client UI to advance forward in the client-only ephemeral queue, - /// preventing the crayon from becoming a magic text storage device. - /// - [Serializable, NetSerializable] - public sealed class CrayonUsedMessage : BoundUserInterfaceMessage - { - public readonly string DrawnDecal; - - public CrayonUsedMessage(string drawn) - { - DrawnDecal = drawn; - } - } - - /// - /// Component state, describes how many charges are left in the crayon in the near-hand UI - /// - [Serializable, NetSerializable] - public sealed class CrayonComponentState : ComponentState - { - public readonly Color Color; - public readonly string State; - public readonly int Charges; - public readonly int Capacity; - - public CrayonComponentState(Color color, string state, int charges, int capacity) - { - Color = color; - State = state; - Charges = charges; - Capacity = capacity; - } - } - - /// - /// The state of the crayon UI as sent by the server - /// - [Serializable, NetSerializable] - public sealed class CrayonBoundUserInterfaceState : BoundUserInterfaceState - { - public string Selected; - /// - /// Whether or not the color can be selected - /// - public bool SelectableColor; - public Color Color; - - public CrayonBoundUserInterfaceState(string selected, bool selectableColor, Color color) - { - Selected = selected; - SelectableColor = selectableColor; - Color = color; - } - } -} diff --git a/Resources/Prototypes/Entities/Objects/Fun/crayons.yml b/Resources/Prototypes/Entities/Objects/Fun/crayons.yml index a32e3ba89c..85be8ece45 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/crayons.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/crayons.yml @@ -21,7 +21,9 @@ enum.CrayonUiKey.Key: type: CrayonBoundUserInterface - type: Crayon - capacity: 25 + selectedState: like + - type: LimitedCharges + maxCharges: 25 - type: Food - type: FlavorProfile flavors: @@ -88,7 +90,8 @@ - type: Crayon color: Red selectableColor: true - capacity: 30 + - type: LimitedCharges + maxCharges: 30 - type: Tag tags: - Write @@ -96,6 +99,16 @@ - Recyclable - Trash +- type: entity + parent: CrayonRainbow + id: CrayonInfinite # should not be player available to prevent decal spam + name: infinite crayon + components: + - type: Crayon + deleteEmpty: false + - type: AutoRecharge + rechargeDuration: 5 + - type: entity parent: Crayon id: CrayonBlack