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:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 { }
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
38
Content.Shared/Ghost/Roles/GhostRolePrototype.cs
Normal file
38
Content.Shared/Ghost/Roles/GhostRolePrototype.cs
Normal 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!;
|
||||
}
|
||||
@@ -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 ());
|
||||
|
||||
@@ -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).
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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!;
|
||||
}
|
||||
}
|
||||
|
||||
51
Content.Shared/MagicMirror/MagicMirrorComponent.cs
Normal file
51
Content.Shared/MagicMirror/MagicMirrorComponent.cs
Normal 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");
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Power.Components;
|
||||
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class ActivatableUIRequiresPowerComponent : Component
|
||||
{
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
73
Content.Shared/UserInterface/ActivatableUIComponent.cs
Normal file
73
Content.Shared/UserInterface/ActivatableUIComponent.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
}
|
||||
79
Content.Shared/UserInterface/ActivatableUISystem.Power.cs
Normal file
79
Content.Shared/UserInterface/ActivatableUISystem.Power.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
224
Content.Shared/UserInterface/ActivatableUISystem.cs
Normal file
224
Content.Shared/UserInterface/ActivatableUISystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user