Merge remote-tracking branch 'space-station-14/master' into 29-04-2024-upstream

# Conflicts:
#	Resources/Prototypes/Entities/Mobs/Customization/Markings/human_hair.yml
This commit is contained in:
Ed
2024-04-29 16:28:04 +03:00
565 changed files with 6918 additions and 3437 deletions

View File

@@ -68,9 +68,10 @@ public sealed class GetItemActionsEvent : EntityEventArgs
AddAction(ref actionId, prototypeId, Provider);
}
public void AddAction(EntityUid actionId)
public void AddAction(EntityUid? actionId)
{
Actions.Add(actionId);
if (actionId != null)
Actions.Add(actionId.Value);
}
}

View File

@@ -99,6 +99,11 @@
/// </summary>
Stealth = 1 << 16,
///<summary>
/// Allows you to use Admin chat
///</summary>
Adminchat = 1 << 17,
/// <summary>
/// Dangerous host permissions like scsi.
/// </summary>

View File

@@ -1,4 +1,4 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Prototypes;
namespace Content.Shared.Body.Prototypes
{
@@ -7,5 +7,11 @@ namespace Content.Shared.Body.Prototypes
{
[IdDataField]
public string ID { get; private set; } = default!;
[DataField("name", required: true)]
private LocId Name { get; set; }
[ViewVariables(VVAccess.ReadOnly)]
public string LocalizedName => Loc.GetString(Name);
}
}

View File

@@ -1,4 +1,4 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Prototypes;
namespace Content.Shared.Body.Prototypes
{
@@ -9,6 +9,9 @@ namespace Content.Shared.Body.Prototypes
public string ID { get; private set; } = default!;
[DataField("name", required: true)]
public string Name { get; private set; } = default!;
private LocId Name { get; set; }
[ViewVariables(VVAccess.ReadOnly)]
public string LocalizedName => Loc.GetString(Name);
}
}

View File

@@ -435,6 +435,12 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<string> LoginTipsDataset =
CVarDef.Create("tips.login_dataset", "Tips");
/// <summary>
/// The chance for Tippy to replace a normal tip message.
/// </summary>
public static readonly CVarDef<float> TipsTippyChance =
CVarDef.Create("tips.tippy_chance", 0.01f);
/*
* Console
*/
@@ -1994,6 +2000,10 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<bool> GatewayGeneratorEnabled =
CVarDef.Create("gateway.generator_enabled", true);
// Clippy!
public static readonly CVarDef<string> TippyEntity =
CVarDef.Create("tippy.entity", "Tippy", CVar.SERVER | CVar.REPLICATED);
/*
* DEBUG
*/

View File

@@ -0,0 +1,11 @@
using Content.Shared.Chat.Prototypes;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Chat;
[Serializable, NetSerializable]
public sealed class PlayEmoteMessage(ProtoId<EmotePrototype> protoId) : EntityEventArgs
{
public readonly ProtoId<EmotePrototype> ProtoId = protoId;
}

View File

@@ -1,11 +1,13 @@
using Content.Shared.Whitelist;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.Chat.Prototypes;
/// <summary>
/// IC emotes (scream, smile, clapping, etc).
/// Entities can activate emotes by chat input or code.
/// Entities can activate emotes by chat input, radial or code.
/// </summary>
[Prototype("emote")]
public sealed partial class EmotePrototype : IPrototype
@@ -13,18 +15,50 @@ public sealed partial class EmotePrototype : IPrototype
[IdDataField]
public string ID { get; private set; } = default!;
/// <summary>
/// Localization string for the emote name. Displayed in the radial UI.
/// </summary>
[DataField(required: true)]
public string Name = default!;
/// <summary>
/// Determines if emote available to all by default
/// <see cref="Whitelist"/> check comes after this setting
/// <see cref="Content.Shared.Speech.SpeechComponent.AllowedEmotes"/> can ignore this setting
/// </summary>
[DataField]
public bool Available = true;
/// <summary>
/// Different emote categories may be handled by different systems.
/// Also may be used for filtering.
/// </summary>
[DataField("category")]
[DataField]
public EmoteCategory Category = EmoteCategory.General;
/// <summary>
/// An icon used to visually represent the emote in radial UI.
/// </summary>
[DataField]
public SpriteSpecifier Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/Actions/scream.png"));
/// <summary>
/// Determines conditions to this emote be available to use
/// </summary>
[DataField]
public EntityWhitelist? Whitelist;
/// <summary>
/// Determines conditions to this emote be unavailable to use
/// </summary>
[DataField]
public EntityWhitelist? Blacklist;
/// <summary>
/// Collection of words that will be sent to chat if emote activates.
/// Will be picked randomly from list.
/// </summary>
[DataField("chatMessages")]
[DataField]
public List<string> ChatMessages = new();
/// <summary>
@@ -32,7 +66,7 @@ public sealed partial class EmotePrototype : IPrototype
/// When typed into players chat they will activate emote event.
/// All words should be unique across all emote prototypes.
/// </summary>
[DataField("chatTriggers")]
[DataField]
public HashSet<string> ChatTriggers = new();
}

View File

@@ -1,5 +1,6 @@
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
namespace Content.Shared.Chat.Prototypes;
@@ -8,8 +9,8 @@ namespace Content.Shared.Chat.Prototypes;
/// Sounds collection for each <see cref="EmotePrototype"/>.
/// Different entities may use different sounds collections.
/// </summary>
[Prototype("emoteSounds")]
public sealed partial class EmoteSoundsPrototype : IPrototype
[Prototype("emoteSounds"), Serializable, NetSerializable]
public sealed class EmoteSoundsPrototype : IPrototype
{
[IdDataField]
public string ID { get; private set; } = default!;

View File

@@ -96,20 +96,23 @@ public abstract class ClothingSystem : EntitySystem
{
if (TryComp(item, out HideLayerClothingComponent? comp))
{
//Checks for mask toggling. TODO: Make a generic system for this
if (comp.HideOnToggle && TryComp(item, out MaskComponent? mask) && TryComp(item, out ClothingComponent? clothing))
if (comp.Slots.Contains(layer))
{
if (clothing.EquippedPrefix != mask.EquippedPrefix)
//Checks for mask toggling. TODO: Make a generic system for this
if (comp.HideOnToggle && TryComp(item, out MaskComponent? mask) && TryComp(item, out ClothingComponent? clothing))
{
if (clothing.EquippedPrefix != mask.EquippedPrefix)
{
shouldLayerShow = false;
break;
}
}
else
{
shouldLayerShow = false;
break;
}
}
else
{
shouldLayerShow = false;
break;
}
}
}
_humanoidSystem.SetLayerVisibility(equipee, layer, shouldLayerShow);

View File

@@ -45,7 +45,7 @@ public sealed class MaskSystem : EntitySystem
var dir = mask.IsToggled ? "down" : "up";
var msg = $"action-mask-pull-{dir}-popup-message";
_popupSystem.PopupEntity(Loc.GetString(msg, ("mask", uid)), args.Performer, args.Performer);
_popupSystem.PopupClient(Loc.GetString(msg, ("mask", uid)), args.Performer, args.Performer);
ToggleMaskComponents(uid, mask, args.Performer, mask.EquippedPrefix);
}

View File

@@ -17,6 +17,12 @@ namespace Content.Shared.Damage.Prototypes
{
[IdDataField] public string ID { get; } = default!;
[DataField(required: true)]
private LocId Name { get; set; }
[ViewVariables(VVAccess.ReadOnly)]
public string LocalizedName => Loc.GetString(Name);
[DataField("damageTypes", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer<DamageTypePrototype>))]
public List<string> DamageTypes { get; private set; } = default!;
}

View File

@@ -11,6 +11,12 @@ namespace Content.Shared.Damage.Prototypes
[IdDataField]
public string ID { get; private set; } = default!;
[DataField(required: true)]
private LocId Name { get; set; }
[ViewVariables(VVAccess.ReadOnly)]
public string LocalizedName => Loc.GetString(Name);
/// <summary>
/// The price for each 1% damage reduction in armors
/// </summary>

View File

@@ -1,8 +1,10 @@
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Events;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Examine;
using Content.Shared.FixedPoint;
using Content.Shared.Verbs;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Shared.Damage.Systems;
@@ -10,6 +12,7 @@ namespace Content.Shared.Damage.Systems;
public sealed class DamageExamineSystem : EntitySystem
{
[Dependency] private readonly ExamineSystemShared _examine = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
public override void Initialize()
{
@@ -66,7 +69,7 @@ public sealed class DamageExamineSystem : EntitySystem
if (damage.Value != FixedPoint2.Zero)
{
msg.PushNewline();
msg.AddMarkup(Loc.GetString("damage-value", ("type", damage.Key), ("amount", damage.Value)));
msg.AddMarkup(Loc.GetString("damage-value", ("type", _prototype.Index<DamageTypePrototype>(damage.Key).LocalizedName), ("amount", damage.Value)));
}
}

View File

@@ -36,13 +36,6 @@ public abstract partial class SharedDisposalUnitComponent : Component
[ViewVariables(VVAccess.ReadWrite), DataField("soundInsert")]
public SoundSpecifier? InsertSound = new SoundPathSpecifier("/Audio/Effects/trashbag1.ogg");
/// <summary>
/// Sound played when an item is thrown and misses the disposal unit.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("soundMiss")]
public SoundSpecifier? MissSound = new SoundPathSpecifier("/Audio/Effects/thudswoosh.ogg");
/// <summary>
/// State for this disposals unit.
/// </summary>

View File

@@ -0,0 +1,166 @@
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Paper;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Fax.Components;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class FaxMachineComponent : Component
{
/// <summary>
/// Name with which the fax will be visible to others on the network
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("name")]
public string FaxName { get; set; } = "Unknown";
/// <summary>
/// Sprite to use when inserting an object.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField, AutoNetworkedField]
public string InsertingState = "inserting";
/// <summary>
/// Device address of fax in network to which data will be send
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("destinationAddress")]
public string? DestinationFaxAddress { get; set; }
/// <summary>
/// Contains the item to be sent, assumes it's paper...
/// </summary>
[DataField(required: true)]
public ItemSlot PaperSlot = new();
/// <summary>
/// Is fax machine should respond to pings in network
/// This will make it visible to others on the network
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public bool ResponsePings { get; set; } = true;
/// <summary>
/// Should admins be notified on message receive
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public bool NotifyAdmins { get; set; } = false;
/// <summary>
/// Should that fax receive nuke codes send by admins. Probably should be captain fax only
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public bool ReceiveNukeCodes { get; set; } = false;
/// <summary>
/// Sound to play when fax has been emagged
/// </summary>
[DataField]
public SoundSpecifier EmagSound = new SoundCollectionSpecifier("sparks");
/// <summary>
/// Sound to play when fax printing new message
/// </summary>
[DataField]
public SoundSpecifier PrintSound = new SoundPathSpecifier("/Audio/Machines/printer.ogg");
/// <summary>
/// Sound to play when fax successfully send message
/// </summary>
[DataField]
public SoundSpecifier SendSound = new SoundPathSpecifier("/Audio/Machines/high_tech_confirm.ogg");
/// <summary>
/// Known faxes in network by address with fax names
/// </summary>
[ViewVariables]
public Dictionary<string, string> KnownFaxes { get; } = new();
/// <summary>
/// Print queue of the incoming message
/// </summary>
[ViewVariables]
[DataField]
public Queue<FaxPrintout> PrintingQueue { get; private set; } = new();
/// <summary>
/// Message sending timeout
/// </summary>
[ViewVariables]
[DataField]
public float SendTimeoutRemaining;
/// <summary>
/// Message sending timeout
/// </summary>
[ViewVariables]
[DataField]
public float SendTimeout = 5f;
/// <summary>
/// Remaining time of inserting animation
/// </summary>
[DataField]
public float InsertingTimeRemaining;
/// <summary>
/// How long the inserting animation will play
/// </summary>
[ViewVariables]
public float InsertionTime = 1.88f; // 0.02 off for correct animation
/// <summary>
/// Remaining time of printing animation
/// </summary>
[DataField]
public float PrintingTimeRemaining;
/// <summary>
/// How long the printing animation will play
/// </summary>
[ViewVariables]
public float PrintingTime = 2.3f;
}
[DataDefinition]
public sealed partial class FaxPrintout
{
[DataField(required: true)]
public string Name { get; private set; } = default!;
[DataField]
public string? Label { get; private set; }
[DataField(required: true)]
public string Content { get; private set; } = default!;
[DataField(customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>), required: true)]
public string PrototypeId { get; private set; } = default!;
[DataField("stampState")]
public string? StampState { get; private set; }
[DataField("stampedBy")]
public List<StampDisplayInfo> StampedBy { get; private set; } = new();
private FaxPrintout()
{
}
public FaxPrintout(string content, string name, string? label = null, string? prototypeId = null, string? stampState = null, List<StampDisplayInfo>? stampedBy = null)
{
Content = content;
Name = name;
Label = label;
PrototypeId = prototypeId ?? "";
StampState = stampState;
StampedBy = stampedBy ?? new List<StampDisplayInfo>();
}
}

View File

@@ -0,0 +1,16 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Fax.Components;
/// <summary>
/// Entity with this component can be faxed.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class FaxableObjectComponent : Component
{
/// <summary>
/// Sprite to use when inserting an object.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField, AutoNetworkedField]
public string InsertingState = "inserting";
}

View File

@@ -0,0 +1,19 @@
using Content.Shared.Damage;
using Robust.Shared.GameStates;
namespace Content.Shared.Fax.Components;
/// <summary>
/// A fax component which stores a damage specifier for attempting to fax a mob.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class FaxecuteComponent : Component
{
/// <summary>
/// Type of damage dealt when entity is faxecuted.
/// </summary>
[DataField(required: true), AutoNetworkedField]
public DamageSpecifier Damage = new();
}

View File

@@ -0,0 +1,9 @@
namespace Content.Shared.Fax.Components;
/// <summary>
/// Event for killing any mob within the fax machine.
/// </summary
[ByRefEvent]
public record struct DamageOnFaxecuteEvent(FaxMachineComponent? Action);

View File

@@ -37,11 +37,13 @@ public sealed class FaxUiState : BoundUserInterfaceState
[Serializable, NetSerializable]
public sealed class FaxFileMessage : BoundUserInterfaceMessage
{
public string? Label;
public string Content;
public bool OfficePaper;
public FaxFileMessage(string content, bool officePaper)
public FaxFileMessage(string? label, string content, bool officePaper)
{
Label = label;
Content = content;
OfficePaper = officePaper;
}
@@ -49,6 +51,7 @@ public sealed class FaxFileMessage : BoundUserInterfaceMessage
public static class FaxFileMessageValidation
{
public const int MaxLabelSize = 50; // parity with Content.Server.Labels.Components.HandLabelerComponent.MaxLabelChars
public const int MaxContentSize = 10000;
}

View File

@@ -0,0 +1,34 @@
using Content.Shared.Damage;
using Content.Shared.Popups;
using Content.Shared.Fax.Components;
namespace Content.Shared.Fax.Systems;
/// <summary>
/// System for handling execution of a mob within fax when copy or send attempt is made.
/// </summary>
public sealed class FaxecuteSystem : EntitySystem
{
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
public override void Initialize()
{
base.Initialize();
}
public void Faxecute(EntityUid uid, FaxMachineComponent component, DamageOnFaxecuteEvent? args = null)
{
var sendEntity = component.PaperSlot.Item;
if (sendEntity == null)
return;
if (!TryComp<FaxecuteComponent>(uid, out var faxecute))
return;
var damageSpec = faxecute.Damage;
_damageable.TryChangeDamage(sendEntity, damageSpec);
_popupSystem.PopupEntity(Loc.GetString("fax-machine-popup-error", ("target", uid)), uid, PopupType.LargeCaution);
return;
}
}

View File

@@ -87,7 +87,6 @@ public abstract partial class SharedHandsSystem
/// </summary>
/// <param name="uid"></param>
/// <param name="handsComp"></param>
public void RemoveHands(EntityUid uid, HandsComponent? handsComp = null)
{
if (!Resolve(uid, ref handsComp))
@@ -137,6 +136,43 @@ public abstract partial class SharedHandsSystem
return false;
}
public bool TryGetActiveHand(Entity<HandsComponent?> entity, [NotNullWhen(true)] out Hand? hand)
{
if (!Resolve(entity, ref entity.Comp, false))
{
hand = null;
return false;
}
hand = entity.Comp.ActiveHand;
return hand != null;
}
public bool TryGetActiveItem(Entity<HandsComponent?> entity, [NotNullWhen(true)] out EntityUid? item)
{
if (!TryGetActiveHand(entity, out var hand))
{
item = null;
return false;
}
item = hand.HeldEntity;
return item != null;
}
public Hand? GetActiveHand(Entity<HandsComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp))
return null;
return entity.Comp.ActiveHand;
}
public EntityUid? GetActiveItem(Entity<HandsComponent?> entity)
{
return GetActiveHand(entity)?.HeldEntity;
}
/// <summary>
/// Enumerate over hands, starting with the currently active hand.
/// </summary>
@@ -227,9 +263,17 @@ public abstract partial class SharedHandsSystem
return true;
}
public bool IsHolding(EntityUid uid, EntityUid? entity, [NotNullWhen(true)] out Hand? inHand, HandsComponent? handsComp = null)
public bool IsHolding(Entity<HandsComponent?> entity, [NotNullWhen(true)] EntityUid? item)
{
return IsHolding(entity, item, out _, entity);
}
public bool IsHolding(EntityUid uid, [NotNullWhen(true)] EntityUid? entity, [NotNullWhen(true)] out Hand? inHand, HandsComponent? handsComp = null)
{
inHand = null;
if (entity == null)
return false;
if (!Resolve(uid, ref handsComp, false))
return false;

View File

@@ -25,6 +25,7 @@ namespace Content.Shared.Input
public static readonly BoundKeyFunction CycleChatChannelBackward = "CycleChatChannelBackward";
public static readonly BoundKeyFunction EscapeContext = "EscapeContext";
public static readonly BoundKeyFunction OpenCharacterMenu = "OpenCharacterMenu";
public static readonly BoundKeyFunction OpenEmotesMenu = "OpenEmotesMenu";
public static readonly BoundKeyFunction OpenCraftingMenu = "OpenCraftingMenu";
public static readonly BoundKeyFunction OpenGuidebook = "OpenGuidebook";
public static readonly BoundKeyFunction OpenInventoryMenu = "OpenInventoryMenu";

View File

@@ -123,6 +123,11 @@ public sealed class InventoryRelayedEvent<TEvent> : EntityEventArgs
}
}
public interface IClothingSlots
{
SlotFlags Slots { get; }
}
/// <summary>
/// Events that should be relayed to inventory slots should implement this interface.
/// </summary>

View File

@@ -27,6 +27,31 @@ public partial class InventorySystem : EntitySystem
.RemoveHandler(HandleViewVariablesSlots, ListViewVariablesSlots);
}
/// <summary>
/// Tries to find an entity in the specified slot with the specified component.
/// </summary>
public bool TryGetInventoryEntity<T>(Entity<InventoryComponent?> entity, out EntityUid targetUid)
where T : IComponent, IClothingSlots
{
if (TryGetContainerSlotEnumerator(entity.Owner, out var containerSlotEnumerator))
{
while (containerSlotEnumerator.NextItem(out var item, out var slot))
{
if (!TryComp<T>(item, out var required))
continue;
if ((((IClothingSlots) required).Slots & slot.SlotFlags) == 0x0)
continue;
targetUid = item;
return true;
}
}
targetUid = EntityUid.Invalid;
return false;
}
protected virtual void OnInit(EntityUid uid, InventoryComponent component, ComponentInit args)
{
if (!_prototypeManager.TryIndex(component.TemplateId, out InventoryTemplatePrototype? invTemplate))

View File

@@ -32,4 +32,6 @@ public enum SlotFlags
CLOAK = 1 << 19,
KEYS = 1 << 20,
All = ~NONE,
WITHOUT_POCKET = All & ~POCKET
}

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

View File

@@ -2,7 +2,6 @@ using Content.Shared.Decals;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Light.Components;
@@ -17,7 +16,7 @@ public sealed partial class UnpoweredFlashlightComponent : Component
public SoundSpecifier ToggleSound = new SoundPathSpecifier("/Audio/Items/flashlight_pda.ogg");
[DataField, AutoNetworkedField]
public bool LightOn = false;
public bool LightOn;
[DataField]
public EntProtoId ToggleAction = "ActionToggleLight";

View File

@@ -0,0 +1,122 @@
using Content.Shared.Actions;
using Content.Shared.Emag.Systems;
using Content.Shared.Light.Components;
using Content.Shared.Mind.Components;
using Content.Shared.Toggleable;
using Content.Shared.Verbs;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Shared.Light.EntitySystems;
public sealed class UnpoweredFlashlightSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
[Dependency] private readonly ActionContainerSystem _actionContainer = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly SharedPointLightSystem _light = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<UnpoweredFlashlightComponent, GetVerbsEvent<ActivationVerb>>(AddToggleLightVerbs);
SubscribeLocalEvent<UnpoweredFlashlightComponent, GetItemActionsEvent>(OnGetActions);
SubscribeLocalEvent<UnpoweredFlashlightComponent, ToggleActionEvent>(OnToggleAction);
SubscribeLocalEvent<UnpoweredFlashlightComponent, MindAddedMessage>(OnMindAdded);
SubscribeLocalEvent<UnpoweredFlashlightComponent, GotEmaggedEvent>(OnGotEmagged);
SubscribeLocalEvent<UnpoweredFlashlightComponent, MapInitEvent>(OnMapInit);
}
private void OnMapInit(EntityUid uid, UnpoweredFlashlightComponent component, MapInitEvent args)
{
_actionContainer.EnsureAction(uid, ref component.ToggleActionEntity, component.ToggleAction);
Dirty(uid, component);
}
private void OnToggleAction(EntityUid uid, UnpoweredFlashlightComponent component, ToggleActionEvent args)
{
if (args.Handled)
return;
TryToggleLight((uid, component), args.Performer);
args.Handled = true;
}
private void OnGetActions(EntityUid uid, UnpoweredFlashlightComponent component, GetItemActionsEvent args)
{
args.AddAction(component.ToggleActionEntity);
}
private void AddToggleLightVerbs(EntityUid uid, UnpoweredFlashlightComponent component, GetVerbsEvent<ActivationVerb> args)
{
if (!args.CanAccess || !args.CanInteract)
return;
ActivationVerb verb = new()
{
Text = Loc.GetString("toggle-flashlight-verb-get-data-text"),
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/light.svg.192dpi.png")),
Act = () => TryToggleLight((uid, component), args.User),
Priority = -1 // For things like PDA's, Open-UI and other verbs that should be higher priority.
};
args.Verbs.Add(verb);
}
private void OnMindAdded(EntityUid uid, UnpoweredFlashlightComponent component, MindAddedMessage args)
{
_actionsSystem.AddAction(uid, ref component.ToggleActionEntity, component.ToggleAction);
}
private void OnGotEmagged(EntityUid uid, UnpoweredFlashlightComponent component, ref GotEmaggedEvent args)
{
if (!_light.TryGetLight(uid, out var light))
return;
if (_prototypeManager.TryIndex(component.EmaggedColorsPrototype, out var possibleColors))
{
var pick = _random.Pick(possibleColors.Colors.Values);
_light.SetColor(uid, pick, light);
}
args.Repeatable = true;
args.Handled = true;
}
public void TryToggleLight(Entity<UnpoweredFlashlightComponent?> ent, EntityUid? user = null, bool quiet = false)
{
if (!Resolve(ent, ref ent.Comp, false))
return;
SetLight(ent, !ent.Comp.LightOn, user, quiet);
}
public void SetLight(Entity<UnpoweredFlashlightComponent?> ent, bool value, EntityUid? user = null, bool quiet = false)
{
if (!Resolve(ent, ref ent.Comp))
return;
if (ent.Comp.LightOn == value)
return;
if (!_light.TryGetLight(ent, out var light))
return;
Dirty(ent);
ent.Comp.LightOn = value;
_light.SetEnabled(ent, value, light);
_appearance.SetData(ent, UnpoweredFlashlightVisuals.LightOn, value);
if (!quiet)
_audioSystem.PlayPredicted(ent.Comp.ToggleSound, ent, user);
_actionsSystem.SetToggled(ent.Comp.ToggleActionEntity, value);
RaiseLocalEvent(ent, new LightToggleEvent(value));
}
}

View File

@@ -0,0 +1,6 @@
namespace Content.Shared.Light;
public sealed class LightToggleEvent(bool isOn) : EntityEventArgs
{
public bool IsOn = isOn;
}

View File

@@ -79,7 +79,7 @@ namespace Content.Shared.Localizations
var maxDecimals = (int)Math.Floor(((LocValueNumber) args.Args[1]).Value);
var formatter = (NumberFormatInfo)NumberFormatInfo.GetInstance(CultureInfo.GetCultureInfo(Culture)).Clone();
formatter.NumberDecimalDigits = maxDecimals;
return new LocValueString(string.Format(formatter, "{0:N}", number).TrimEnd('0').TrimEnd('.') + "%");
return new LocValueString(string.Format(formatter, "{0:N}", number).TrimEnd('0').TrimEnd(char.Parse(formatter.NumberDecimalSeparator)) + "%");
}
private ILocValue FormatNaturalFixed(LocArgs args)
@@ -88,7 +88,7 @@ namespace Content.Shared.Localizations
var maxDecimals = (int)Math.Floor(((LocValueNumber) args.Args[1]).Value);
var formatter = (NumberFormatInfo)NumberFormatInfo.GetInstance(CultureInfo.GetCultureInfo(Culture)).Clone();
formatter.NumberDecimalDigits = maxDecimals;
return new LocValueString(string.Format(formatter, "{0:N}", number).TrimEnd('0').TrimEnd('.'));
return new LocValueString(string.Format(formatter, "{0:N}", number).TrimEnd('0').TrimEnd(char.Parse(formatter.NumberDecimalSeparator)));
}
private static readonly Regex PluralEsRule = new("^.*(s|sh|ch|x|z)$");

View File

@@ -259,7 +259,7 @@ namespace Content.Shared.Movement.Systems
}
var oldMapId = args.OldMapId;
var mapId = args.Transform.MapID;
var mapId = args.Transform.MapUid;
// If we change maps then reset eye rotation entirely.
if (oldMapId != mapId)

View File

@@ -0,0 +1,6 @@
namespace Content.Shared.Preferences.Loadouts.Effects;
public sealed class SpeciesLoadoutEffect
{
}

View File

@@ -0,0 +1,53 @@
using Content.Shared.Chat.Prototypes;
using Content.Shared.Humanoid;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
namespace Content.Shared.Speech.Components;
/// <summary>
/// Component required for entities to be able to do vocal emotions.
/// </summary>
[RegisterComponent, NetworkedComponent]
[AutoGenerateComponentState]
public sealed partial class VocalComponent : Component
{
/// <summary>
/// Emote sounds prototype id for each sex (not gender).
/// Entities without <see cref="HumanoidComponent"/> considered to be <see cref="Sex.Unsexed"/>.
/// </summary>
[DataField("sounds", customTypeSerializer: typeof(PrototypeIdValueDictionarySerializer<Sex, EmoteSoundsPrototype>))]
[AutoNetworkedField]
public Dictionary<Sex, string>? Sounds;
[DataField("screamId", customTypeSerializer: typeof(PrototypeIdSerializer<EmotePrototype>))]
[AutoNetworkedField]
public string ScreamId = "Scream";
[DataField("wilhelm")]
[AutoNetworkedField]
public SoundSpecifier Wilhelm = new SoundPathSpecifier("/Audio/Voice/Human/wilhelm_scream.ogg");
[DataField("wilhelmProbability")]
[AutoNetworkedField]
public float WilhelmProbability = 0.0002f;
[DataField("screamAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
[AutoNetworkedField]
public string ScreamAction = "ActionScream";
[DataField("screamActionEntity")]
[AutoNetworkedField]
public EntityUid? ScreamActionEntity;
/// <summary>
/// Currently loaded emote sounds prototype, based on entity sex.
/// Null if no valid prototype for entity sex was found.
/// </summary>
[ViewVariables]
[AutoNetworkedField]
public EmoteSoundsPrototype? EmoteSounds = null;
}

View File

@@ -1,3 +1,4 @@
using Content.Shared.Chat.Prototypes;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
@@ -26,6 +27,13 @@ namespace Content.Shared.Speech
[DataField]
public ProtoId<SpeechVerbPrototype> SpeechVerb = "Default";
/// <summary>
/// What emotes allowed to use event if emote <see cref="EmotePrototype.Available"/> is false
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public List<ProtoId<EmotePrototype>> AllowedEmotes = new();
/// <summary>
/// A mapping from chat suffixes loc strings to speech verb prototypes that should be conditionally used.
/// For things like '?' changing to 'asks' or '!!' making text bold and changing to 'yells'. Can be overridden if necessary.

View File

@@ -113,7 +113,7 @@ public abstract class SharedStealthSystem : EntitySystem
private void OnMove(EntityUid uid, StealthOnMoveComponent component, ref MoveEvent args)
{
if (args.FromStateHandling)
if (_timing.ApplyingState)
return;
if (args.NewPosition.EntityId != args.OldPosition.EntityId)

View File

@@ -0,0 +1,10 @@
using Content.Shared.Inventory;
using Robust.Shared.GameStates;
namespace Content.Shared.StepTrigger.Components;
/// <summary>
/// This is used for marking step trigger events that require the user to wear shoes, such as for glass shards.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class ClothingRequiredStepTriggerComponent : Component;

View File

@@ -0,0 +1,16 @@
using Content.Shared.Inventory;
using Content.Shared.StepTrigger.Systems;
using Robust.Shared.GameStates;
namespace Content.Shared.StepTrigger.Components;
/// <summary>
/// This is used for cancelling step trigger events if the user is wearing clothing in a valid slot.
/// </summary>
[RegisterComponent, NetworkedComponent]
[Access(typeof(StepTriggerImmuneSystem))]
public sealed partial class ClothingRequiredStepTriggerImmuneComponent : Component, IClothingSlots
{
[DataField]
public SlotFlags Slots { get; set; } = SlotFlags.FEET;
}

View File

@@ -1,11 +0,0 @@
using Robust.Shared.GameStates;
namespace Content.Shared.StepTrigger.Components;
/// <summary>
/// This is used for cancelling step trigger events if the user is wearing shoes, such as for glass shards.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class ShoesRequiredStepTriggerComponent : Component
{
}

View File

@@ -0,0 +1,9 @@
using Robust.Shared.GameStates;
namespace Content.Shared.StepTrigger.Components;
/// <summary>
/// Grants the attached entity to step triggers.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class StepTriggerImmuneComponent : Component;

View File

@@ -1,41 +0,0 @@
using Content.Shared.Examine;
using Content.Shared.Inventory;
using Content.Shared.StepTrigger.Components;
using Content.Shared.Tag;
namespace Content.Shared.StepTrigger.Systems;
public sealed class ShoesRequiredStepTriggerSystem : EntitySystem
{
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly TagSystem _tagSystem = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<ShoesRequiredStepTriggerComponent, StepTriggerAttemptEvent>(OnStepTriggerAttempt);
SubscribeLocalEvent<ShoesRequiredStepTriggerComponent, ExaminedEvent>(OnExamined);
}
private void OnStepTriggerAttempt(EntityUid uid, ShoesRequiredStepTriggerComponent component, ref StepTriggerAttemptEvent args)
{
if (_tagSystem.HasTag(args.Tripper, "ShoesRequiredStepTriggerImmune"))
{
args.Cancelled = true;
return;
}
if (!TryComp<InventoryComponent>(args.Tripper, out var inventory))
return;
if (_inventory.TryGetSlotEntity(args.Tripper, "shoes", out _, inventory))
{
args.Cancelled = true;
}
}
private void OnExamined(EntityUid uid, ShoesRequiredStepTriggerComponent component, ExaminedEvent args)
{
args.PushMarkup(Loc.GetString("shoes-required-step-trigger-examine"));
}
}

View File

@@ -0,0 +1,37 @@
using Content.Shared.Examine;
using Content.Shared.Inventory;
using Content.Shared.StepTrigger.Components;
using Content.Shared.Tag;
namespace Content.Shared.StepTrigger.Systems;
public sealed class StepTriggerImmuneSystem : EntitySystem
{
[Dependency] private readonly InventorySystem _inventory = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<StepTriggerImmuneComponent, StepTriggerAttemptEvent>(OnStepTriggerAttempt);
SubscribeLocalEvent<ClothingRequiredStepTriggerComponent, StepTriggerAttemptEvent>(OnStepTriggerClothingAttempt);
SubscribeLocalEvent<ClothingRequiredStepTriggerComponent, ExaminedEvent>(OnExamined);
}
private void OnStepTriggerAttempt(Entity<StepTriggerImmuneComponent> ent, ref StepTriggerAttemptEvent args)
{
args.Cancelled = true;
}
private void OnStepTriggerClothingAttempt(EntityUid uid, ClothingRequiredStepTriggerComponent component, ref StepTriggerAttemptEvent args)
{
if (_inventory.TryGetInventoryEntity<ClothingRequiredStepTriggerImmuneComponent>(args.Tripper, out _))
{
args.Cancelled = true;
}
}
private void OnExamined(EntityUid uid, ClothingRequiredStepTriggerComponent component, ExaminedEvent args)
{
args.PushMarkup(Loc.GetString("clothing-required-step-trigger-examine"));
}
}

View File

@@ -145,27 +145,14 @@ public sealed class SwapTeleporterSystem : EntitySystem
}
var teleEnt = GetTeleportingEntity((uid, xform));
var teleEntXform = Transform(teleEnt);
var otherTeleEnt = GetTeleportingEntity((linkedEnt, Transform(linkedEnt)));
var otherTeleEntXform = Transform(otherTeleEnt);
_popup.PopupEntity(Loc.GetString("swap-teleporter-popup-teleport-other",
("entity", Identity.Entity(linkedEnt, EntityManager))),
otherTeleEnt,
otherTeleEnt,
PopupType.MediumCaution);
var pos = teleEntXform.Coordinates;
var otherPos = otherTeleEntXform.Coordinates;
if (_transform.ContainsEntity(teleEnt, (otherTeleEnt, otherTeleEntXform)) ||
_transform.ContainsEntity(otherTeleEnt, (teleEnt, teleEntXform)))
{
Log.Error($"Invalid teleport swap attempt between {ToPrettyString(teleEnt)} and {ToPrettyString(otherTeleEnt)}");
return;
}
_transform.SetCoordinates(teleEnt, otherPos);
_transform.SetCoordinates(otherTeleEnt, pos);
_transform.SwapPositions(teleEnt, otherTeleEnt);
}
/// <remarks>

View File

@@ -0,0 +1,19 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Tips;
[Serializable, NetSerializable]
public sealed class TippyEvent : EntityEventArgs
{
public TippyEvent(string msg)
{
Msg = msg;
}
public string Msg;
public string? Proto;
public float SpeakTime = 5;
public float SlideTime = 3;
public float WaddleInterval = 0.5f;
}

View File

@@ -4,22 +4,26 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations;
namespace Content.Shared.UserInterface
{
[RegisterComponent, NetworkedComponent]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ActivatableUIComponent : Component
{
[DataField(required: true, customTypeSerializer: typeof(EnumSerializer))]
public Enum Key { get; set; } = default!;
public Enum? Key;
/// <summary>
/// Whether the item must be held in one of the user's hands to work.
/// This is ignored unless <see cref="RequireHands"/> is true.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public bool InHandsOnly;
[DataField]
public bool SingleUser;
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public bool InHandsOnly { get; set; } = false;
[DataField]
public bool SingleUser { get; set; } = false;
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public bool AdminOnly { get; set; } = false;
public bool AdminOnly;
[DataField]
public LocId VerbText = "ui-verb-toggle-open";
@@ -38,16 +42,15 @@ namespace Content.Shared.UserInterface
/// <summary>
/// Entities that are required to open this UI.
/// </summary>
[DataField("allowedItems")]
[ViewVariables(VVAccess.ReadWrite)]
public EntityWhitelist? AllowedItems = null;
[DataField, ViewVariables(VVAccess.ReadWrite)]
public EntityWhitelist? RequiredItems;
/// <summary>
/// Whether you can activate this ui with activateinhand or not
/// If true, then this UI can only be opened via verbs. I.e., normal interactions/activations will not open
/// the UI.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public bool RightClickOnly;
[DataField, ViewVariables(VVAccess.ReadWrite)]
public bool VerbOnly;
/// <summary>
/// Whether spectators (non-admin ghosts) should be allowed to view this UI.
@@ -57,17 +60,18 @@ namespace Content.Shared.UserInterface
public bool AllowSpectator = true;
/// <summary>
/// Whether the UI should close when the item is deselected due to a hand swap or drop
/// Whether the item must be in the user's currently selected/active hand.
/// This is ignored unless <see cref="InHandsOnly"/> is true.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public bool CloseOnHandDeselect = true;
public bool RequireActiveHand = true;
/// <summary>
/// The client channel currently using the object, or null if there's none/not single user.
/// NOTE: DO NOT DIRECTLY SET, USE ActivatableUISystem.SetCurrentSingleUser
/// </summary>
[ViewVariables]
[DataField, AutoNetworkedField]
public EntityUid? CurrentSingleUser;
}
}

View File

@@ -20,11 +20,19 @@ public sealed partial class ActivatableUISystem
{
_cell.SetPowerCellDrawEnabled(uid, false);
if (HasComp<ActivatableUIRequiresPowerCellComponent>(uid) &&
TryComp<ActivatableUIComponent>(uid, out var activatable))
if (!HasComp<ActivatableUIRequiresPowerCellComponent>(uid) ||
!TryComp(uid, out ActivatableUIComponent? activatable))
{
_uiSystem.CloseUi(uid, activatable.Key);
return;
}
if (activatable.Key == null)
{
Log.Error($"Encountered null key in activatable ui on entity {ToPrettyString(uid)}");
return;
}
_uiSystem.CloseUi(uid, activatable.Key);
}
private void OnBatteryOpened(EntityUid uid, ActivatableUIRequiresPowerCellComponent component, BoundUIOpenedEvent args)
@@ -57,6 +65,12 @@ public sealed partial class ActivatableUISystem
if (!Resolve(uid, ref component, ref draw, ref active, false))
return;
if (active.Key == null)
{
Log.Error($"Encountered null key in activatable ui on entity {ToPrettyString(uid)}");
return;
}
if (_cell.HasActivatableCharge(uid))
return;

View File

@@ -3,11 +3,11 @@ using Content.Shared.Administration.Managers;
using Content.Shared.Ghost;
using Content.Shared.Hands;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Popups;
using Content.Shared.Verbs;
using Robust.Shared.Player;
using Robust.Shared.Containers;
namespace Content.Shared.UserInterface;
@@ -17,23 +17,31 @@ public sealed partial class ActivatableUISystem : EntitySystem
[Dependency] private readonly ActionBlockerSystem _blockerSystem = default!;
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
private readonly List<EntityUid> _toClose = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ActivatableUIComponent, ActivateInWorldEvent>(OnActivate);
SubscribeLocalEvent<ActivatableUIComponent, UseInHandEvent>(OnUseInHand);
SubscribeLocalEvent<ActivatableUIComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<ActivatableUIComponent, HandDeselectedEvent>(OnHandDeselected);
SubscribeLocalEvent<ActivatableUIComponent, GotUnequippedHandEvent>((uid, aui, _) => CloseAll(uid, aui));
// *THIS IS A BLATANT WORKAROUND!* RATIONALE: Microwaves need it
SubscribeLocalEvent<ActivatableUIComponent, EntParentChangedMessage>(OnParentChanged);
SubscribeLocalEvent<ActivatableUIComponent, GotUnequippedHandEvent>(OnHandUnequipped);
SubscribeLocalEvent<ActivatableUIComponent, BoundUIClosedEvent>(OnUIClose);
SubscribeLocalEvent<ActivatableUIComponent, GetVerbsEvent<ActivationVerb>>(GetActivationVerb);
SubscribeLocalEvent<ActivatableUIComponent, GetVerbsEvent<Verb>>(GetVerb);
// TODO ActivatableUI
// Add UI-user component, and listen for user container changes.
// I.e., should lose a computer UI if a player gets shut into a locker.
SubscribeLocalEvent<ActivatableUIComponent, EntGotInsertedIntoContainerMessage>(OnGotInserted);
SubscribeLocalEvent<ActivatableUIComponent, EntGotRemovedFromContainerMessage>(OnGotRemoved);
SubscribeLocalEvent<BoundUserInterfaceMessageAttempt>(OnBoundInterfaceInteractAttempt);
SubscribeLocalEvent<ActivatableUIComponent, GetVerbsEvent<ActivationVerb>>(AddOpenUiVerb);
SubscribeLocalEvent<UserInterfaceComponent, OpenUiActionEvent>(OnActionPerform);
InitializePower();
@@ -59,25 +67,54 @@ public sealed partial class ActivatableUISystem : EntitySystem
args.Handled = _uiSystem.TryToggleUi(uid, args.Key, args.Performer);
}
private void AddOpenUiVerb(EntityUid uid, ActivatableUIComponent component, GetVerbsEvent<ActivationVerb> args)
private void GetActivationVerb(EntityUid uid, ActivatableUIComponent component, GetVerbsEvent<ActivationVerb> args)
{
if (component.VerbOnly || !ShouldAddVerb(uid, component, args))
return;
args.Verbs.Add(new ActivationVerb
{
// TODO VERBS add "open UI" icon
Act = () => InteractUI(args.User, uid, component),
Text = Loc.GetString(component.VerbText)
});
}
private void GetVerb(EntityUid uid, ActivatableUIComponent component, GetVerbsEvent<Verb> args)
{
if (!component.VerbOnly || !ShouldAddVerb(uid, component, args))
return;
args.Verbs.Add(new Verb
{
// TODO VERBS add "open UI" icon
Act = () => InteractUI(args.User, uid, component),
Text = Loc.GetString(component.VerbText)
});
}
private bool ShouldAddVerb<T>(EntityUid uid, ActivatableUIComponent component, GetVerbsEvent<T> args) where T : Verb
{
if (!args.CanAccess)
return;
return false;
if (component.RequireHands && args.Hands == null)
return;
if (component.RequireHands)
{
if (args.Hands == null)
return false;
if (component.InHandsOnly && args.Using != uid)
return;
if (component.InHandsOnly)
{
if (!_hands.IsHolding(args.User, uid, out var hand, args.Hands))
return false;
if (!args.CanInteract && (!component.AllowSpectator || !HasComp<GhostComponent>(args.User)))
return;
if (component.RequireActiveHand && args.Hands.ActiveHand != hand)
return false;
}
}
ActivationVerb verb = new();
verb.Act = () => InteractUI(args.User, uid, component);
verb.Text = Loc.GetString(component.VerbText);
// TODO VERBS add "open UI" icon?
args.Verbs.Add(verb);
return args.CanInteract || component.AllowSpectator && HasComp<GhostComponent>(args.User);
}
private void OnActivate(EntityUid uid, ActivatableUIComponent component, ActivateInWorldEvent args)
@@ -85,24 +122,10 @@ public sealed partial class ActivatableUISystem : EntitySystem
if (args.Handled)
return;
if (component.InHandsOnly)
if (component.VerbOnly)
return;
if (component.AllowedItems != null)
return;
args.Handled = InteractUI(args.User, uid, component);
}
private void OnUseInHand(EntityUid uid, ActivatableUIComponent component, UseInHandEvent args)
{
if (args.Handled)
return;
if (component.RightClickOnly)
return;
if (component.AllowedItems != null)
if (component.RequiredItems != null)
return;
args.Handled = InteractUI(args.User, uid, component);
@@ -110,15 +133,19 @@ public sealed partial class ActivatableUISystem : EntitySystem
private void OnInteractUsing(EntityUid uid, ActivatableUIComponent component, InteractUsingEvent args)
{
if (args.Handled) return;
if (component.AllowedItems == null) return;
if (!component.AllowedItems.IsValid(args.Used, EntityManager)) return;
args.Handled = InteractUI(args.User, uid, component);
}
if (args.Handled)
return;
private void OnParentChanged(EntityUid uid, ActivatableUIComponent aui, ref EntParentChangedMessage args)
{
CloseAll(uid, aui);
if (component.VerbOnly)
return;
if (component.RequiredItems == null)
return;
if (!component.RequiredItems.IsValid(args.Used, EntityManager))
return;
args.Handled = InteractUI(args.User, uid, component);
}
private void OnUIClose(EntityUid uid, ActivatableUIComponent component, BoundUIClosedEvent args)
@@ -136,7 +163,7 @@ public sealed partial class ActivatableUISystem : EntitySystem
private bool InteractUI(EntityUid user, EntityUid uiEntity, ActivatableUIComponent aui)
{
if (!_uiSystem.HasUi(uiEntity, aui.Key))
if (aui.Key == null || !_uiSystem.HasUi(uiEntity, aui.Key))
return false;
if (_uiSystem.IsUiOpen(uiEntity, aui.Key, user))
@@ -148,22 +175,33 @@ public sealed partial class ActivatableUISystem : EntitySystem
if (!_blockerSystem.CanInteract(user, uiEntity) && (!aui.AllowSpectator || !HasComp<GhostComponent>(user)))
return false;
if (aui.RequireHands && !HasComp<HandsComponent>(user))
return false;
if (aui.RequireHands)
{
if (!TryComp(user, out HandsComponent? hands))
return false;
if (aui.InHandsOnly)
{
if (!_hands.IsHolding(user, uiEntity, out var hand, hands))
return false;
if (aui.RequireActiveHand && hands.ActiveHand != hand)
return false;
}
}
if (aui.AdminOnly && !_adminManager.IsAdmin(user))
return false;
if (aui.SingleUser && aui.CurrentSingleUser != null && user != aui.CurrentSingleUser)
{
string message = Loc.GetString("machine-already-in-use", ("machine", uiEntity));
var message = Loc.GetString("machine-already-in-use", ("machine", uiEntity));
_popupSystem.PopupEntity(message, uiEntity, user);
// If we get here, supposedly, the object is in use.
// Check with BUI that it's ACTUALLY in use just in case.
// Since this could brick the object if it goes wrong.
if (_uiSystem.IsUiOpen(uiEntity, aui.Key))
return false;
return true;
Log.Error($"Activatable UI has user without being opened? Entity: {ToPrettyString(uiEntity)}. User: {aui.CurrentSingleUser}, Key: {aui.Key}");
}
// If we've gotten this far, fire a cancellable event that indicates someone is about to activate this.
@@ -199,6 +237,7 @@ public sealed partial class ActivatableUISystem : EntitySystem
return;
aui.CurrentSingleUser = user;
Dirty(uid, aui);
RaiseLocalEvent(uid, new ActivatableUIPlayerChangedEvent());
}
@@ -208,17 +247,67 @@ public sealed partial class ActivatableUISystem : EntitySystem
if (!Resolve(uid, ref aui, false))
return;
if (aui.Key == null)
{
Log.Error($"Encountered null key in activatable ui on entity {ToPrettyString(uid)}");
return;
}
_uiSystem.CloseUi(uid, aui.Key);
}
private void OnHandDeselected(EntityUid uid, ActivatableUIComponent? aui, HandDeselectedEvent args)
private void OnHandDeselected(Entity<ActivatableUIComponent> ent, ref HandDeselectedEvent args)
{
if (!Resolve(uid, ref aui, false))
if (ent.Comp.RequireHands && ent.Comp.InHandsOnly && ent.Comp.RequireActiveHand)
CloseAll(ent, ent);
}
private void OnHandUnequipped(Entity<ActivatableUIComponent> ent, ref GotUnequippedHandEvent args)
{
if (ent.Comp.RequireHands && ent.Comp.InHandsOnly)
CloseAll(ent, ent);
}
private void OnGotInserted(Entity<ActivatableUIComponent> ent, ref EntGotInsertedIntoContainerMessage args)
{
CheckAccess((ent, ent));
}
private void OnGotRemoved(Entity<ActivatableUIComponent> ent, ref EntGotRemovedFromContainerMessage args)
{
CheckAccess((ent, ent));
}
public void CheckAccess(Entity<ActivatableUIComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp))
return;
if (!aui.CloseOnHandDeselect)
if (ent.Comp.Key == null)
{
Log.Error($"Encountered null key in activatable ui on entity {ToPrettyString(ent)}");
return;
}
CloseAll(uid, aui);
foreach (var user in _uiSystem.GetActors(ent.Owner, ent.Comp.Key))
{
if (!_container.IsInSameOrParentContainer(user, ent)
&& !_interaction.CanAccessViaStorage(user, ent))
{
_toClose.Add(user);
continue;
}
if (!_interaction.InRangeUnobstructed(user, ent))
_toClose.Add(user);
}
foreach (var user in _toClose)
{
_uiSystem.CloseUi(ent.Owner, ent.Comp.Key, user);
}
_toClose.Clear();
}
}

View File

@@ -55,13 +55,21 @@ namespace Content.Shared.Verbs
return GetLocalVerbs(target, user, new List<Type>() { type }, force);
}
/// <inheritdoc cref="GetLocalVerbs(Robust.Shared.GameObjects.EntityUid,Robust.Shared.GameObjects.EntityUid,System.Type,bool)"/>
public SortedSet<Verb> GetLocalVerbs(EntityUid target, EntityUid user, List<Type> types, bool force = false)
{
return GetLocalVerbs(target, user, types, out _, force);
}
/// <summary>
/// Raises a number of events in order to get all verbs of the given type(s) defined in local systems. This
/// does not request verbs from the server.
/// </summary>
public SortedSet<Verb> GetLocalVerbs(EntityUid target, EntityUid user, List<Type> types, bool force = false)
public SortedSet<Verb> GetLocalVerbs(EntityUid target, EntityUid user, List<Type> types,
out List<VerbCategory> extraCategories, bool force = false)
{
SortedSet<Verb> verbs = new();
extraCategories = new();
// accessibility checks
bool canAccess = false;
@@ -108,7 +116,7 @@ namespace Content.Shared.Verbs
// TODO: fix this garbage and use proper generics or reflection or something else, not this.
if (types.Contains(typeof(InteractionVerb)))
{
var verbEvent = new GetVerbsEvent<InteractionVerb>(user, target, @using, hands, canInteract, canAccess);
var verbEvent = new GetVerbsEvent<InteractionVerb>(user, target, @using, hands, canInteract, canAccess, extraCategories);
RaiseLocalEvent(target, verbEvent, true);
verbs.UnionWith(verbEvent.Verbs);
}
@@ -117,35 +125,35 @@ namespace Content.Shared.Verbs
&& @using != null
&& @using != target)
{
var verbEvent = new GetVerbsEvent<UtilityVerb>(user, target, @using, hands, canInteract, canAccess);
var verbEvent = new GetVerbsEvent<UtilityVerb>(user, target, @using, hands, canInteract, canAccess, extraCategories);
RaiseLocalEvent(@using.Value, verbEvent, true); // directed at used, not at target
verbs.UnionWith(verbEvent.Verbs);
}
if (types.Contains(typeof(InnateVerb)))
{
var verbEvent = new GetVerbsEvent<InnateVerb>(user, target, @using, hands, canInteract, canAccess);
var verbEvent = new GetVerbsEvent<InnateVerb>(user, target, @using, hands, canInteract, canAccess, extraCategories);
RaiseLocalEvent(user, verbEvent, true);
verbs.UnionWith(verbEvent.Verbs);
}
if (types.Contains(typeof(AlternativeVerb)))
{
var verbEvent = new GetVerbsEvent<AlternativeVerb>(user, target, @using, hands, canInteract, canAccess);
var verbEvent = new GetVerbsEvent<AlternativeVerb>(user, target, @using, hands, canInteract, canAccess, extraCategories);
RaiseLocalEvent(target, verbEvent, true);
verbs.UnionWith(verbEvent.Verbs);
}
if (types.Contains(typeof(ActivationVerb)))
{
var verbEvent = new GetVerbsEvent<ActivationVerb>(user, target, @using, hands, canInteract, canAccess);
var verbEvent = new GetVerbsEvent<ActivationVerb>(user, target, @using, hands, canInteract, canAccess, extraCategories);
RaiseLocalEvent(target, verbEvent, true);
verbs.UnionWith(verbEvent.Verbs);
}
if (types.Contains(typeof(ExamineVerb)))
{
var verbEvent = new GetVerbsEvent<ExamineVerb>(user, target, @using, hands, canInteract, canAccess);
var verbEvent = new GetVerbsEvent<ExamineVerb>(user, target, @using, hands, canInteract, canAccess, extraCategories);
RaiseLocalEvent(target, verbEvent, true);
verbs.UnionWith(verbEvent.Verbs);
}
@@ -153,7 +161,7 @@ namespace Content.Shared.Verbs
// generic verbs
if (types.Contains(typeof(Verb)))
{
var verbEvent = new GetVerbsEvent<Verb>(user, target, @using, hands, canInteract, canAccess);
var verbEvent = new GetVerbsEvent<Verb>(user, target, @using, hands, canInteract, canAccess, extraCategories);
RaiseLocalEvent(target, verbEvent, true);
verbs.UnionWith(verbEvent.Verbs);
}
@@ -161,7 +169,7 @@ namespace Content.Shared.Verbs
if (types.Contains(typeof(EquipmentVerb)))
{
var access = canAccess || _interactionSystem.CanAccessEquipment(user, target);
var verbEvent = new GetVerbsEvent<EquipmentVerb>(user, target, @using, hands, canInteract, access);
var verbEvent = new GetVerbsEvent<EquipmentVerb>(user, target, @using, hands, canInteract, access, extraCategories);
RaiseLocalEvent(target, verbEvent);
verbs.UnionWith(verbEvent.Verbs);
}

View File

@@ -77,6 +77,13 @@ namespace Content.Shared.Verbs
/// </summary>
public readonly SortedSet<TVerb> Verbs = new();
/// <summary>
/// Additional verb categories to show in the pop-up menu, even if there are no verbs currently associated
/// with that category. This is mainly useful to prevent verb menu pop-in. E.g., admins will get admin/debug
/// related verbs on entities, even though most of those verbs are all defined server-side.
/// </summary>
public readonly List<VerbCategory> ExtraCategories;
/// <summary>
/// Can the user physically access the target?
/// </summary>
@@ -123,7 +130,7 @@ namespace Content.Shared.Verbs
/// </remarks>
public readonly EntityUid? Using;
public GetVerbsEvent(EntityUid user, EntityUid target, EntityUid? @using, HandsComponent? hands, bool canInteract, bool canAccess)
public GetVerbsEvent(EntityUid user, EntityUid target, EntityUid? @using, HandsComponent? hands, bool canInteract, bool canAccess, List<VerbCategory> extraCategories)
{
User = user;
Target = target;
@@ -131,6 +138,7 @@ namespace Content.Shared.Verbs
Hands = hands;
CanAccess = canAccess;
CanInteract = canInteract;
ExtraCategories = extraCategories;
}
}
}

View File

@@ -17,9 +17,6 @@ public sealed partial class WaggingComponent : Component
[DataField]
public EntityUid? ActionEntity;
[DataField]
public ProtoId<EmotePrototype> EmoteId = "WagTail";
/// <summary>
/// Suffix to add to get the animated marking.
/// </summary>

View File

@@ -57,7 +57,7 @@ public sealed class ReflectSystem : EntitySystem
if (args.Reflected)
return;
foreach (var ent in _inventorySystem.GetHandOrInventoryEntities(uid, SlotFlags.All & ~SlotFlags.POCKET))
foreach (var ent in _inventorySystem.GetHandOrInventoryEntities(uid, SlotFlags.WITHOUT_POCKET))
{
if (!TryReflectHitscan(uid, ent, args.Shooter, args.SourceItem, args.Direction, out var dir))
continue;
@@ -70,7 +70,7 @@ public sealed class ReflectSystem : EntitySystem
private void OnReflectUserCollide(EntityUid uid, ReflectUserComponent component, ref ProjectileReflectAttemptEvent args)
{
foreach (var ent in _inventorySystem.GetHandOrInventoryEntities(uid, SlotFlags.All & ~SlotFlags.POCKET))
foreach (var ent in _inventorySystem.GetHandOrInventoryEntities(uid, SlotFlags.WITHOUT_POCKET))
{
if (!TryReflectProjectile(uid, ent, args.ProjUid))
continue;
@@ -222,7 +222,7 @@ public sealed class ReflectSystem : EntitySystem
/// </summary>
private void RefreshReflectUser(EntityUid user)
{
foreach (var ent in _inventorySystem.GetHandOrInventoryEntities(user, SlotFlags.All & ~SlotFlags.POCKET))
foreach (var ent in _inventorySystem.GetHandOrInventoryEntities(user, SlotFlags.WITHOUT_POCKET))
{
if (!HasComp<ReflectComponent>(ent))
continue;

View File

@@ -94,6 +94,9 @@ namespace Content.Shared.Whitelist
return RequireAll ? tagSystem.HasAllTags(tags, Tags) : tagSystem.HasAnyTag(tags, Tags);
}
if (RequireAll)
return true;
return false;
}
}