Fix handlabeler/utility belt misprediction (#26660)

* Fix handlabeler/utility belt misprediction

* Partly moved HandLabelerSystem to shared
And cleaned up HandLabelerComponent

* WIP format the files so later commits look clearer
Doesn't change individual code lines, but may move
functions to another file

* WIP some more code movement

* Hand Labeler is now mostly predicted
Only the UI isn't

* WIP: Formatting and moved stuff

* Using componentstates for prediction correction

* review

* Update label on label change

* Don't overwrite label while editing

---------

Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
This commit is contained in:
osjarw
2024-04-28 07:19:30 +03:00
committed by GitHub
parent 1c78f60bc3
commit a4504e2fef
10 changed files with 250 additions and 190 deletions

View File

@@ -0,0 +1,18 @@
using Content.Client.Labels.UI;
using Content.Shared.Labels;
using Content.Shared.Labels.Components;
using Content.Shared.Labels.EntitySystems;
namespace Content.Client.Labels.EntitySystems;
public sealed class HandLabelerSystem : SharedHandLabelerSystem
{
protected override void UpdateUI(Entity<HandLabelerComponent> ent)
{
if (UserInterfaceSystem.TryGetOpenUi(ent.Owner, HandLabelerUiKey.Key, out var bui)
&& bui is HandLabelerBoundUserInterface cBui)
{
cBui.Reload();
}
}
}

View File

@@ -1,4 +1,5 @@
using Content.Shared.Labels;
using Content.Shared.Labels.Components;
using Robust.Client.GameObjects;
namespace Content.Client.Labels.UI
@@ -8,11 +9,14 @@ namespace Content.Client.Labels.UI
/// </summary>
public sealed class HandLabelerBoundUserInterface : BoundUserInterface
{
[Dependency] private readonly IEntityManager _entManager = default!;
[ViewVariables]
private HandLabelerWindow? _window;
public HandLabelerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
IoCManager.InjectDependencies(this);
}
protected override void Open()
@@ -27,24 +31,25 @@ namespace Content.Client.Labels.UI
_window.OnClose += Close;
_window.OnLabelChanged += OnLabelChanged;
Reload();
}
private void OnLabelChanged(string newLabel)
{
SendMessage(new HandLabelerLabelChangedMessage(newLabel));
}
/// <summary>
/// Update the UI state based on server-sent info
/// </summary>
/// <param name="state"></param>
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (_window == null || state is not HandLabelerBoundUserInterfaceState cast)
// Focus moment
if (_entManager.TryGetComponent(Owner, out HandLabelerComponent? labeler) &&
labeler.AssignedLabel.Equals(newLabel))
return;
_window.SetCurrentLabel(cast.CurrentLabel);
SendPredictedMessage(new HandLabelerLabelChangedMessage(newLabel));
}
public void Reload()
{
if (_window == null || !_entManager.TryGetComponent(Owner, out HandLabelerComponent? component))
return;
_window.SetCurrentLabel(component.AssignedLabel);
}
protected override void Dispose(bool disposing)

View File

@@ -9,17 +9,40 @@ namespace Content.Client.Labels.UI
{
public event Action<string>? OnLabelChanged;
/// <summary>
/// Is the user currently entering text into the control?
/// </summary>
private bool _focused;
// TODO LineEdit Make this a bool on the LineEdit control
private string _label = string.Empty;
public HandLabelerWindow()
{
RobustXamlLoader.Load(this);
LabelLineEdit.OnTextEntered += e => OnLabelChanged?.Invoke(e.Text);
LabelLineEdit.OnFocusExit += e => OnLabelChanged?.Invoke(e.Text);
LabelLineEdit.OnTextEntered += e =>
{
_label = e.Text;
OnLabelChanged?.Invoke(_label);
};
LabelLineEdit.OnFocusEnter += _ => _focused = true;
LabelLineEdit.OnFocusExit += _ =>
{
_focused = false;
LabelLineEdit.Text = _label;
};
}
public void SetCurrentLabel(string label)
{
LabelLineEdit.Text = label;
if (label == _label)
return;
_label = label;
if (!_focused)
LabelLineEdit.Text = label;
}
}
}

View File

@@ -1,19 +0,0 @@
using Content.Shared.Whitelist;
namespace Content.Server.Labels.Components
{
[RegisterComponent]
public sealed partial class HandLabelerComponent : Component
{
[ViewVariables(VVAccess.ReadWrite)]
[DataField("assignedLabel")]
public string AssignedLabel { get; set; } = string.Empty;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("maxLabelChars")]
public int MaxLabelChars { get; set; } = 50;
[DataField("whitelist")]
public EntityWhitelist Whitelist = new();
}
}

View File

@@ -1,116 +1,8 @@
using Content.Server.Labels.Components;
using Content.Server.UserInterface;
using Content.Server.Popups;
using Content.Shared.Administration.Logs;
using Content.Shared.Database;
using Content.Shared.Interaction;
using Content.Shared.Labels;
using Content.Shared.Verbs;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Player;
using Content.Shared.Labels.EntitySystems;
namespace Content.Server.Labels
namespace Content.Server.Labels.Label;
public sealed class HandLabelerSystem : SharedHandLabelerSystem
{
/// <summary>
/// A hand labeler system that lets an object apply labels to objects with the <see cref="LabelComponent"/> .
/// </summary>
[UsedImplicitly]
public sealed class HandLabelerSystem : EntitySystem
{
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly LabelSystem _labelSystem = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<HandLabelerComponent, AfterInteractEvent>(AfterInteractOn);
SubscribeLocalEvent<HandLabelerComponent, GetVerbsEvent<UtilityVerb>>(OnUtilityVerb);
// Bound UI subscriptions
SubscribeLocalEvent<HandLabelerComponent, HandLabelerLabelChangedMessage>(OnHandLabelerLabelChanged);
}
private void OnUtilityVerb(EntityUid uid, HandLabelerComponent handLabeler, GetVerbsEvent<UtilityVerb> args)
{
if (args.Target is not { Valid: true } target || !handLabeler.Whitelist.IsValid(target) || !args.CanAccess)
return;
string labelerText = handLabeler.AssignedLabel == string.Empty ? Loc.GetString("hand-labeler-remove-label-text") : Loc.GetString("hand-labeler-add-label-text");
var verb = new UtilityVerb()
{
Act = () =>
{
AddLabelTo(uid, handLabeler, target, out var result);
if (result != null)
_popupSystem.PopupEntity(result, args.User, args.User);
},
Text = labelerText
};
args.Verbs.Add(verb);
}
private void AfterInteractOn(EntityUid uid, HandLabelerComponent handLabeler, AfterInteractEvent args)
{
if (args.Target is not {Valid: true} target || !handLabeler.Whitelist.IsValid(target) || !args.CanReach)
return;
AddLabelTo(uid, handLabeler, target, out string? result);
if (result == null)
return;
_popupSystem.PopupEntity(result, args.User, args.User);
// Log labeling
_adminLogger.Add(LogType.Action, LogImpact.Low,
$"{ToPrettyString(args.User):user} labeled {ToPrettyString(target):target} with {ToPrettyString(uid):labeler}");
}
private void AddLabelTo(EntityUid uid, HandLabelerComponent? handLabeler, EntityUid target, out string? result)
{
if (!Resolve(uid, ref handLabeler))
{
result = null;
return;
}
if (handLabeler.AssignedLabel == string.Empty)
{
_labelSystem.Label(target, null);
result = Loc.GetString("hand-labeler-successfully-removed");
return;
}
_labelSystem.Label(target, handLabeler.AssignedLabel);
result = Loc.GetString("hand-labeler-successfully-applied");
}
private void OnHandLabelerLabelChanged(EntityUid uid, HandLabelerComponent handLabeler, HandLabelerLabelChangedMessage args)
{
if (args.Actor is not {Valid: true} player)
return;
var label = args.Label.Trim();
handLabeler.AssignedLabel = label.Substring(0, Math.Min(handLabeler.MaxLabelChars, label.Length));
DirtyUI(uid, handLabeler);
// Log label change
_adminLogger.Add(LogType.Action, LogImpact.Low,
$"{ToPrettyString(player):user} set {ToPrettyString(uid):labeler} to apply label \"{handLabeler.AssignedLabel}\"");
}
private void DirtyUI(EntityUid uid,
HandLabelerComponent? handLabeler = null)
{
if (!Resolve(uid, ref handLabeler))
return;
_userInterfaceSystem.SetUiState(uid, HandLabelerUiKey.Key,
new HandLabelerBoundUserInterfaceState(handLabeler.AssignedLabel));
}
}
}

View File

@@ -50,7 +50,7 @@ namespace Content.Server.Labels
/// <param name="text">intended label text (null to remove)</param>
/// <param name="label">label component for resolve</param>
/// <param name="metadata">metadata component for resolve</param>
public void Label(EntityUid uid, string? text, MetaDataComponent? metadata = null, LabelComponent? label = null)
public override void Label(EntityUid uid, string? text, MetaDataComponent? metadata = null, LabelComponent? label = null)
{
if (!Resolve(uid, ref metadata))
return;

View File

@@ -0,0 +1,30 @@
using Content.Shared.Labels.EntitySystems;
using Content.Shared.Whitelist;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Labels.Components;
[RegisterComponent, NetworkedComponent]
[Access(typeof(SharedHandLabelerSystem))]
public sealed partial class HandLabelerComponent : Component
{
[ViewVariables(VVAccess.ReadWrite), Access(Other = AccessPermissions.ReadWriteExecute)]
[DataField]
public string AssignedLabel = string.Empty;
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public int MaxLabelChars = 50;
[DataField]
public EntityWhitelist Whitelist = new();
}
[Serializable, NetSerializable]
public sealed class HandLabelerComponentState(string assignedLabel) : IComponentState
{
public string AssignedLabel = assignedLabel;
public int MaxLabelChars;
}

View File

@@ -0,0 +1,129 @@
using Content.Shared.Administration.Logs;
using Content.Shared.Database;
using Content.Shared.Interaction;
using Content.Shared.Labels.Components;
using Content.Shared.Popups;
using Content.Shared.Verbs;
using Robust.Shared.GameStates;
using Robust.Shared.Network;
namespace Content.Shared.Labels.EntitySystems;
public abstract class SharedHandLabelerSystem : EntitySystem
{
[Dependency] protected readonly SharedUserInterfaceSystem UserInterfaceSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly SharedLabelSystem _labelSystem = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly INetManager _netManager = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<HandLabelerComponent, AfterInteractEvent>(AfterInteractOn);
SubscribeLocalEvent<HandLabelerComponent, GetVerbsEvent<UtilityVerb>>(OnUtilityVerb);
// Bound UI subscriptions
SubscribeLocalEvent<HandLabelerComponent, HandLabelerLabelChangedMessage>(OnHandLabelerLabelChanged);
SubscribeLocalEvent<HandLabelerComponent, ComponentGetState>(OnGetState);
SubscribeLocalEvent<HandLabelerComponent, ComponentHandleState>(OnHandleState);
}
private void OnGetState(Entity<HandLabelerComponent> ent, ref ComponentGetState args)
{
args.State = new HandLabelerComponentState(ent.Comp.AssignedLabel)
{
MaxLabelChars = ent.Comp.MaxLabelChars,
};
}
private void OnHandleState(Entity<HandLabelerComponent> ent, ref ComponentHandleState args)
{
if (args.Current is not HandLabelerComponentState state)
return;
ent.Comp.MaxLabelChars = state.MaxLabelChars;
if (ent.Comp.AssignedLabel == state.AssignedLabel)
return;
ent.Comp.AssignedLabel = state.AssignedLabel;
UpdateUI(ent);
}
protected virtual void UpdateUI(Entity<HandLabelerComponent> ent)
{
}
private void AddLabelTo(EntityUid uid, HandLabelerComponent? handLabeler, EntityUid target, out string? result)
{
if (!Resolve(uid, ref handLabeler))
{
result = null;
return;
}
if (handLabeler.AssignedLabel == string.Empty)
{
if (_netManager.IsServer)
_labelSystem.Label(target, null);
result = Loc.GetString("hand-labeler-successfully-removed");
return;
}
if (_netManager.IsServer)
_labelSystem.Label(target, handLabeler.AssignedLabel);
result = Loc.GetString("hand-labeler-successfully-applied");
}
private void OnUtilityVerb(EntityUid uid, HandLabelerComponent handLabeler, GetVerbsEvent<UtilityVerb> args)
{
if (args.Target is not { Valid: true } target || !handLabeler.Whitelist.IsValid(target) || !args.CanAccess)
return;
var labelerText = handLabeler.AssignedLabel == string.Empty ? Loc.GetString("hand-labeler-remove-label-text") : Loc.GetString("hand-labeler-add-label-text");
var verb = new UtilityVerb()
{
Act = () =>
{
Labeling(uid, target, args.User, handLabeler);
},
Text = labelerText
};
args.Verbs.Add(verb);
}
private void AfterInteractOn(EntityUid uid, HandLabelerComponent handLabeler, AfterInteractEvent args)
{
if (args.Target is not { Valid: true } target || !handLabeler.Whitelist.IsValid(target) || !args.CanReach)
return;
Labeling(uid, target, args.User, handLabeler);
}
private void Labeling(EntityUid uid, EntityUid target, EntityUid User, HandLabelerComponent handLabeler)
{
AddLabelTo(uid, handLabeler, target, out var result);
if (result == null)
return;
_popupSystem.PopupClient(result, User, User);
// Log labeling
_adminLogger.Add(LogType.Action, LogImpact.Low,
$"{ToPrettyString(User):user} labeled {ToPrettyString(target):target} with {ToPrettyString(uid):labeler}");
}
private void OnHandLabelerLabelChanged(EntityUid uid, HandLabelerComponent handLabeler, HandLabelerLabelChangedMessage args)
{
var label = args.Label.Trim();
handLabeler.AssignedLabel = label[..Math.Min(handLabeler.MaxLabelChars, label.Length)];
UpdateUI((uid, handLabeler));
Dirty(uid, handLabeler);
// Log label change
_adminLogger.Add(LogType.Action, LogImpact.Low,
$"{ToPrettyString(args.Actor):user} set {ToPrettyString(uid):labeler} to apply label \"{handLabeler.AssignedLabel}\"");
}
}

View File

@@ -13,6 +13,8 @@ public abstract partial class SharedLabelSystem : EntitySystem
SubscribeLocalEvent<LabelComponent, ExaminedEvent>(OnExamine);
}
public virtual void Label(EntityUid uid, string? text, MetaDataComponent? metadata = null, LabelComponent? label = null){}
private void OnExamine(EntityUid uid, LabelComponent? label, ExaminedEvent args)
{
if (!Resolve(uid, ref label))

View File

@@ -1,47 +1,27 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Labels
namespace Content.Shared.Labels;
/// <summary>
/// Key representing which <see cref="PlayerBoundUserInterface"/> is currently open.
/// Useful when there are multiple UI for an object. Here it's future-proofing only.
/// </summary>
[Serializable, NetSerializable]
public enum HandLabelerUiKey
{
/// <summary>
/// Key representing which <see cref="PlayerBoundUserInterface"/> is currently open.
/// Useful when there are multiple UI for an object. Here it's future-proofing only.
/// </summary>
[Serializable, NetSerializable]
public enum HandLabelerUiKey
{
Key,
}
[Serializable, NetSerializable]
public enum PaperLabelVisuals : byte
{
Layer,
HasLabel,
LabelType
}
/// <summary>
/// Represents a <see cref="HandLabelerComponent"/> state that can be sent to the client
/// </summary>
[Serializable, NetSerializable]
public sealed class HandLabelerBoundUserInterfaceState : BoundUserInterfaceState
{
public string CurrentLabel { get; }
public HandLabelerBoundUserInterfaceState(string currentLabel)
{
CurrentLabel = currentLabel;
}
}
[Serializable, NetSerializable]
public sealed class HandLabelerLabelChangedMessage : BoundUserInterfaceMessage
{
public string Label { get; }
public HandLabelerLabelChangedMessage(string label)
{
Label = label;
}
}
Key,
}
[Serializable, NetSerializable]
public enum PaperLabelVisuals : byte
{
Layer,
HasLabel,
LabelType
}
[Serializable, NetSerializable]
public sealed class HandLabelerLabelChangedMessage(string label) : BoundUserInterfaceMessage
{
public string Label { get; } = label;
}