Merge remote-tracking branch 'refs/remotes/upstream/master' into ed-27-04-2024-displacement-upstream

# Conflicts:
#	Resources/Prototypes/Entities/Mobs/Customization/Markings/human_hair.yml
This commit is contained in:
Ed
2024-04-27 10:45:35 +03:00
471 changed files with 9993 additions and 9391 deletions

View File

@@ -26,12 +26,14 @@ namespace Content.Shared.Access.Systems
public readonly HashSet<string> Icons;
public string CurrentName { get; }
public string CurrentJob { get; }
public string CurrentJobIconId { get; }
public AgentIDCardBoundUserInterfaceState(string currentName, string currentJob, HashSet<string> icons)
public AgentIDCardBoundUserInterfaceState(string currentName, string currentJob, string currentJobIconId, HashSet<string> icons)
{
Icons = icons;
CurrentName = currentName;
CurrentJob = currentJob;
CurrentJobIconId = currentJobIconId;
}
}
@@ -60,11 +62,11 @@ namespace Content.Shared.Access.Systems
[Serializable, NetSerializable]
public sealed class AgentIDCardJobIconChangedMessage : BoundUserInterfaceMessage
{
public string JobIcon { get; }
public string JobIconId { get; }
public AgentIDCardJobIconChangedMessage(string jobIcon)
public AgentIDCardJobIconChangedMessage(string jobIconId)
{
JobIcon = jobIcon;
JobIconId = jobIconId;
}
}
}

View File

@@ -1,13 +1,32 @@
using Content.Shared.Access.Components;
using Content.Shared.Administration.Logs;
using Content.Shared.Database;
using Content.Shared.Hands.Components;
using Content.Shared.Inventory;
using Content.Shared.PDA;
using Content.Shared.Roles;
using Content.Shared.StatusIcon;
using Robust.Shared.Prototypes;
namespace Content.Shared.Access.Systems;
public abstract class SharedIdCardSystem : EntitySystem
{
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly InventorySystem _inventorySystem = default!;
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<IdCardComponent, MapInitEvent>(OnMapInit);
}
private void OnMapInit(EntityUid uid, IdCardComponent id, MapInitEvent args)
{
UpdateEntityName(uid, id);
}
/// <summary>
/// Attempt to find an ID card on an entity. This will look in the entity itself, in the entity's hands, and
@@ -56,4 +75,143 @@ public abstract class SharedIdCardSystem : EntitySystem
idCard = default;
return false;
}
/// <summary>
/// Attempts to change the job title of a card.
/// Returns true/false.
/// </summary>
/// <remarks>
/// If provided with a player's EntityUid to the player parameter, adds the change to the admin logs.
/// </remarks>
public bool TryChangeJobTitle(EntityUid uid, string? jobTitle, IdCardComponent? id = null, EntityUid? player = null)
{
if (!Resolve(uid, ref id))
return false;
if (!string.IsNullOrWhiteSpace(jobTitle))
{
jobTitle = jobTitle.Trim();
if (jobTitle.Length > IdCardConsoleComponent.MaxJobTitleLength)
jobTitle = jobTitle[..IdCardConsoleComponent.MaxJobTitleLength];
}
else
{
jobTitle = null;
}
if (id.JobTitle == jobTitle)
return true;
id.JobTitle = jobTitle;
Dirty(uid, id);
UpdateEntityName(uid, id);
if (player != null)
{
_adminLogger.Add(LogType.Identity, LogImpact.Low,
$"{ToPrettyString(player.Value):player} has changed the job title of {ToPrettyString(uid):entity} to {jobTitle} ");
}
return true;
}
public bool TryChangeJobIcon(EntityUid uid, StatusIconPrototype jobIcon, IdCardComponent? id = null, EntityUid? player = null)
{
if (!Resolve(uid, ref id))
{
return false;
}
if (id.JobIcon == jobIcon.ID)
{
return true;
}
id.JobIcon = jobIcon.ID;
Dirty(uid, id);
if (player != null)
{
_adminLogger.Add(LogType.Identity, LogImpact.Low,
$"{ToPrettyString(player.Value):player} has changed the job icon of {ToPrettyString(uid):entity} to {jobIcon} ");
}
return true;
}
public bool TryChangeJobDepartment(EntityUid uid, JobPrototype job, IdCardComponent? id = null)
{
if (!Resolve(uid, ref id))
return false;
id.JobDepartments.Clear();
foreach (var department in _prototypeManager.EnumeratePrototypes<DepartmentPrototype>())
{
if (department.Roles.Contains(job.ID))
id.JobDepartments.Add("department-" + department.ID);
}
Dirty(uid, id);
return true;
}
/// <summary>
/// Attempts to change the full name of a card.
/// Returns true/false.
/// </summary>
/// <remarks>
/// If provided with a player's EntityUid to the player parameter, adds the change to the admin logs.
/// </remarks>
public bool TryChangeFullName(EntityUid uid, string? fullName, IdCardComponent? id = null, EntityUid? player = null)
{
if (!Resolve(uid, ref id))
return false;
if (!string.IsNullOrWhiteSpace(fullName))
{
fullName = fullName.Trim();
if (fullName.Length > IdCardConsoleComponent.MaxFullNameLength)
fullName = fullName[..IdCardConsoleComponent.MaxFullNameLength];
}
else
{
fullName = null;
}
if (id.FullName == fullName)
return true;
id.FullName = fullName;
Dirty(uid, id);
UpdateEntityName(uid, id);
if (player != null)
{
_adminLogger.Add(LogType.Identity, LogImpact.Low,
$"{ToPrettyString(player.Value):player} has changed the name of {ToPrettyString(uid):entity} to {fullName} ");
}
return true;
}
/// <summary>
/// Changes the name of the id's owner.
/// </summary>
/// <remarks>
/// If either <see cref="FullName"/> or <see cref="JobTitle"/> is empty, it's replaced by placeholders.
/// If both are empty, the original entity's name is restored.
/// </remarks>
private void UpdateEntityName(EntityUid uid, IdCardComponent? id = null)
{
if (!Resolve(uid, ref id))
return;
var jobSuffix = string.IsNullOrWhiteSpace(id.JobTitle) ? string.Empty : $" ({id.JobTitle})";
var val = string.IsNullOrWhiteSpace(id.FullName)
? Loc.GetString("access-id-card-component-owner-name-job-title-text",
("jobSuffix", jobSuffix))
: Loc.GetString("access-id-card-component-owner-full-name-job-title-text",
("fullName", id.FullName),
("jobSuffix", jobSuffix));
_metaSystem.SetEntityName(uid, val);
}
}

View File

@@ -1,6 +1,9 @@
namespace Content.Shared.Administration.Components;
using Robust.Shared.GameStates;
namespace Content.Shared.Administration.Components;
/// <summary>
/// Flips the target's sprite on it's head, so they do a headstand.
/// </summary>
[NetworkedComponent]
public abstract partial class SharedHeadstandComponent : Component { }

View File

@@ -1,5 +1,8 @@
namespace Content.Shared.Administration.Components;
using Robust.Shared.GameStates;
namespace Content.Shared.Administration.Components;
[NetworkedComponent]
public abstract partial class SharedKillSignComponent : Component
{

View File

@@ -177,7 +177,7 @@ namespace Content.Shared.CCVar
/// Controls the maximum number of character slots a player is allowed to have.
/// </summary>
public static readonly CVarDef<int>
GameMaxCharacterSlots = CVarDef.Create("game.maxcharacterslots", 10, CVar.ARCHIVE | CVar.SERVERONLY);
GameMaxCharacterSlots = CVarDef.Create("game.maxcharacterslots", 30, CVar.ARCHIVE | CVar.SERVERONLY);
/// <summary>
/// Controls the game map prototype to load. SS14 stores these prototypes in Prototypes/Maps.

View File

@@ -42,7 +42,7 @@ public sealed class SolutionTransferSystem : EntitySystem
var newTransferAmount = FixedPoint2.Clamp(message.Value, ent.Comp.MinimumTransferAmount, ent.Comp.MaximumTransferAmount);
ent.Comp.TransferAmount = newTransferAmount;
if (message.Session.AttachedEntity is { Valid: true } user)
if (message.Actor is { Valid: true } user)
_popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", newTransferAmount)), ent, user);
}
@@ -53,10 +53,9 @@ public sealed class SolutionTransferSystem : EntitySystem
if (!args.CanAccess || !args.CanInteract || !comp.CanChangeTransferAmount || args.Hands == null)
return;
if (!TryComp<ActorComponent>(args.User, out var actor))
return;
// Custom transfer verb
var @event = args;
args.Verbs.Add(new AlternativeVerb()
{
Text = Loc.GetString("comp-solution-transfer-verb-custom-amount"),
@@ -64,8 +63,7 @@ public sealed class SolutionTransferSystem : EntitySystem
// TODO: remove server check when bui prediction is a thing
Act = () =>
{
if (_net.IsServer)
_ui.TryOpen(uid, TransferAmountUiKey.Key, actor.PlayerSession);
_ui.OpenUi(uid, TransferAmountUiKey.Key, @event.User);
},
Priority = 1
});

View File

@@ -626,9 +626,9 @@ namespace Content.Shared.Containers.ItemSlots
return;
if (args.TryEject && slot.HasItem)
TryEjectToHands(uid, slot, args.Session.AttachedEntity, false);
else if (args.TryInsert && !slot.HasItem && args.Session.AttachedEntity is EntityUid user)
TryInsertFromHand(uid, slot, user);
TryEjectToHands(uid, slot, args.Actor, true);
else if (args.TryInsert && !slot.HasItem)
TryInsertFromHand(uid, slot, args.Actor);
}
#endregion

View File

@@ -23,6 +23,9 @@
<Private>false</Private>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Folder Include="Power\Components\" />
</ItemGroup>
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />
<Import Project="..\RobustToolbox\MSBuild\Robust.CompNetworkGenerator.targets" />
</Project>

View File

@@ -1,10 +1,23 @@
using Content.Shared.Actions;
using Content.Shared.DeviceNetwork.Components;
using Content.Shared.UserInterface;
using Robust.Shared.Serialization;
namespace Content.Shared.DeviceNetwork.Systems;
public abstract class SharedNetworkConfiguratorSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NetworkConfiguratorComponent, ActivatableUIOpenAttemptEvent>(OnUiOpenAttempt);
}
private void OnUiOpenAttempt(EntityUid uid, NetworkConfiguratorComponent configurator, ActivatableUIOpenAttemptEvent args)
{
if (configurator.LinkModeActive)
args.Cancel();
}
}
public sealed partial class ClearAllOverlaysEvent : InstantActionEvent

View File

@@ -1,8 +1,10 @@
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Extinguisher;
[NetworkedComponent]
public abstract partial class SharedFireExtinguisherComponent : Component
{
[DataField("refillSound")] public SoundSpecifier RefillSound = new SoundPathSpecifier("/Audio/Effects/refill.ogg");

View File

@@ -0,0 +1,38 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.Ghost.Roles;
/// <summary>
/// For selectable ghostrole prototypes in ghostrole spawners.
/// </summary>
[Prototype]
public sealed partial class GhostRolePrototype : IPrototype
{
[ViewVariables]
[IdDataField]
public string ID { get; private set; } = default!;
/// <summary>
/// The name of the ghostrole.
/// </summary>
[DataField]
public string Name { get; set; } = default!;
/// <summary>
/// The description of the ghostrole.
/// </summary>
[DataField]
public string Description { get; set; } = default!;
/// <summary>
/// The entity prototype of the ghostrole
/// </summary>
[DataField]
public string EntityPrototype = default!;
/// <summary>
/// Rules of the ghostrole
/// </summary>
[DataField]
public string Rules = default!;
}

View File

@@ -104,7 +104,8 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance
HumanoidSkinColor.HumanToned => Humanoid.SkinColor.HumanSkinTone(speciesPrototype.DefaultHumanSkinTone),
HumanoidSkinColor.Hues => speciesPrototype.DefaultSkinTone,
HumanoidSkinColor.TintedHues => Humanoid.SkinColor.TintedHues(speciesPrototype.DefaultSkinTone),
_ => Humanoid.SkinColor.ValidHumanSkinTone
HumanoidSkinColor.VoxFeathers => Humanoid.SkinColor.ClosestVoxColor(speciesPrototype.DefaultSkinTone),
_ => Humanoid.SkinColor.ValidHumanSkinTone,
};
return new(
@@ -166,6 +167,9 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance
case HumanoidSkinColor.TintedHues:
newSkinColor = Humanoid.SkinColor.ValidTintedHuesSkinTone(newSkinColor);
break;
case HumanoidSkinColor.VoxFeathers:
newSkinColor = Humanoid.SkinColor.ProportionalVoxColor(newSkinColor);
break;
}
return new HumanoidCharacterAppearance(newHairStyle, newHairColor, newFacialHairStyle, newHairColor, newEyeColor, newSkinColor, new ());

View File

@@ -1,3 +1,6 @@
using System.Security.Cryptography;
using Microsoft.VisualBasic.CompilerServices;
namespace Content.Shared.Humanoid;
public static class SkinColor
@@ -7,6 +10,13 @@ public static class SkinColor
public const float MinHuesLightness = 0.175f;
public const float MinFeathersHue = 29f / 360;
public const float MaxFeathersHue = 174f / 360;
public const float MinFeathersSaturation = 20f / 100;
public const float MaxFeathersSaturation = 88f / 100;
public const float MinFeathersValue = 36f / 100;
public const float MaxFeathersValue = 55f / 100;
public static Color ValidHumanSkinTone => Color.FromHsv(new Vector4(0.07f, 0.2f, 1f, 1f));
/// <summary>
@@ -140,11 +150,65 @@ public static class SkinColor
return Color.ToHsl(color).Y <= MaxTintedHuesSaturation && Color.ToHsl(color).Z >= MinTintedHuesLightness;
}
/// <summary>
/// Converts a Color proportionally to the allowed vox color range.
/// Will NOT preserve the specific input color even if it is within the allowed vox color range.
/// </summary>
/// <param name="color">Color to convert</param>
/// <returns>Vox feather coloration</returns>
public static Color ProportionalVoxColor(Color color)
{
var newColor = Color.ToHsv(color);
newColor.X = newColor.X * (MaxFeathersHue - MinFeathersHue) + MinFeathersHue;
newColor.Y = newColor.Y * (MaxFeathersSaturation - MinFeathersSaturation) + MinFeathersSaturation;
newColor.Z = newColor.Z * (MaxFeathersValue - MinFeathersValue) + MinFeathersValue;
return Color.FromHsv(newColor);
}
// /// <summary>
// /// Ensures the input Color is within the allowed vox color range.
// /// </summary>
// /// <param name="color">Color to convert</param>
// /// <returns>The same Color if it was within the allowed range, or the closest matching Color otherwise</returns>
public static Color ClosestVoxColor(Color color)
{
var hsv = Color.ToHsv(color);
hsv.X = Math.Clamp(hsv.X, MinFeathersHue, MaxFeathersHue);
hsv.Y = Math.Clamp(hsv.Y, MinFeathersSaturation, MaxFeathersSaturation);
hsv.Z = Math.Clamp(hsv.Z, MinFeathersValue, MaxFeathersValue);
return Color.FromHsv(hsv);
}
/// <summary>
/// Verify if this color is a valid vox feather coloration, or not.
/// </summary>
/// <param name="color">The color to verify</param>
/// <returns>True if valid, false otherwise</returns>
public static bool VerifyVoxFeathers(Color color)
{
var colorHsv = Color.ToHsv(color);
if (colorHsv.X < MinFeathersHue || colorHsv.X > MaxFeathersHue)
return false;
if (colorHsv.Y < MinFeathersSaturation || colorHsv.Y > MaxFeathersSaturation)
return false;
if (colorHsv.Z < MinFeathersValue || colorHsv.Z > MaxFeathersValue)
return false;
return true;
}
/// <summary>
/// This takes in a color, and returns a color guaranteed to be above MinHuesLightness
/// </summary>
/// <param name="color"></param>
/// <returns>Either the color as-is if it's above MinHuesLightness, or the color with luminosity increased above MinHuesLightness</returns>
/// <returns>Either the color as-is if it's above MinHuesLightness, or the color with luminosity increased above MinHuesLightness</returns>
public static Color MakeHueValid(Color color)
{
var manipulatedColor = Color.ToHsv(color);
@@ -169,6 +233,7 @@ public static class SkinColor
HumanoidSkinColor.HumanToned => VerifyHumanSkinTone(color),
HumanoidSkinColor.TintedHues => VerifyTintedHues(color),
HumanoidSkinColor.Hues => VerifyHues(color),
HumanoidSkinColor.VoxFeathers => VerifyVoxFeathers(color),
_ => false,
};
}
@@ -180,6 +245,7 @@ public static class SkinColor
HumanoidSkinColor.HumanToned => ValidHumanSkinTone,
HumanoidSkinColor.TintedHues => ValidTintedHuesSkinTone(color),
HumanoidSkinColor.Hues => MakeHueValid(color),
HumanoidSkinColor.VoxFeathers => ClosestVoxColor(color),
_ => color
};
}
@@ -189,5 +255,6 @@ public enum HumanoidSkinColor : byte
{
HumanToned,
Hues,
VoxFeathers, // Vox feathers are limited to a specific color range
TintedHues, //This gives a color tint to a humanoid's skin (10% saturation with full hue range).
}

View File

@@ -42,6 +42,7 @@ namespace Content.Shared.Input
public static readonly BoundKeyFunction MovePulledObject = "MovePulledObject";
public static readonly BoundKeyFunction ReleasePulledObject = "ReleasePulledObject";
public static readonly BoundKeyFunction MouseMiddle = "MouseMiddle";
public static readonly BoundKeyFunction ToggleRoundEndSummaryWindow = "ToggleRoundEndSummaryWindow";
public static readonly BoundKeyFunction OpenEntitySpawnWindow = "OpenEntitySpawnWindow";
public static readonly BoundKeyFunction OpenSandboxWindow = "OpenSandboxWindow";
public static readonly BoundKeyFunction OpenTileSpawnWindow = "OpenTileSpawnWindow";

View File

@@ -76,8 +76,11 @@ namespace Content.Shared.Interaction
public override void Initialize()
{
SubscribeLocalEvent<BoundUserInterfaceCheckRangeEvent>(HandleUserInterfaceRangeCheck);
SubscribeLocalEvent<BoundUserInterfaceMessageAttempt>(OnBoundInterfaceInteractAttempt);
SubscribeAllEvent<InteractInventorySlotEvent>(HandleInteractInventorySlotEvent);
SubscribeLocalEvent<UnremoveableComponent, ContainerGettingRemovedAttemptEvent>(OnRemoveAttempt);
SubscribeLocalEvent<UnremoveableComponent, GotUnequippedEvent>(OnUnequip);
SubscribeLocalEvent<UnremoveableComponent, GotUnequippedHandEvent>(OnUnequipHand);
@@ -108,7 +111,9 @@ namespace Content.Shared.Interaction
/// </summary>
private void OnBoundInterfaceInteractAttempt(BoundUserInterfaceMessageAttempt ev)
{
if (ev.Sender.AttachedEntity is not { } user || !_actionBlockerSystem.CanInteract(user, ev.Target))
var user = ev.Actor;
if (!_actionBlockerSystem.CanInteract(user, ev.Target))
{
ev.Cancel();
return;
@@ -973,8 +978,8 @@ namespace Content.Shared.Interaction
return false;
DoContactInteraction(user, used, activateMsg);
if (delayComponent != null)
_useDelay.TryResetDelay((used, delayComponent));
// Still need to call this even without checkUseDelay in case this gets relayed from Activate.
_useDelay.TryResetDelay(used, component: delayComponent);
if (!activateMsg.WasLogged)
_adminLogger.Add(LogType.InteractActivate, LogImpact.Low, $"{ToPrettyString(user):user} activated {ToPrettyString(used):used}");
return true;
@@ -1145,6 +1150,21 @@ namespace Content.Shared.Interaction
RaiseLocalEvent(uidA, new ContactInteractionEvent(uidB.Value));
RaiseLocalEvent(uidB.Value, new ContactInteractionEvent(uidA));
}
private void HandleUserInterfaceRangeCheck(ref BoundUserInterfaceCheckRangeEvent ev)
{
if (ev.Result == BoundUserInterfaceRangeResult.Fail)
return;
if (InRangeUnobstructed(ev.Actor, ev.Target, ev.Data.InteractionRange))
{
ev.Result = BoundUserInterfaceRangeResult.Pass;
}
else
{
ev.Result = BoundUserInterfaceRangeResult.Fail;
}
}
}
/// <summary>

View File

@@ -13,6 +13,16 @@ public sealed partial class InventoryComponent : Component
[DataField("speciesId")] public string? SpeciesId { get; set; }
[DataField] public string JumpsuitShader = "StencilDraw";
[DataField] public Dictionary<string, SlotDisplacementData> Displacements = [];
public SlotDefinition[] Slots = Array.Empty<SlotDefinition>();
public ContainerSlot[] Containers = Array.Empty<ContainerSlot>();
[DataDefinition]
public sealed partial class SlotDisplacementData
{
[DataField(required: true)]
public PrototypeLayerData Layer = default!;
}
}

View File

@@ -0,0 +1,51 @@
using Content.Shared.DoAfter;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
namespace Content.Shared.MagicMirror;
/// <summary>
/// Allows humanoids to change their appearance mid-round.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class MagicMirrorComponent : Component
{
[DataField]
public DoAfterId? DoAfter;
/// <summary>
/// Magic mirror target, used for validating UI messages.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? Target;
/// <summary>
/// doafter time required to add a new slot
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public TimeSpan AddSlotTime = TimeSpan.FromSeconds(5);
/// <summary>
/// doafter time required to remove a existing slot
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public TimeSpan RemoveSlotTime = TimeSpan.FromSeconds(2);
/// <summary>
/// doafter time required to change slot
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public TimeSpan SelectSlotTime = TimeSpan.FromSeconds(3);
/// <summary>
/// doafter time required to recolor slot
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public TimeSpan ChangeSlotTime = TimeSpan.FromSeconds(1);
/// <summary>
/// Sound emitted when slots are changed
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier ChangeHairSound = new SoundPathSpecifier("/Audio/Items/scissors.ogg");
}

View File

@@ -1,10 +1,29 @@
using Content.Shared.DoAfter;
using Content.Shared.Humanoid.Markings;
using Robust.Shared.Player;
using Content.Shared.Interaction;
using Robust.Shared.Serialization;
namespace Content.Shared.MagicMirror;
public abstract class SharedMagicMirrorSystem : EntitySystem
{
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MagicMirrorComponent, BoundUserInterfaceCheckRangeEvent>(OnMirrorRangeCheck);
}
private void OnMirrorRangeCheck(EntityUid uid, MagicMirrorComponent component, ref BoundUserInterfaceCheckRangeEvent args)
{
if (!Exists(component.Target) || !_interaction.InRangeUnobstructed(uid, component.Target.Value))
{
args.Result = BoundUserInterfaceRangeResult.Fail;
}
}
}
[Serializable, NetSerializable]
public enum MagicMirrorUiKey : byte
{

View File

@@ -1,8 +1,10 @@
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Paper;
[NetworkedComponent]
public abstract partial class SharedPaperComponent : Component
{
/// <summary>

View File

@@ -0,0 +1,8 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Power.Components;
[RegisterComponent, NetworkedComponent]
public sealed partial class ActivatableUIRequiresPowerComponent : Component
{
}

View File

@@ -1,15 +1,14 @@
using Content.Shared.Prying.Components;
using Content.Shared.Verbs;
using Content.Shared.DoAfter;
using Robust.Shared.Serialization;
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Administration.Logs;
using Content.Shared.Database;
using Content.Shared.DoAfter;
using Content.Shared.Doors.Components;
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Robust.Shared.Audio;
using Content.Shared.Prying.Components;
using Content.Shared.Verbs;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Serialization;
using PryUnpoweredComponent = Content.Shared.Prying.Components.PryUnpoweredComponent;
namespace Content.Shared.Prying.Systems;
@@ -99,14 +98,16 @@ public sealed class PryingSystem : EntitySystem
// to be marked as handled.
return true;
return StartPry(target, user, null, 0.1f, out id); // hand-prying is much slower
// hand-prying is much slower
var modifier = CompOrNull<PryingComponent>(user)?.SpeedModifier ?? 0.1f;
return StartPry(target, user, null, modifier, out id);
}
private bool CanPry(EntityUid target, EntityUid user, out string? message, PryingComponent? comp = null)
{
BeforePryEvent canev;
if (comp != null)
if (comp != null || Resolve(user, ref comp, false))
{
canev = new BeforePryEvent(user, comp.PryPowered, comp.Force);
}

View File

@@ -1,3 +1,4 @@
using System.Linq;
using Content.Shared.Administration.Logs;
using Content.Shared.Database;
using Content.Shared.Mind;
@@ -62,6 +63,38 @@ public abstract class SharedRoleSystem : EntitySystem
_antagTypes.Add(typeof(T));
}
public void MindAddRoles(EntityUid mindId, ComponentRegistry components, MindComponent? mind = null, bool silent = false)
{
if (!Resolve(mindId, ref mind))
return;
EntityManager.AddComponents(mindId, components);
var antagonist = false;
foreach (var compReg in components.Values)
{
var compType = compReg.Component.GetType();
var comp = EntityManager.ComponentFactory.GetComponent(compType);
if (IsAntagonistRole(comp.GetType()))
{
antagonist = true;
break;
}
}
var mindEv = new MindRoleAddedEvent(silent);
RaiseLocalEvent(mindId, ref mindEv);
var message = new RoleAddedEvent(mindId, mind, antagonist, silent);
if (mind.OwnedEntity != null)
{
RaiseLocalEvent(mind.OwnedEntity.Value, message, true);
}
_adminLogger.Add(LogType.Mind, LogImpact.Low,
$"Role components {string.Join(components.Keys.ToString(), ", ")} added to mind of {_minds.MindOwnerLoggingString(mind)}");
}
public void MindAddRole(EntityUid mindId, Component component, MindComponent? mind = null, bool silent = false)
{
if (!Resolve(mindId, ref mind))

View File

@@ -2,9 +2,12 @@ using System.Collections.Frozen;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared.ActionBlocker;
using Content.Shared.Administration;
using Content.Shared.Administration.Managers;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Destructible;
using Content.Shared.DoAfter;
using Content.Shared.Ghost;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Implants.Components;
@@ -29,6 +32,7 @@ using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.Storage.EntitySystems;
@@ -36,6 +40,7 @@ public abstract class SharedStorageSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] protected readonly IRobustRandom Random = default!;
[Dependency] private readonly ISharedAdminManager _admin = default!;
[Dependency] protected readonly ActionBlockerSystem ActionBlocker = default!;
[Dependency] private readonly EntityLookupSystem _entityLookupSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
@@ -72,6 +77,9 @@ public abstract class SharedStorageSystem : EntitySystem
private readonly List<ItemSizePrototype> _sortedSizes = new();
private FrozenDictionary<string, ItemSizePrototype> _nextSmallest = FrozenDictionary<string, ItemSizePrototype>.Empty;
private const string QuickInsertUseDelayID = "quickInsert";
private const string OpenUiUseDelayID = "storage";
protected readonly List<string> CantFillReasons = [];
/// <inheritdoc />
@@ -84,6 +92,13 @@ public abstract class SharedStorageSystem : EntitySystem
_xformQuery = GetEntityQuery<TransformComponent>();
_prototype.PrototypesReloaded += OnPrototypesReloaded;
Subs.BuiEvents<StorageComponent>(StorageComponent.StorageUiKey.Key, subs =>
{
subs.Event<BoundUIClosedEvent>(OnBoundUIClosed);
});
SubscribeLocalEvent<StorageComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<StorageComponent, GetVerbsEvent<ActivationVerb>>(AddUiVerb);
SubscribeLocalEvent<StorageComponent, ComponentGetState>(OnStorageGetState);
SubscribeLocalEvent<StorageComponent, ComponentHandleState>(OnStorageHandleState);
SubscribeLocalEvent<StorageComponent, ComponentInit>(OnComponentInit, before: new[] { typeof(SharedContainerSystem) });
@@ -118,6 +133,15 @@ public abstract class SharedStorageSystem : EntitySystem
UpdatePrototypeCache();
}
private void OnMapInit(Entity<StorageComponent> entity, ref MapInitEvent args)
{
if (TryComp<UseDelayComponent>(entity, out var useDelayComp))
{
UseDelay.SetLength((entity, useDelayComp), entity.Comp.QuickInsertCooldown, QuickInsertUseDelayID);
UseDelay.SetLength((entity, useDelayComp), entity.Comp.OpenUiCooldown, OpenUiUseDelayID);
}
}
private void OnStorageGetState(EntityUid uid, StorageComponent component, ref ComponentGetState args)
{
var storedItems = new Dictionary<NetEntity, ItemStorageLocation>();
@@ -130,7 +154,6 @@ public abstract class SharedStorageSystem : EntitySystem
args.State = new StorageComponentState()
{
Grid = new List<Box2i>(component.Grid),
IsUiOpen = component.IsUiOpen,
MaxItemSize = component.MaxItemSize,
StoredItems = storedItems,
SavedLocations = component.SavedLocations
@@ -144,7 +167,6 @@ public abstract class SharedStorageSystem : EntitySystem
component.Grid.Clear();
component.Grid.AddRange(state.Grid);
component.IsUiOpen = state.IsUiOpen;
component.MaxItemSize = state.MaxItemSize;
component.StoredItems.Clear();
@@ -196,9 +218,108 @@ public abstract class SharedStorageSystem : EntitySystem
UpdateAppearance((uid, storageComp, null));
}
public virtual void UpdateUI(Entity<StorageComponent?> entity) {}
/// <summary>
/// If the user has nested-UIs open (e.g., PDA UI open when pda is in a backpack), close them.
/// </summary>
private void CloseNestedInterfaces(EntityUid uid, EntityUid actor, StorageComponent? storageComp = null)
{
if (!Resolve(uid, ref storageComp))
return;
public virtual void OpenStorageUI(EntityUid uid, EntityUid entity, StorageComponent? storageComp = null, bool silent = false) { }
// for each containing thing
// if it has a storage comp
// ensure unsubscribe from session
// if it has a ui component
// close ui
foreach (var entity in storageComp.Container.ContainedEntities)
{
_ui.CloseUis(entity, actor);
}
}
private void OnBoundUIClosed(EntityUid uid, StorageComponent storageComp, BoundUIClosedEvent args)
{
CloseNestedInterfaces(uid, args.Actor, storageComp);
// If UI is closed for everyone
if (!_ui.IsUiOpen(uid, args.UiKey))
{
UpdateAppearance((uid, storageComp, null));
Audio.PlayPredicted(storageComp.StorageCloseSound, uid, args.Actor);
}
}
private void AddUiVerb(EntityUid uid, StorageComponent component, GetVerbsEvent<ActivationVerb> args)
{
var silent = false;
if (!args.CanAccess || !args.CanInteract || TryComp<LockComponent>(uid, out var lockComponent) && lockComponent.Locked)
{
// we allow admins to open the storage anyways
if (!_admin.HasAdminFlag(args.User, AdminFlags.Admin))
return;
silent = true;
}
silent |= HasComp<GhostComponent>(args.User);
// Does this player currently have the storage UI open?
var uiOpen = _ui.IsUiOpen(uid, StorageComponent.StorageUiKey.Key, args.User);
ActivationVerb verb = new()
{
Act = () =>
{
if (uiOpen)
{
_ui.CloseUi(uid, StorageComponent.StorageUiKey.Key, args.User);
}
else
{
OpenStorageUI(uid, args.User, component, silent);
}
}
};
if (uiOpen)
{
verb.Text = Loc.GetString("comp-storage-verb-close-storage");
verb.Icon = new SpriteSpecifier.Texture(
new("/Textures/Interface/VerbIcons/close.svg.192dpi.png"));
}
else
{
verb.Text = Loc.GetString("comp-storage-verb-open-storage");
verb.Icon = new SpriteSpecifier.Texture(
new("/Textures/Interface/VerbIcons/open.svg.192dpi.png"));
}
args.Verbs.Add(verb);
}
/// <summary>
/// Opens the storage UI for an entity
/// </summary>
/// <param name="entity">The entity to open the UI for</param>
public void OpenStorageUI(EntityUid uid, EntityUid entity, StorageComponent? storageComp = null, bool silent = false)
{
if (!Resolve(uid, ref storageComp, false))
return;
// prevent spamming bag open / honkerton honk sound
silent |= TryComp<UseDelayComponent>(uid, out var useDelay) && UseDelay.IsDelayed((uid, useDelay));
if (!silent)
{
if (!_ui.IsUiOpen(uid, StorageComponent.StorageUiKey.Key))
Audio.PlayPredicted(storageComp.StorageOpenSound, uid, entity);
if (useDelay != null)
UseDelay.TryResetDelay((uid, useDelay));
}
_ui.OpenUi(uid, StorageComponent.StorageUiKey.Key, entity);
}
public virtual void UpdateUI(Entity<StorageComponent?> entity) {}
private void AddTransferVerbs(EntityUid uid, StorageComponent component, GetVerbsEvent<UtilityVerb> args)
{
@@ -252,7 +373,16 @@ public abstract class SharedStorageSystem : EntitySystem
if (args.Handled || TryComp<LockComponent>(uid, out var lockComponent) && lockComponent.Locked)
return;
OpenStorageUI(uid, args.User, storageComp);
// Toggle
if (_ui.IsUiOpen(uid, StorageComponent.StorageUiKey.Key, args.User))
{
_ui.CloseUi(uid, StorageComponent.StorageUiKey.Key, args.User);
}
else
{
OpenStorageUI(uid, args.User, storageComp);
}
args.Handled = true;
}
@@ -275,7 +405,7 @@ public abstract class SharedStorageSystem : EntitySystem
/// <returns></returns>
private void AfterInteract(EntityUid uid, StorageComponent storageComp, AfterInteractEvent args)
{
if (args.Handled || !args.CanReach || !UseDelay.TryResetDelay(uid, checkDelayed: true))
if (args.Handled || !args.CanReach || !UseDelay.TryResetDelay(uid, checkDelayed: true, id: QuickInsertUseDelayID))
return;
// Pick up all entities in a radius around the clicked location.
@@ -451,8 +581,7 @@ public abstract class SharedStorageSystem : EntitySystem
if (!TryComp<StorageComponent>(uid, out var storageComp))
return;
if (!_ui.TryGetUi(uid, StorageComponent.StorageUiKey.Key, out var bui) ||
!bui.SubscribedSessions.Contains(args.SenderSession))
if (!_ui.IsUiOpen(uid, StorageComponent.StorageUiKey.Key, player))
return;
if (!Exists(entity))
@@ -494,8 +623,7 @@ public abstract class SharedStorageSystem : EntitySystem
if (!TryComp<StorageComponent>(storageEnt, out var storageComp))
return;
if (!_ui.TryGetUi(storageEnt, StorageComponent.StorageUiKey.Key, out var bui) ||
!bui.SubscribedSessions.Contains(args.SenderSession))
if (!_ui.IsUiOpen(storageEnt, StorageComponent.StorageUiKey.Key, player))
return;
if (!Exists(itemEnt))
@@ -521,8 +649,7 @@ public abstract class SharedStorageSystem : EntitySystem
if (!TryComp<StorageComponent>(storageEnt, out var storageComp))
return;
if (!_ui.TryGetUi(storageEnt, StorageComponent.StorageUiKey.Key, out var bui) ||
!bui.SubscribedSessions.Contains(args.SenderSession))
if (!_ui.IsUiOpen(storageEnt, StorageComponent.StorageUiKey.Key, player))
return;
if (!Exists(itemEnt))
@@ -549,8 +676,7 @@ public abstract class SharedStorageSystem : EntitySystem
if (!TryComp<StorageComponent>(storageEnt, out var storageComp))
return;
if (!_ui.TryGetUi(storageEnt, StorageComponent.StorageUiKey.Key, out var bui) ||
!bui.SubscribedSessions.Contains(args.SenderSession))
if (!_ui.IsUiOpen(storageEnt, StorageComponent.StorageUiKey.Key, player))
return;
if (!Exists(itemEnt))
@@ -575,11 +701,10 @@ public abstract class SharedStorageSystem : EntitySystem
var storage = GetEntity(msg.Storage);
var item = GetEntity(msg.Item);
if (!TryComp<StorageComponent>(storage, out var storageComp))
if (!HasComp<StorageComponent>(storage))
return;
if (!_ui.TryGetUi(storage, StorageComponent.StorageUiKey.Key, out var bui) ||
!bui.SubscribedSessions.Contains(args.SenderSession))
if (!_ui.IsUiOpen(storage, StorageComponent.StorageUiKey.Key, player))
return;
if (!Exists(item))
@@ -596,11 +721,7 @@ public abstract class SharedStorageSystem : EntitySystem
private void OnBoundUIOpen(EntityUid uid, StorageComponent storageComp, BoundUIOpenedEvent args)
{
if (!storageComp.IsUiOpen)
{
storageComp.IsUiOpen = true;
UpdateAppearance((uid, storageComp, null));
}
UpdateAppearance((uid, storageComp, null));
}
private void OnEntInserted(Entity<StorageComponent> entity, ref EntInsertedIntoContainerMessage args)
@@ -678,11 +799,13 @@ public abstract class SharedStorageSystem : EntitySystem
var capacity = storage.Grid.GetArea();
var used = GetCumulativeItemAreas((uid, storage));
var isOpen = _ui.IsUiOpen(entity.Owner, StorageComponent.StorageUiKey.Key);
_appearance.SetData(uid, StorageVisuals.StorageUsed, used, appearance);
_appearance.SetData(uid, StorageVisuals.Capacity, capacity, appearance);
_appearance.SetData(uid, StorageVisuals.Open, storage.IsUiOpen, appearance);
_appearance.SetData(uid, SharedBagOpenVisuals.BagState, storage.IsUiOpen ? SharedBagState.Open : SharedBagState.Closed, appearance);
_appearance.SetData(uid, StackVisuals.Hide, !storage.IsUiOpen, appearance);
_appearance.SetData(uid, StorageVisuals.Open, isOpen, appearance);
_appearance.SetData(uid, SharedBagOpenVisuals.BagState, isOpen ? SharedBagState.Open : SharedBagState.Closed, appearance);
_appearance.SetData(uid, StackVisuals.Hide, !isOpen, appearance);
}
/// <summary>
@@ -1334,8 +1457,6 @@ public abstract class SharedStorageSystem : EntitySystem
[Serializable, NetSerializable]
protected sealed class StorageComponentState : ComponentState
{
public bool IsUiOpen;
public Dictionary<NetEntity, ItemStorageLocation> StoredItems = new();
public Dictionary<string, List<ItemStorageLocation>> SavedLocations = new();

View File

@@ -19,10 +19,6 @@ namespace Content.Shared.Storage
{
public static string ContainerId = "storagebase";
// TODO: This fucking sucks
[ViewVariables(VVAccess.ReadWrite), DataField]
public bool IsUiOpen;
[ViewVariables]
public Container Container = default!;
@@ -57,6 +53,19 @@ namespace Content.Shared.Storage
[DataField]
public bool QuickInsert; // Can insert storables by clicking them with the storage entity
/// <summary>
/// Minimum delay between quick/area insert actions.
/// </summary>
/// <remarks>Used to prevent autoclickers spamming server with individual pickup actions.</remarks>
public TimeSpan QuickInsertCooldown = TimeSpan.FromSeconds(0.5);
/// <summary>
/// Minimum delay between UI open actions.
/// <remarks>Used to spamming opening sounds.</remarks>
/// </summary>
[DataField]
public TimeSpan OpenUiCooldown = TimeSpan.Zero;
[DataField]
public bool ClickInsert = true; // Can insert stuff by clicking the storage entity with it
@@ -219,15 +228,6 @@ namespace Content.Shared.Storage
}
}
/// <summary>
/// An extra BUI message that either opens, closes, or focuses the storage window based on context.
/// </summary>
[Serializable, NetSerializable]
public sealed class StorageModifyWindowMessage : BoundUserInterfaceMessage
{
}
[NetSerializable]
[Serializable]
public enum StorageVisuals : byte

View File

@@ -1,38 +1,53 @@
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Serialization;
namespace Content.Shared.Timing;
/// <summary>
/// Timer that creates a cooldown each time an object is activated/used
/// Timer that creates a cooldown each time an object is activated/used.
/// Can support additional, separate cooldown timers on the object by passing a unique ID with the system methods.
/// </summary>
/// <remarks>
/// Currently it only supports a single delay per entity, this means that for things that have two delay interactions they will share one timer, so this can cause issues. For example, the bible has a delay when opening the storage UI and when applying it's interaction effect, and they share the same delay.
/// </remarks>
[RegisterComponent]
[NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
[NetworkedComponent]
[Access(typeof(UseDelaySystem))]
public sealed partial class UseDelayComponent : Component
{
/// <summary>
/// When the delay starts.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField]
[AutoPausedField]
public TimeSpan DelayStartTime;
/// <summary>
/// When the delay ends.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField]
[AutoPausedField]
public TimeSpan DelayEndTime;
/// <summary>
/// Default delay time
/// </summary>
[DataField]
[ViewVariables(VVAccess.ReadWrite)]
[AutoNetworkedField]
public Dictionary<string, UseDelayInfo> Delays = [];
/// <summary>
/// Default delay time.
/// </summary>
/// <remarks>
/// This is only used at MapInit and should not be expected
/// to reflect the length of the default delay after that.
/// Use <see cref="UseDelaySystem.TryGetDelayInfo"/> instead.
/// </remarks>
[DataField]
public TimeSpan Delay = TimeSpan.FromSeconds(1);
}
[Serializable, NetSerializable]
public sealed class UseDelayComponentState : IComponentState
{
public Dictionary<string, UseDelayInfo> Delays = new();
}
[Serializable, NetSerializable]
[DataDefinition]
public sealed partial class UseDelayInfo
{
[DataField]
public TimeSpan Length { get; set; }
[DataField]
public TimeSpan StartTime { get; set; }
[DataField]
public TimeSpan EndTime { get; set; }
public UseDelayInfo(TimeSpan length, TimeSpan startTime = default, TimeSpan endTime = default)
{
Length = length;
StartTime = startTime;
EndTime = endTime;
}
}

View File

@@ -1,3 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.GameStates;
using Robust.Shared.Timing;
namespace Content.Shared.Timing;
@@ -7,53 +9,166 @@ public sealed class UseDelaySystem : EntitySystem
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly MetaDataSystem _metadata = default!;
public void SetDelay(Entity<UseDelayComponent> ent, TimeSpan delay)
private const string DefaultId = "default";
public override void Initialize()
{
if (ent.Comp.Delay == delay)
base.Initialize();
SubscribeLocalEvent<UseDelayComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<UseDelayComponent, EntityUnpausedEvent>(OnUnpaused);
SubscribeLocalEvent<UseDelayComponent, ComponentGetState>(OnDelayGetState);
SubscribeLocalEvent<UseDelayComponent, ComponentHandleState>(OnDelayHandleState);
}
private void OnDelayHandleState(Entity<UseDelayComponent> ent, ref ComponentHandleState args)
{
if (args.Current is not UseDelayComponentState state)
return;
ent.Comp.Delay = delay;
Dirty(ent);
ent.Comp.Delays.Clear();
// At time of writing sourcegen networking doesn't deep copy so this will mispredict if you try.
foreach (var (key, delay) in state.Delays)
{
ent.Comp.Delays[key] = new UseDelayInfo(delay.Length, delay.StartTime, delay.EndTime);
}
}
private void OnDelayGetState(Entity<UseDelayComponent> ent, ref ComponentGetState args)
{
args.State = new UseDelayComponentState()
{
Delays = ent.Comp.Delays
};
}
private void OnMapInit(Entity<UseDelayComponent> ent, ref MapInitEvent args)
{
// Set default delay length from the prototype
// This makes it easier for simple use cases that only need a single delay
SetLength(ent, ent.Comp.Delay, DefaultId);
}
private void OnUnpaused(Entity<UseDelayComponent> ent, ref EntityUnpausedEvent args)
{
// We have to do this manually, since it's not just a single field.
foreach (var entry in ent.Comp.Delays.Values)
{
entry.EndTime += args.PausedTime;
}
}
/// <summary>
/// Returns true if the entity has a currently active UseDelay.
/// Sets the length of the delay with the specified ID.
/// </summary>
public bool IsDelayed(Entity<UseDelayComponent> ent)
public bool SetLength(Entity<UseDelayComponent> ent, TimeSpan length, string id = DefaultId)
{
return ent.Comp.DelayEndTime >= _gameTiming.CurTime;
}
if (ent.Comp.Delays.TryGetValue(id, out var entry))
{
if (entry.Length == length)
return true;
/// <summary>
/// Cancels the current delay.
/// </summary>
public void CancelDelay(Entity<UseDelayComponent> ent)
{
ent.Comp.DelayEndTime = _gameTiming.CurTime;
Dirty(ent);
}
entry.Length = length;
}
else
{
ent.Comp.Delays.Add(id, new UseDelayInfo(length));
}
/// <summary>
/// Resets the UseDelay entirely for this entity if possible.
/// </summary>
/// <param name="checkDelayed">Check if the entity has an ongoing delay, return false if it does, return true if it does not.</param>
public bool TryResetDelay(Entity<UseDelayComponent> ent, bool checkDelayed = false)
{
if (checkDelayed && IsDelayed(ent))
return false;
var curTime = _gameTiming.CurTime;
ent.Comp.DelayStartTime = curTime;
ent.Comp.DelayEndTime = curTime - _metadata.GetPauseTime(ent) + ent.Comp.Delay;
Dirty(ent);
return true;
}
public bool TryResetDelay(EntityUid uid, bool checkDelayed = false, UseDelayComponent? component = null)
/// <summary>
/// Returns true if the entity has a currently active UseDelay with the specified ID.
/// </summary>
public bool IsDelayed(Entity<UseDelayComponent> ent, string id = DefaultId)
{
if (!ent.Comp.Delays.TryGetValue(id, out var entry))
return false;
return entry.EndTime >= _gameTiming.CurTime;
}
/// <summary>
/// Cancels the delay with the specified ID.
/// </summary>
public void CancelDelay(Entity<UseDelayComponent> ent, string id = DefaultId)
{
if (!ent.Comp.Delays.TryGetValue(id, out var entry))
return;
entry.EndTime = _gameTiming.CurTime;
Dirty(ent);
}
/// <summary>
/// Tries to get info about the delay with the specified ID. See <see cref="UseDelayInfo"/>.
/// </summary>
/// <param name="ent"></param>
/// <param name="info"></param>
/// <param name="id"></param>
/// <returns></returns>
public bool TryGetDelayInfo(Entity<UseDelayComponent> ent, [NotNullWhen(true)] out UseDelayInfo? info, string id = DefaultId)
{
return ent.Comp.Delays.TryGetValue(id, out info);
}
/// <summary>
/// Returns info for the delay that will end farthest in the future.
/// </summary>
public UseDelayInfo GetLastEndingDelay(Entity<UseDelayComponent> ent)
{
var last = ent.Comp.Delays[DefaultId];
foreach (var entry in ent.Comp.Delays)
{
if (entry.Value.EndTime > last.EndTime)
last = entry.Value;
}
return last;
}
/// <summary>
/// Resets the delay with the specified ID for this entity if possible.
/// </summary>
/// <param name="checkDelayed">Check if the entity has an ongoing delay with the specified ID.
/// If it does, return false and don't reset it.
/// Otherwise reset it and return true.</param>
public bool TryResetDelay(Entity<UseDelayComponent> ent, bool checkDelayed = false, string id = DefaultId)
{
if (checkDelayed && IsDelayed(ent, id))
return false;
if (!ent.Comp.Delays.TryGetValue(id, out var entry))
return false;
var curTime = _gameTiming.CurTime;
entry.StartTime = curTime;
entry.EndTime = curTime - _metadata.GetPauseTime(ent) + entry.Length;
Dirty(ent);
return true;
}
public bool TryResetDelay(EntityUid uid, bool checkDelayed = false, UseDelayComponent? component = null, string id = DefaultId)
{
if (!Resolve(uid, ref component, false))
return false;
return TryResetDelay((uid, component), checkDelayed);
return TryResetDelay((uid, component), checkDelayed, id);
}
/// <summary>
/// Resets all delays on the entity.
/// </summary>
public void ResetAllDelays(Entity<UseDelayComponent> ent)
{
var curTime = _gameTiming.CurTime;
foreach (var entry in ent.Comp.Delays.Values)
{
entry.StartTime = curTime;
entry.EndTime = curTime - _metadata.GetPauseTime(ent) + entry.Length;
}
Dirty(ent);
}
}

View File

@@ -0,0 +1,73 @@
using Content.Shared.Whitelist;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations;
namespace Content.Shared.UserInterface
{
[RegisterComponent, NetworkedComponent]
public sealed partial class ActivatableUIComponent : Component
{
[DataField(required: true, customTypeSerializer: typeof(EnumSerializer))]
public Enum Key { get; set; } = default!;
[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;
[DataField]
public LocId VerbText = "ui-verb-toggle-open";
/// <summary>
/// Whether you need a hand to operate this UI. The hand does not need to be free, you just need to have one.
/// </summary>
/// <remarks>
/// This should probably be true for most machines & computers, but there will still be UIs that represent a
/// more generic interaction / configuration that might not require hands.
/// </remarks>
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public bool RequireHands = true;
/// <summary>
/// Entities that are required to open this UI.
/// </summary>
[DataField("allowedItems")]
[ViewVariables(VVAccess.ReadWrite)]
public EntityWhitelist? AllowedItems = null;
/// <summary>
/// Whether you can activate this ui with activateinhand or not
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public bool RightClickOnly;
/// <summary>
/// Whether spectators (non-admin ghosts) should be allowed to view this UI.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public bool AllowSpectator = true;
/// <summary>
/// Whether the UI should close when the item is deselected due to a hand swap or drop
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public bool CloseOnHandDeselect = 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]
public EntityUid? CurrentSingleUser;
}
}

View File

@@ -24,12 +24,12 @@ public sealed class UserOpenActivatableUIAttemptEvent : CancellableEntityEventAr
public sealed class AfterActivatableUIOpenEvent : EntityEventArgs
{
public EntityUid User { get; }
public readonly ICommonSession Session;
public readonly EntityUid Actor;
public AfterActivatableUIOpenEvent(EntityUid who, ICommonSession session)
public AfterActivatableUIOpenEvent(EntityUid who, EntityUid actor)
{
User = who;
Session = session;
Actor = actor;
}
}

View File

@@ -0,0 +1,13 @@
using Content.Shared.PowerCell;
using Robust.Shared.GameStates;
namespace Content.Shared.UserInterface;
/// <summary>
/// Specifies that the attached entity requires <see cref="PowerCellDrawComponent"/> power.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class ActivatableUIRequiresPowerCellComponent : Component
{
}

View File

@@ -0,0 +1,79 @@
using Content.Shared.PowerCell;
using Robust.Shared.Containers;
namespace Content.Shared.UserInterface;
public sealed partial class ActivatableUISystem
{
[Dependency] private readonly SharedPowerCellSystem _cell = default!;
private void InitializePower()
{
SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, ActivatableUIOpenAttemptEvent>(OnBatteryOpenAttempt);
SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, BoundUIOpenedEvent>(OnBatteryOpened);
SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, BoundUIClosedEvent>(OnBatteryClosed);
SubscribeLocalEvent<PowerCellDrawComponent, EntRemovedFromContainerMessage>(OnPowerCellRemoved);
}
private void OnPowerCellRemoved(EntityUid uid, PowerCellDrawComponent component, EntRemovedFromContainerMessage args)
{
_cell.SetPowerCellDrawEnabled(uid, false);
if (HasComp<ActivatableUIRequiresPowerCellComponent>(uid) &&
TryComp<ActivatableUIComponent>(uid, out var activatable))
{
_uiSystem.CloseUi(uid, activatable.Key);
}
}
private void OnBatteryOpened(EntityUid uid, ActivatableUIRequiresPowerCellComponent component, BoundUIOpenedEvent args)
{
var activatable = Comp<ActivatableUIComponent>(uid);
if (!args.UiKey.Equals(activatable.Key))
return;
_cell.SetPowerCellDrawEnabled(uid, true);
}
private void OnBatteryClosed(EntityUid uid, ActivatableUIRequiresPowerCellComponent component, BoundUIClosedEvent args)
{
var activatable = Comp<ActivatableUIComponent>(uid);
if (!args.UiKey.Equals(activatable.Key))
return;
// Stop drawing power if this was the last person with the UI open.
if (!_uiSystem.IsUiOpen(uid, activatable.Key))
_cell.SetPowerCellDrawEnabled(uid, false);
}
/// <summary>
/// Call if you want to check if the UI should close due to a recent battery usage.
/// </summary>
public void CheckUsage(EntityUid uid, ActivatableUIComponent? active = null, ActivatableUIRequiresPowerCellComponent? component = null, PowerCellDrawComponent? draw = null)
{
if (!Resolve(uid, ref component, ref draw, ref active, false))
return;
if (_cell.HasActivatableCharge(uid))
return;
_uiSystem.CloseUi(uid, active.Key);
}
private void OnBatteryOpenAttempt(EntityUid uid, ActivatableUIRequiresPowerCellComponent component, ActivatableUIOpenAttemptEvent args)
{
if (!TryComp<PowerCellDrawComponent>(uid, out var draw))
return;
// Check if we have the appropriate drawrate / userate to even open it.
if (args.Cancelled ||
!_cell.HasActivatableCharge(uid, draw, user: args.User) ||
!_cell.HasDrawCharge(uid, draw, user: args.User))
{
args.Cancel();
}
}
}

View File

@@ -0,0 +1,224 @@
using Content.Shared.ActionBlocker;
using Content.Shared.Administration.Managers;
using Content.Shared.Ghost;
using Content.Shared.Hands;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Popups;
using Content.Shared.Verbs;
using Robust.Shared.Player;
namespace Content.Shared.UserInterface;
public sealed partial class ActivatableUISystem : EntitySystem
{
[Dependency] private readonly ISharedAdminManager _adminManager = default!;
[Dependency] private readonly ActionBlockerSystem _blockerSystem = default!;
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
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, BoundUIClosedEvent>(OnUIClose);
SubscribeLocalEvent<BoundUserInterfaceMessageAttempt>(OnBoundInterfaceInteractAttempt);
SubscribeLocalEvent<ActivatableUIComponent, GetVerbsEvent<ActivationVerb>>(AddOpenUiVerb);
SubscribeLocalEvent<UserInterfaceComponent, OpenUiActionEvent>(OnActionPerform);
InitializePower();
}
private void OnBoundInterfaceInteractAttempt(BoundUserInterfaceMessageAttempt ev)
{
if (!TryComp(ev.Target, out ActivatableUIComponent? comp))
return;
if (!comp.RequireHands)
return;
if (!TryComp(ev.Actor, out HandsComponent? hands) || hands.Hands.Count == 0)
ev.Cancel();
}
private void OnActionPerform(EntityUid uid, UserInterfaceComponent component, OpenUiActionEvent args)
{
if (args.Handled || args.Key == null)
return;
args.Handled = _uiSystem.TryToggleUi(uid, args.Key, args.Performer);
}
private void AddOpenUiVerb(EntityUid uid, ActivatableUIComponent component, GetVerbsEvent<ActivationVerb> args)
{
if (!args.CanAccess)
return;
if (component.RequireHands && args.Hands == null)
return;
if (component.InHandsOnly && args.Using != uid)
return;
if (!args.CanInteract && (!component.AllowSpectator || !HasComp<GhostComponent>(args.User)))
return;
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);
}
private void OnActivate(EntityUid uid, ActivatableUIComponent component, ActivateInWorldEvent args)
{
if (args.Handled)
return;
if (component.InHandsOnly)
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)
return;
args.Handled = InteractUI(args.User, uid, component);
}
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);
}
private void OnParentChanged(EntityUid uid, ActivatableUIComponent aui, ref EntParentChangedMessage args)
{
CloseAll(uid, aui);
}
private void OnUIClose(EntityUid uid, ActivatableUIComponent component, BoundUIClosedEvent args)
{
var user = args.Actor;
if (user != component.CurrentSingleUser)
return;
if (!Equals(args.UiKey, component.Key))
return;
SetCurrentSingleUser(uid, null, component);
}
private bool InteractUI(EntityUid user, EntityUid uiEntity, ActivatableUIComponent aui)
{
if (!_uiSystem.HasUi(uiEntity, aui.Key))
return false;
if (_uiSystem.IsUiOpen(uiEntity, aui.Key, user))
{
_uiSystem.CloseUi(uiEntity, aui.Key, user);
return true;
}
if (!_blockerSystem.CanInteract(user, uiEntity) && (!aui.AllowSpectator || !HasComp<GhostComponent>(user)))
return false;
if (aui.RequireHands && !HasComp<HandsComponent>(user))
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));
_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;
}
// If we've gotten this far, fire a cancellable event that indicates someone is about to activate this.
// This is so that stuff can require further conditions (like power).
var oae = new ActivatableUIOpenAttemptEvent(user);
var uae = new UserOpenActivatableUIAttemptEvent(user, uiEntity);
RaiseLocalEvent(user, uae);
RaiseLocalEvent(uiEntity, oae);
if (oae.Cancelled || uae.Cancelled)
return false;
// Give the UI an opportunity to prepare itself if it needs to do anything
// before opening
var bae = new BeforeActivatableUIOpenEvent(user);
RaiseLocalEvent(uiEntity, bae);
SetCurrentSingleUser(uiEntity, user, aui);
_uiSystem.OpenUi(uiEntity, aui.Key, user);
//Let the component know a user opened it so it can do whatever it needs to do
var aae = new AfterActivatableUIOpenEvent(user, user);
RaiseLocalEvent(uiEntity, aae);
return true;
}
public void SetCurrentSingleUser(EntityUid uid, EntityUid? user, ActivatableUIComponent? aui = null)
{
if (!Resolve(uid, ref aui))
return;
if (!aui.SingleUser)
return;
aui.CurrentSingleUser = user;
RaiseLocalEvent(uid, new ActivatableUIPlayerChangedEvent());
}
public void CloseAll(EntityUid uid, ActivatableUIComponent? aui = null)
{
if (!Resolve(uid, ref aui, false))
return;
_uiSystem.CloseUi(uid, aui.Key);
}
private void OnHandDeselected(EntityUid uid, ActivatableUIComponent? aui, HandDeselectedEvent args)
{
if (!Resolve(uid, ref aui, false))
return;
if (!aui.CloseOnHandDeselect)
return;
CloseAll(uid, aui);
}
}

View File

@@ -26,6 +26,13 @@ public sealed partial class WieldableComponent : Component
[AutoNetworkedField, DataField("wielded")]
public bool Wielded = false;
/// <summary>
/// Whether using the item inhand while wielding causes the item to unwield.
/// Unwielding can conflict with other inhand actions.
/// </summary>
[DataField]
public bool UnwieldOnUse = true;
[DataField("wieldedInhandPrefix")]
public string? WieldedInhandPrefix = "wielded";

View File

@@ -125,7 +125,7 @@ public sealed class WieldableSystem : EntitySystem
if (!component.Wielded)
args.Handled = TryWield(uid, component, args.User);
else
else if (component.UnwieldOnUse)
args.Handled = TryUnwield(uid, component, args.User);
}