Predict GlueSystem (#39079)

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
This commit is contained in:
Kyle Tyo
2025-07-30 15:57:50 -04:00
committed by GitHub
parent a8b65f2da7
commit 68ba22548d
5 changed files with 64 additions and 49 deletions

View File

@@ -2,41 +2,44 @@ using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint; using Content.Shared.FixedPoint;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.Prototypes;
namespace Content.Shared.Glue; namespace Content.Shared.Glue;
[RegisterComponent, NetworkedComponent] /// <summary>
[Access(typeof(SharedGlueSystem))] /// This component indicates that an item is glue and can be used as such.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(GlueSystem))]
public sealed partial class GlueComponent : Component public sealed partial class GlueComponent : Component
{ {
/// <summary> /// <summary>
/// Noise made when glue applied. /// Noise made when glue applied.
/// </summary> /// </summary>
[DataField("squeeze")] [DataField, AutoNetworkedField]
public SoundSpecifier Squeeze = new SoundPathSpecifier("/Audio/Items/squeezebottle.ogg"); public SoundSpecifier Squeeze = new SoundPathSpecifier("/Audio/Items/squeezebottle.ogg");
/// <summary> /// <summary>
/// Solution on the entity that contains the glue. /// Solution on the entity that contains the glue.
/// </summary> /// </summary>
[DataField("solution")] [DataField, AutoNetworkedField]
public string Solution = "drink"; public string Solution = "drink";
/// <summary> /// <summary>
/// Reagent that will be used as glue. /// Reagent that will be used as glue.
/// </summary> /// </summary>
[DataField("reagent", customTypeSerializer: typeof(PrototypeIdSerializer<ReagentPrototype>))] [DataField, AutoNetworkedField]
public string Reagent = "SpaceGlue"; public ProtoId<ReagentPrototype> Reagent = "SpaceGlue";
/// <summary> /// <summary>
/// Reagent consumption per use. /// Reagent consumption per use.
/// </summary> /// </summary>
[DataField("consumptionUnit"), ViewVariables(VVAccess.ReadWrite)] [DataField, AutoNetworkedField]
public FixedPoint2 ConsumptionUnit = FixedPoint2.New(5); public FixedPoint2 ConsumptionUnit = FixedPoint2.New(5);
/// <summary> /// <summary>
/// Duration per unit /// Duration per unit
/// </summary> /// </summary>
[DataField("durationPerUnit"), ViewVariables(VVAccess.ReadWrite)] [DataField, AutoNetworkedField]
public TimeSpan DurationPerUnit = TimeSpan.FromSeconds(6); public TimeSpan DurationPerUnit = TimeSpan.FromSeconds(6);
} }

View File

@@ -1,7 +1,6 @@
using Content.Server.Administration.Logs; using Content.Shared.Administration.Logs;
using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.Glue;
using Content.Shared.Hands; using Content.Shared.Hands;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Interaction.Components; using Content.Shared.Interaction.Components;
@@ -13,17 +12,17 @@ using Content.Shared.Verbs;
using Robust.Shared.Audio.Systems; using Robust.Shared.Audio.Systems;
using Robust.Shared.Timing; using Robust.Shared.Timing;
namespace Content.Server.Glue; namespace Content.Shared.Glue;
public sealed class GlueSystem : SharedGlueSystem public sealed class GlueSystem : EntitySystem
{ {
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly NameModifierSystem _nameMod = default!;
[Dependency] private readonly OpenableSystem _openable = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly OpenableSystem _openable = default!;
[Dependency] private readonly NameModifierSystem _nameMod = default!;
public override void Initialize() public override void Initialize()
{ {
@@ -62,7 +61,7 @@ public sealed class GlueSystem : SharedGlueSystem
Act = () => TryGlue(entity, target, user), Act = () => TryGlue(entity, target, user),
IconEntity = GetNetEntity(entity), IconEntity = GetNetEntity(entity),
Text = Loc.GetString("glue-verb-text"), Text = Loc.GetString("glue-verb-text"),
Message = Loc.GetString("glue-verb-message") Message = Loc.GetString("glue-verb-message"),
}; };
args.Verbs.Add(verb); args.Verbs.Add(verb);
@@ -72,26 +71,29 @@ public sealed class GlueSystem : SharedGlueSystem
{ {
// if item is glued then don't apply glue again so it can be removed for reasonable time // if item is glued then don't apply glue again so it can be removed for reasonable time
// If glue is applied to an unremoveable item, the component will disappear after the duration. // If glue is applied to an unremoveable item, the component will disappear after the duration.
// This effecitvely means any unremoveable item could be removed with a bottle of glue. // This effectively means any unremoveable item could be removed with a bottle of glue.
if (HasComp<GluedComponent>(target) || !HasComp<ItemComponent>(target) || HasComp<UnremoveableComponent>(target)) if (HasComp<GluedComponent>(target) || !HasComp<ItemComponent>(target) || HasComp<UnremoveableComponent>(target))
{ {
_popup.PopupEntity(Loc.GetString("glue-failure", ("target", target)), actor, actor, PopupType.Medium); _popup.PopupClient(Loc.GetString("glue-failure", ("target", target)), actor, actor, PopupType.Medium);
return false; return false;
} }
if (HasComp<ItemComponent>(target) && _solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out _, out var solution)) if (HasComp<ItemComponent>(target) && _solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out var solutionEntity, out _))
{ {
var quantity = solution.RemoveReagent(entity.Comp.Reagent, entity.Comp.ConsumptionUnit); var quantity = _solutionContainer.RemoveReagent(solutionEntity.Value, entity.Comp.Reagent, entity.Comp.ConsumptionUnit);
if (quantity > 0) if (quantity > 0)
{ {
EnsureComp<GluedComponent>(target).Duration = quantity.Double() * entity.Comp.DurationPerUnit; _audio.PlayPredicted(entity.Comp.Squeeze, entity.Owner, actor);
_popup.PopupClient(Loc.GetString("glue-success", ("target", target)), actor, actor, PopupType.Medium);
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(actor):actor} glued {ToPrettyString(target):subject} with {ToPrettyString(entity.Owner):tool}"); _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(actor):actor} glued {ToPrettyString(target):subject} with {ToPrettyString(entity.Owner):tool}");
_audio.PlayPvs(entity.Comp.Squeeze, entity.Owner); var gluedComp = EnsureComp<GluedComponent>(target);
_popup.PopupEntity(Loc.GetString("glue-success", ("target", target)), actor, actor, PopupType.Medium); gluedComp.Duration = quantity.Double() * entity.Comp.DurationPerUnit;
Dirty(target, gluedComp);
return true; return true;
} }
} }
_popup.PopupEntity(Loc.GetString("glue-failure", ("target", target)), actor, actor, PopupType.Medium);
_popup.PopupClient(Loc.GetString("glue-failure", ("target", target)), actor, actor, PopupType.Medium);
return false; return false;
} }
@@ -119,9 +121,16 @@ public sealed class GlueSystem : SharedGlueSystem
private void OnHandPickUp(Entity<GluedComponent> entity, ref GotEquippedHandEvent args) private void OnHandPickUp(Entity<GluedComponent> entity, ref GotEquippedHandEvent args)
{ {
// When predicting dropping a glued item prediction will reinsert the item into the hand when rerolling the state to a previous one.
// So dropping the item would add UnRemoveableComponent on the client without this guard statement.
if (_timing.ApplyingState)
return;
var comp = EnsureComp<UnremoveableComponent>(entity); var comp = EnsureComp<UnremoveableComponent>(entity);
comp.DeleteOnDrop = false; comp.DeleteOnDrop = false;
entity.Comp.Until = _timing.CurTime + entity.Comp.Duration; entity.Comp.Until = _timing.CurTime + entity.Comp.Duration;
Dirty(entity.Owner, comp);
Dirty(entity);
} }
private void OnRefreshNameModifiers(Entity<GluedComponent> entity, ref RefreshNameModifiersEvent args) private void OnRefreshNameModifiers(Entity<GluedComponent> entity, ref RefreshNameModifiersEvent args)

View File

@@ -1,15 +1,26 @@
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Glue; namespace Content.Shared.Glue;
[RegisterComponent] /// <summary>
[Access(typeof(SharedGlueSystem))] /// This component gets attached to an item that has been glued.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
[Access(typeof(GlueSystem))]
public sealed partial class GluedComponent : Component public sealed partial class GluedComponent : Component
{ {
/// <summary>
[DataField("until", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)] /// The TimeSpan this effect expires at.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoNetworkedField, AutoPausedField]
public TimeSpan Until; public TimeSpan Until;
[DataField("duration", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)] /// <summary>
/// The duration this effect will last. Determined by the quantity of the reagent that is applied.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoNetworkedField]
public TimeSpan Duration; public TimeSpan Duration;
} }

View File

@@ -1,5 +0,0 @@
namespace Content.Shared.Glue;
public abstract class SharedGlueSystem : EntitySystem
{
}

View File

@@ -1,17 +1,14 @@
using Content.Shared.Whitelist;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
namespace Content.Shared.Interaction.Components namespace Content.Shared.Interaction.Components;
{
[RegisterComponent] [RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[NetworkedComponent]
public sealed partial class UnremoveableComponent : Component public sealed partial class UnremoveableComponent : Component
{ {
/// <summary> /// <summary>
/// If this is true then unremovable items that are removed from inventory are deleted (typically from corpse gibbing). /// If this is true then unremovable items that are removed from inventory are deleted (typically from corpse gibbing).
/// Items within unremovable containers are not deleted when removed. /// Items within unremovable containers are not deleted when removed.
/// </summary> /// </summary>
[DataField("deleteOnDrop")] [DataField, AutoNetworkedField]
public bool DeleteOnDrop = true; public bool DeleteOnDrop = true;
} }
}