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>
This commit is contained in:
David
2025-10-10 13:45:48 -06:00
committed by GitHub
parent 766c2b8759
commit 3503cb52d2
7 changed files with 169 additions and 219 deletions

View File

@@ -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; }
}
}

View File

@@ -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<CrayonComponent, ComponentHandleState>(OnCrayonHandleState);
Subs.ItemStatus<CrayonComponent>(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<CrayonComponent>(ent => new StatusControl(ent, _charges, _entityManager));
}
private sealed class StatusControl : Control
{
private readonly CrayonComponent _parent;
private readonly Entity<CrayonComponent> _crayon;
private readonly SharedChargesSystem _charges;
private readonly RichTextLabel _label;
private readonly int _capacity;
public StatusControl(CrayonComponent parent)
public StatusControl(Entity<CrayonComponent> crayon, SharedChargesSystem charges, EntityManager entityManage)
{
_parent = parent;
_crayon = crayon;
_charges = charges;
_capacity = entityManage.GetComponent<LimitedChargesComponent>(_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)));
}
}
}

View File

@@ -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;
}
}

View File

@@ -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<CrayonComponent, ComponentInit>(OnCrayonInit);
SubscribeLocalEvent<CrayonComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<CrayonComponent, CrayonSelectMessage>(OnCrayonBoundUI);
SubscribeLocalEvent<CrayonComponent, CrayonColorMessage>(OnCrayonBoundUIColor);
SubscribeLocalEvent<CrayonComponent, UseInHandEvent>(OnCrayonUse, before: new[] { typeof(FoodSystem) });
SubscribeLocalEvent<CrayonComponent, AfterInteractEvent>(OnCrayonAfterInteract, after: new[] { typeof(FoodSystem) });
SubscribeLocalEvent<CrayonComponent, DroppedEvent>(OnCrayonDropped);
SubscribeLocalEvent<CrayonComponent, ComponentGetState>(OnCrayonGetState);
}
private static void OnCrayonGetState(EntityUid uid, CrayonComponent component, ref ComponentGetState args)
private void OnMapInit(Entity<CrayonComponent> 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<DecalPrototype>().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<DecalPrototype>().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)

View File

@@ -0,0 +1,119 @@
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Crayon;
/// <summary>
/// Component holding the state of a crayon-like component
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(SharedCrayonSystem))]
public sealed partial class CrayonComponent : Component
{
/// <summary>
/// The ID of currently selected decal prototype that will be placed when the crayon is used.
/// </summary>
[DataField, AutoNetworkedField]
public string SelectedState;
/// <summary>
/// Color with which the crayon will draw.
/// </summary>
[DataField, AutoNetworkedField]
public Color Color;
/// <summary>
/// Play a sound when drawing if specified.
/// </summary>
[DataField]
public SoundSpecifier? UseSound;
/// <summary>
/// Can the color can be changed?
/// </summary>
[DataField, AutoNetworkedField]
public bool SelectableColor;
/// <summary>
/// Should the crayon be deleted when all charges are consumed?
/// </summary>
[DataField, AutoNetworkedField]
public bool DeleteEmpty = true;
}
/// <summary>
/// Opens the crayon window for decal and color selection.
/// </summary>
[Serializable, NetSerializable]
public enum CrayonUiKey : byte
{
Key,
}
/// <summary>
/// Used by the client to notify the server about the selected decal ID
/// </summary>
[Serializable, NetSerializable]
public sealed class CrayonSelectMessage : BoundUserInterfaceMessage
{
public readonly string State;
public CrayonSelectMessage(string selected)
{
State = selected;
}
}
/// <summary>
/// Sets the color of the crayon, used by Rainbow Crayon
/// </summary>
[Serializable, NetSerializable]
public sealed class CrayonColorMessage : BoundUserInterfaceMessage
{
public readonly Color Color;
public CrayonColorMessage(Color color)
{
Color = color;
}
}
/// <summary>
/// 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.
/// </summary>
[Serializable, NetSerializable]
public sealed class CrayonUsedMessage : BoundUserInterfaceMessage
{
public readonly string DrawnDecal;
public CrayonUsedMessage(string drawn)
{
DrawnDecal = drawn;
}
}
/// <summary>
/// The state of the crayon UI as sent by the server
/// </summary>
/// <summary>
/// TODO: Delete this and use component states and predict the UI.
/// This info is already networked on its own.
/// </summary>
[Serializable, NetSerializable]
public sealed class CrayonBoundUserInterfaceState : BoundUserInterfaceState
{
public string Selected;
/// <summary>
/// Can the color can be changed
/// </summary>
public bool SelectableColor;
public Color Color;
public CrayonBoundUserInterfaceState(string selected, bool selectableColor, Color color)
{
Selected = selected;
SelectableColor = selectableColor;
Color = color;
}
}

View File

@@ -1,113 +0,0 @@
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Crayon
{
/// <summary>
/// Component holding the state of a crayon-like component
/// </summary>
[NetworkedComponent, ComponentProtoName("Crayon"), Access(typeof(SharedCrayonSystem))]
public abstract partial class SharedCrayonComponent : Component
{
/// <summary>
/// The ID of currently selected decal prototype that will be placed when the crayon is used
/// </summary>
public string SelectedState { get; set; } = string.Empty;
/// <summary>
/// Color with which the crayon will draw
/// </summary>
[DataField("color")]
public Color Color;
[Serializable, NetSerializable]
public enum CrayonUiKey : byte
{
Key,
}
}
/// <summary>
/// Used by the client to notify the server about the selected decal ID
/// </summary>
[Serializable, NetSerializable]
public sealed class CrayonSelectMessage : BoundUserInterfaceMessage
{
public readonly string State;
public CrayonSelectMessage(string selected)
{
State = selected;
}
}
/// <summary>
/// Sets the color of the crayon, used by Rainbow Crayon
/// </summary>
[Serializable, NetSerializable]
public sealed class CrayonColorMessage : BoundUserInterfaceMessage
{
public readonly Color Color;
public CrayonColorMessage(Color color)
{
Color = color;
}
}
/// <summary>
/// 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.
/// </summary>
[Serializable, NetSerializable]
public sealed class CrayonUsedMessage : BoundUserInterfaceMessage
{
public readonly string DrawnDecal;
public CrayonUsedMessage(string drawn)
{
DrawnDecal = drawn;
}
}
/// <summary>
/// Component state, describes how many charges are left in the crayon in the near-hand UI
/// </summary>
[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;
}
}
/// <summary>
/// The state of the crayon UI as sent by the server
/// </summary>
[Serializable, NetSerializable]
public sealed class CrayonBoundUserInterfaceState : BoundUserInterfaceState
{
public string Selected;
/// <summary>
/// Whether or not the color can be selected
/// </summary>
public bool SelectableColor;
public Color Color;
public CrayonBoundUserInterfaceState(string selected, bool selectableColor, Color color)
{
Selected = selected;
SelectableColor = selectableColor;
Color = color;
}
}
}

View File

@@ -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