Add access configurator (#18638)
The access configurator programs the access levels of any access reader. To use the access configurator, players must: - Insert an ID card - Click a nearby entity with an access reader with the access configurator in hand - Change the access list Note that players only need one of the access levels listed on the device to lock/unlock it, but will only be able to alter access settings when they all of the access levels listed on the device For example, an airlock which has 'Science' and 'Engineering' access listed can be opened by any player with either 'Science' or 'Engineering' access. However, to change the access settings on this airlock, a player must have both 'Science' and 'Engineering' access. This is to prevent people from easily breaking into secure areas with this tool, by adding one of their own access levels to the target device Obviously, the most useful ID card to use with this tool is one with all access, since it can change the settings of any device. Removing all access requirements from a device will make it useable by anyone. --------- Co-authored-by: Kevin Zheng <kevinz5000@gmail.com>
This commit is contained in:
11
Content.Client/Access/AccessOverriderSystem.cs
Normal file
11
Content.Client/Access/AccessOverriderSystem.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using Content.Shared.Access.Systems;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
|
||||||
|
namespace Content.Client.Access
|
||||||
|
{
|
||||||
|
[UsedImplicitly]
|
||||||
|
public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
using Content.Shared.Access.Components;
|
||||||
|
using Content.Shared.Access.Systems;
|
||||||
|
using Content.Shared.Containers.ItemSlots;
|
||||||
|
using Robust.Client.GameObjects;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using static Content.Shared.Access.Components.AccessOverriderComponent;
|
||||||
|
|
||||||
|
namespace Content.Client.Access.UI
|
||||||
|
{
|
||||||
|
public sealed class AccessOverriderBoundUserInterface : BoundUserInterface
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||||
|
private readonly SharedAccessOverriderSystem _accessOverriderSystem = default!;
|
||||||
|
|
||||||
|
private AccessOverriderWindow? _window;
|
||||||
|
|
||||||
|
public AccessOverriderBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||||
|
{
|
||||||
|
_accessOverriderSystem = EntMan.System<SharedAccessOverriderSystem>();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Open()
|
||||||
|
{
|
||||||
|
base.Open();
|
||||||
|
|
||||||
|
List<string> accessLevels;
|
||||||
|
|
||||||
|
if (EntMan.TryGetComponent<AccessOverriderComponent>(Owner, out var accessOverrider))
|
||||||
|
{
|
||||||
|
accessLevels = accessOverrider.AccessLevels;
|
||||||
|
accessLevels.Sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
accessLevels = new List<string>();
|
||||||
|
_accessOverriderSystem.Log.Error($"No AccessOverrider component found for {EntMan.ToPrettyString(Owner)}!");
|
||||||
|
}
|
||||||
|
|
||||||
|
_window = new AccessOverriderWindow(this, _prototypeManager, accessLevels)
|
||||||
|
{
|
||||||
|
Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName
|
||||||
|
};
|
||||||
|
|
||||||
|
_window.PrivilegedIdButton.OnPressed += _ => SendMessage(new ItemSlotButtonPressedEvent(PrivilegedIdCardSlotId));
|
||||||
|
|
||||||
|
_window.OnClose += Close;
|
||||||
|
_window.OpenCentered();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
if (!disposing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_window?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void UpdateState(BoundUserInterfaceState state)
|
||||||
|
{
|
||||||
|
base.UpdateState(state);
|
||||||
|
var castState = (AccessOverriderBoundUserInterfaceState) state;
|
||||||
|
_window?.UpdateState(castState);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SubmitData(List<string> newAccessList)
|
||||||
|
{
|
||||||
|
SendMessage(new WriteToTargetAccessReaderIdMessage(newAccessList));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
Content.Client/Access/UI/AccessOverriderWindow.xaml
Normal file
23
Content.Client/Access/UI/AccessOverriderWindow.xaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<DefaultWindow xmlns="https://spacestation14.io"
|
||||||
|
MinSize="650 290">
|
||||||
|
<BoxContainer Orientation="Vertical">
|
||||||
|
<GridContainer Columns="2">
|
||||||
|
<GridContainer Columns="3" HorizontalExpand="True">
|
||||||
|
<Label Text="{Loc 'access-overrider-window-privileged-id'}" />
|
||||||
|
<Button Name="PrivilegedIdButton" Access="Public"/>
|
||||||
|
<Label Name="PrivilegedIdLabel" />
|
||||||
|
</GridContainer>
|
||||||
|
</GridContainer>
|
||||||
|
<Label Name="TargetNameLabel" />
|
||||||
|
<Control MinSize="0 8"/>
|
||||||
|
<GridContainer Name="AccessLevelGrid" Columns="5" HorizontalAlignment="Center">
|
||||||
|
|
||||||
|
<!-- Access level buttons are added here by the C# code -->
|
||||||
|
|
||||||
|
</GridContainer>
|
||||||
|
<Control MinSize="0 8"/>
|
||||||
|
<Label Name="MissingPrivilegesLabel" />
|
||||||
|
<Control MinSize="0 4"/>
|
||||||
|
<Label Name="MissingPrivilegesText" />
|
||||||
|
</BoxContainer>
|
||||||
|
</DefaultWindow>
|
||||||
111
Content.Client/Access/UI/AccessOverriderWindow.xaml.cs
Normal file
111
Content.Client/Access/UI/AccessOverriderWindow.xaml.cs
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using Content.Shared.Access;
|
||||||
|
using Content.Shared.Access.Systems;
|
||||||
|
using Robust.Client.AutoGenerated;
|
||||||
|
using Robust.Client.UserInterface.Controls;
|
||||||
|
using Robust.Client.UserInterface.CustomControls;
|
||||||
|
using Robust.Client.UserInterface.XAML;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using static Content.Shared.Access.Components.AccessOverriderComponent;
|
||||||
|
|
||||||
|
namespace Content.Client.Access.UI
|
||||||
|
{
|
||||||
|
[GenerateTypedNameReferences]
|
||||||
|
public sealed partial class AccessOverriderWindow : DefaultWindow
|
||||||
|
{
|
||||||
|
[Dependency] private readonly ILogManager _logManager = default!;
|
||||||
|
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||||
|
|
||||||
|
private readonly ISawmill _logMill = default!;
|
||||||
|
private readonly AccessOverriderBoundUserInterface _owner;
|
||||||
|
private readonly Dictionary<string, Button> _accessButtons = new();
|
||||||
|
|
||||||
|
public AccessOverriderWindow(AccessOverriderBoundUserInterface owner, IPrototypeManager prototypeManager,
|
||||||
|
List<string> accessLevels)
|
||||||
|
{
|
||||||
|
RobustXamlLoader.Load(this);
|
||||||
|
IoCManager.InjectDependencies(this);
|
||||||
|
_logMill = _logManager.GetSawmill(SharedAccessOverriderSystem.Sawmill);
|
||||||
|
|
||||||
|
_owner = owner;
|
||||||
|
|
||||||
|
foreach (var access in accessLevels)
|
||||||
|
{
|
||||||
|
if (!prototypeManager.TryIndex<AccessLevelPrototype>(access, out var accessLevel))
|
||||||
|
{
|
||||||
|
_logMill.Error($"Unable to find accesslevel for {access}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newButton = new Button
|
||||||
|
{
|
||||||
|
Text = GetAccessLevelName(accessLevel),
|
||||||
|
ToggleMode = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
AccessLevelGrid.AddChild(newButton);
|
||||||
|
_accessButtons.Add(accessLevel.ID, newButton);
|
||||||
|
newButton.OnPressed += _ => SubmitData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetAccessLevelName(AccessLevelPrototype prototype)
|
||||||
|
{
|
||||||
|
if (prototype.Name is { } name)
|
||||||
|
return Loc.GetString(name);
|
||||||
|
|
||||||
|
return prototype.ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateState(AccessOverriderBoundUserInterfaceState state)
|
||||||
|
{
|
||||||
|
PrivilegedIdLabel.Text = state.PrivilegedIdName;
|
||||||
|
PrivilegedIdButton.Text = state.IsPrivilegedIdPresent
|
||||||
|
? Loc.GetString("access-overrider-window-eject-button")
|
||||||
|
: Loc.GetString("access-overrider-window-insert-button");
|
||||||
|
|
||||||
|
TargetNameLabel.Text = state.TargetLabel;
|
||||||
|
TargetNameLabel.FontColorOverride = state.TargetLabelColor;
|
||||||
|
|
||||||
|
MissingPrivilegesLabel.Text = "";
|
||||||
|
MissingPrivilegesLabel.FontColorOverride = Color.Yellow;
|
||||||
|
|
||||||
|
MissingPrivilegesText.Text = "";
|
||||||
|
MissingPrivilegesText.FontColorOverride = Color.Yellow;
|
||||||
|
|
||||||
|
if (state.MissingPrivilegesList != null && state.MissingPrivilegesList.Any())
|
||||||
|
{
|
||||||
|
List<string> missingPrivileges = new List<string>();
|
||||||
|
|
||||||
|
foreach (string tag in state.MissingPrivilegesList)
|
||||||
|
{
|
||||||
|
string privilege = Loc.GetString(_prototypeManager.Index<AccessLevelPrototype>(tag)?.Name ?? "generic-unknown");
|
||||||
|
missingPrivileges.Add(privilege);
|
||||||
|
}
|
||||||
|
|
||||||
|
MissingPrivilegesLabel.Text = Loc.GetString("access-overrider-window-missing-privileges");
|
||||||
|
MissingPrivilegesText.Text = string.Join(", ", missingPrivileges);
|
||||||
|
}
|
||||||
|
|
||||||
|
var interfaceEnabled = state.IsPrivilegedIdPresent && state.IsPrivilegedIdAuthorized;
|
||||||
|
|
||||||
|
foreach (var (accessName, button) in _accessButtons)
|
||||||
|
{
|
||||||
|
button.Disabled = !interfaceEnabled;
|
||||||
|
if (interfaceEnabled)
|
||||||
|
{
|
||||||
|
button.Pressed = state.TargetAccessReaderIdAccessList?.Contains(accessName) ?? false;
|
||||||
|
button.Disabled = (!state.AllowedModifyAccessList?.Contains(accessName)) ?? true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SubmitData()
|
||||||
|
{
|
||||||
|
_owner.SubmitData(
|
||||||
|
|
||||||
|
// Iterate over the buttons dictionary, filter by `Pressed`, only get key from the key/value pair
|
||||||
|
_accessButtons.Where(x => x.Value.Pressed).Select(x => x.Key).ToList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
265
Content.Server/Access/Systems/AccessOverriderSystem.cs
Normal file
265
Content.Server/Access/Systems/AccessOverriderSystem.cs
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
using Content.Shared.Access.Components;
|
||||||
|
using Content.Shared.Access.Systems;
|
||||||
|
using Content.Shared.Administration.Logs;
|
||||||
|
using Content.Shared.Database;
|
||||||
|
using Content.Shared.Interaction;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Robust.Server.GameObjects;
|
||||||
|
using Robust.Shared.Containers;
|
||||||
|
using System.Linq;
|
||||||
|
using static Content.Shared.Access.Components.AccessOverriderComponent;
|
||||||
|
using Content.Server.Popups;
|
||||||
|
using Content.Shared.DoAfter;
|
||||||
|
|
||||||
|
namespace Content.Server.Access.Systems;
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
|
||||||
|
{
|
||||||
|
[Dependency] private readonly UserInterfaceSystem _userInterface = default!;
|
||||||
|
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
|
||||||
|
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||||
|
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
|
||||||
|
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||||
|
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
|
||||||
|
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
|
||||||
|
SubscribeLocalEvent<AccessOverriderComponent, WriteToTargetAccessReaderIdMessage>(OnWriteToTargetAccessReaderIdMessage);
|
||||||
|
SubscribeLocalEvent<AccessOverriderComponent, ComponentStartup>(UpdateUserInterface);
|
||||||
|
SubscribeLocalEvent<AccessOverriderComponent, EntInsertedIntoContainerMessage>(UpdateUserInterface);
|
||||||
|
SubscribeLocalEvent<AccessOverriderComponent, EntRemovedFromContainerMessage>(UpdateUserInterface);
|
||||||
|
SubscribeLocalEvent<AccessOverriderComponent, AfterInteractEvent>(AfterInteractOn);
|
||||||
|
SubscribeLocalEvent<AccessOverriderComponent, AccessOverriderDoAfterEvent>(OnDoAfter);
|
||||||
|
SubscribeLocalEvent<AccessOverriderComponent, BoundUIOpenedEvent>(UpdateUserInterface);
|
||||||
|
SubscribeLocalEvent<AccessOverriderComponent, BoundUIClosedEvent>(OnClose);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AfterInteractOn(EntityUid uid, AccessOverriderComponent component, AfterInteractEvent args)
|
||||||
|
{
|
||||||
|
if (args.Target == null || !TryComp(args.Target, out AccessReaderComponent? accessReader))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!_interactionSystem.InRangeUnobstructed(args.User, (EntityUid) args.Target))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var doAfterEventArgs = new DoAfterArgs(args.User, component.DoAfterTime, new AccessOverriderDoAfterEvent(), uid, target: args.Target, used: uid)
|
||||||
|
{
|
||||||
|
BreakOnTargetMove = true,
|
||||||
|
BreakOnUserMove = true,
|
||||||
|
BreakOnDamage = true,
|
||||||
|
NeedHand = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
_doAfterSystem.TryStartDoAfter(doAfterEventArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDoAfter(EntityUid uid, AccessOverriderComponent component, AccessOverriderDoAfterEvent args)
|
||||||
|
{
|
||||||
|
if (!TryComp(args.User, out ActorComponent? actor))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (args.Handled || args.Cancelled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (args.Args.Target != null)
|
||||||
|
{
|
||||||
|
component.TargetAccessReaderId = args.Args.Target.Value;
|
||||||
|
_userInterface.TryOpen(uid, AccessOverriderUiKey.Key, actor.PlayerSession);
|
||||||
|
UpdateUserInterface(uid, component, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
args.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnClose(EntityUid uid, AccessOverriderComponent component, BoundUIClosedEvent args)
|
||||||
|
{
|
||||||
|
if (args.UiKey.Equals(AccessOverriderUiKey.Key))
|
||||||
|
{
|
||||||
|
component.TargetAccessReaderId = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnWriteToTargetAccessReaderIdMessage(EntityUid uid, AccessOverriderComponent component, WriteToTargetAccessReaderIdMessage args)
|
||||||
|
{
|
||||||
|
if (args.Session.AttachedEntity is not { Valid: true } player)
|
||||||
|
return;
|
||||||
|
|
||||||
|
TryWriteToTargetAccessReaderId(uid, args.AccessList, player, component);
|
||||||
|
|
||||||
|
UpdateUserInterface(uid, component, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateUserInterface(EntityUid uid, AccessOverriderComponent component, EntityEventArgs args)
|
||||||
|
{
|
||||||
|
if (!component.Initialized)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var privilegedIdName = string.Empty;
|
||||||
|
var targetLabel = Loc.GetString("access-overrider-window-no-target");
|
||||||
|
var targetLabelColor = Color.Red;
|
||||||
|
|
||||||
|
string[]? possibleAccess = null;
|
||||||
|
string[]? currentAccess = null;
|
||||||
|
string[]? missingAccess = null;
|
||||||
|
|
||||||
|
if (component.TargetAccessReaderId is { Valid: true } accessReader)
|
||||||
|
{
|
||||||
|
targetLabel = Loc.GetString("access-overrider-window-target-label") + " " + EntityManager.GetComponent<MetaDataComponent>(component.TargetAccessReaderId).EntityName;
|
||||||
|
targetLabelColor = Color.White;
|
||||||
|
|
||||||
|
List<HashSet<string>> currentAccessHashsets = EntityManager.GetComponent<AccessReaderComponent>(accessReader).AccessLists;
|
||||||
|
currentAccess = ConvertAccessHashSetsToList(currentAccessHashsets)?.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component.PrivilegedIdSlot.Item is { Valid: true } idCard)
|
||||||
|
{
|
||||||
|
privilegedIdName = EntityManager.GetComponent<MetaDataComponent>(idCard).EntityName;
|
||||||
|
|
||||||
|
if (component.TargetAccessReaderId is { Valid: true })
|
||||||
|
{
|
||||||
|
possibleAccess = _accessReader.FindAccessTags(idCard).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentAccess != null && possibleAccess != null)
|
||||||
|
{
|
||||||
|
missingAccess = currentAccess.Except(possibleAccess).ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AccessOverriderBoundUserInterfaceState newState;
|
||||||
|
|
||||||
|
newState = new AccessOverriderBoundUserInterfaceState(
|
||||||
|
component.PrivilegedIdSlot.HasItem,
|
||||||
|
PrivilegedIdIsAuthorized(uid, component),
|
||||||
|
currentAccess,
|
||||||
|
possibleAccess,
|
||||||
|
missingAccess,
|
||||||
|
privilegedIdName,
|
||||||
|
targetLabel,
|
||||||
|
targetLabelColor);
|
||||||
|
|
||||||
|
_userInterface.TrySetUiState(uid, AccessOverriderUiKey.Key, newState);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<string> ConvertAccessHashSetsToList(List<HashSet<string>> accessHashsets)
|
||||||
|
{
|
||||||
|
List<string> accessList = new List<string>();
|
||||||
|
|
||||||
|
if (accessHashsets != null && accessHashsets.Any())
|
||||||
|
{
|
||||||
|
foreach (HashSet<string> hashSet in accessHashsets)
|
||||||
|
{
|
||||||
|
foreach (string hash in hashSet.ToArray())
|
||||||
|
{
|
||||||
|
accessList.Add(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<HashSet<string>> ConvertAccessListToHashSet(List<string> accessList)
|
||||||
|
{
|
||||||
|
List<HashSet<string>> accessHashsets = new List<HashSet<string>>();
|
||||||
|
|
||||||
|
if (accessList != null && accessList.Any())
|
||||||
|
{
|
||||||
|
foreach (string access in accessList)
|
||||||
|
{
|
||||||
|
accessHashsets.Add(new HashSet<string>() { access });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessHashsets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called whenever an access button is pressed, adding or removing that access requirement from the target access reader.
|
||||||
|
/// </summary>
|
||||||
|
private void TryWriteToTargetAccessReaderId(EntityUid uid,
|
||||||
|
List<string> newAccessList,
|
||||||
|
EntityUid player,
|
||||||
|
AccessOverriderComponent? component = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref component) || component.TargetAccessReaderId is not { Valid: true })
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!PrivilegedIdIsAuthorized(uid, component))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!_interactionSystem.InRangeUnobstructed(uid, component.TargetAccessReaderId))
|
||||||
|
{
|
||||||
|
_popupSystem.PopupEntity(Loc.GetString("access-overrider-out-of-range"), player, player);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newAccessList.Count > 0 && !newAccessList.TrueForAll(x => component.AccessLevels.Contains(x)))
|
||||||
|
{
|
||||||
|
_sawmill.Warning($"User {ToPrettyString(uid)} tried to write unknown access tag.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TryComp(component.TargetAccessReaderId, out AccessReaderComponent? accessReader);
|
||||||
|
|
||||||
|
if (accessReader == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var oldTags = ConvertAccessHashSetsToList(accessReader.AccessLists);
|
||||||
|
var privilegedId = component.PrivilegedIdSlot.Item;
|
||||||
|
|
||||||
|
if (oldTags.SequenceEqual(newAccessList))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var difference = newAccessList.Union(oldTags).Except(newAccessList.Intersect(oldTags)).ToHashSet();
|
||||||
|
var privilegedPerms = _accessReader.FindAccessTags(privilegedId!.Value).ToHashSet();
|
||||||
|
|
||||||
|
if (!difference.IsSubsetOf(privilegedPerms))
|
||||||
|
{
|
||||||
|
_sawmill.Warning($"User {ToPrettyString(uid)} tried to modify permissions they could not give/take!");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!oldTags.ToHashSet().IsSubsetOf(privilegedPerms))
|
||||||
|
{
|
||||||
|
_sawmill.Warning($"User {ToPrettyString(uid)} tried to modify permissions when they do not have sufficient access!");
|
||||||
|
_popupSystem.PopupEntity(Loc.GetString("access-overrider-cannot-modify-access"), player, player);
|
||||||
|
_audioSystem.PlayPvs(component.DenialSound, uid);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var addedTags = newAccessList.Except(oldTags).Select(tag => "+" + tag).ToList();
|
||||||
|
var removedTags = oldTags.Except(newAccessList).Select(tag => "-" + tag).ToList();
|
||||||
|
|
||||||
|
_adminLogger.Add(LogType.Action, LogImpact.Medium,
|
||||||
|
$"{ToPrettyString(player):player} has modified {ToPrettyString(component.TargetAccessReaderId):entity} with the following allowed access level holders: [{string.Join(", ", addedTags.Union(removedTags))}] [{string.Join(", ", newAccessList)}]");
|
||||||
|
|
||||||
|
accessReader.AccessLists = ConvertAccessListToHashSet(newAccessList);
|
||||||
|
Dirty(accessReader);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true if there is an ID in <see cref="AccessOverriderComponent.PrivilegedIdSlot"/> and said ID satisfies the requirements of <see cref="AccessReaderComponent"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Other code relies on the fact this returns false if privileged Id is null. Don't break that invariant.
|
||||||
|
/// </remarks>
|
||||||
|
private bool PrivilegedIdIsAuthorized(EntityUid uid, AccessOverriderComponent? component = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref component))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (!EntityManager.TryGetComponent<AccessReaderComponent>(uid, out var reader))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var privilegedId = component.PrivilegedIdSlot.Item;
|
||||||
|
return privilegedId != null && _accessReader.IsAllowed(privilegedId.Value, reader);
|
||||||
|
}
|
||||||
|
}
|
||||||
80
Content.Shared/Access/Components/AccessOverriderComponent.cs
Normal file
80
Content.Shared/Access/Components/AccessOverriderComponent.cs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
using Content.Shared.Access.Systems;
|
||||||
|
using Content.Shared.Containers.ItemSlots;
|
||||||
|
using Robust.Shared.Audio;
|
||||||
|
using Robust.Shared.GameStates;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
|
||||||
|
|
||||||
|
namespace Content.Shared.Access.Components;
|
||||||
|
|
||||||
|
[RegisterComponent, NetworkedComponent]
|
||||||
|
[Access(typeof(SharedAccessOverriderSystem))]
|
||||||
|
public sealed class AccessOverriderComponent : Component
|
||||||
|
{
|
||||||
|
public static string PrivilegedIdCardSlotId = "AccessOverrider-privilegedId";
|
||||||
|
|
||||||
|
[DataField("privilegedIdSlot")]
|
||||||
|
public ItemSlot PrivilegedIdSlot = new();
|
||||||
|
|
||||||
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
[DataField("denialSound")]
|
||||||
|
public SoundSpecifier? DenialSound;
|
||||||
|
|
||||||
|
public EntityUid TargetAccessReaderId = new();
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class WriteToTargetAccessReaderIdMessage : BoundUserInterfaceMessage
|
||||||
|
{
|
||||||
|
public readonly List<string> AccessList;
|
||||||
|
|
||||||
|
public WriteToTargetAccessReaderIdMessage(List<string> accessList)
|
||||||
|
{
|
||||||
|
AccessList = accessList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataField("accessLevels", customTypeSerializer: typeof(PrototypeIdListSerializer<AccessLevelPrototype>))]
|
||||||
|
public List<string> AccessLevels = new();
|
||||||
|
|
||||||
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
[DataField("doAfter")]
|
||||||
|
public float DoAfterTime = 0f;
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class AccessOverriderBoundUserInterfaceState : BoundUserInterfaceState
|
||||||
|
{
|
||||||
|
public readonly string TargetLabel;
|
||||||
|
public readonly Color TargetLabelColor;
|
||||||
|
public readonly string PrivilegedIdName;
|
||||||
|
public readonly bool IsPrivilegedIdPresent;
|
||||||
|
public readonly bool IsPrivilegedIdAuthorized;
|
||||||
|
public readonly string[]? TargetAccessReaderIdAccessList;
|
||||||
|
public readonly string[]? AllowedModifyAccessList;
|
||||||
|
public readonly string[]? MissingPrivilegesList;
|
||||||
|
|
||||||
|
public AccessOverriderBoundUserInterfaceState(bool isPrivilegedIdPresent,
|
||||||
|
bool isPrivilegedIdAuthorized,
|
||||||
|
string[]? targetAccessReaderIdAccessList,
|
||||||
|
string[]? allowedModifyAccessList,
|
||||||
|
string[]? missingPrivilegesList,
|
||||||
|
string privilegedIdName,
|
||||||
|
string targetLabel,
|
||||||
|
Color targetLabelColor)
|
||||||
|
{
|
||||||
|
IsPrivilegedIdPresent = isPrivilegedIdPresent;
|
||||||
|
IsPrivilegedIdAuthorized = isPrivilegedIdAuthorized;
|
||||||
|
TargetAccessReaderIdAccessList = targetAccessReaderIdAccessList;
|
||||||
|
AllowedModifyAccessList = allowedModifyAccessList;
|
||||||
|
MissingPrivilegesList = missingPrivilegesList;
|
||||||
|
PrivilegedIdName = privilegedIdName;
|
||||||
|
TargetLabel = targetLabel;
|
||||||
|
TargetLabelColor = targetLabelColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public enum AccessOverriderUiKey : byte
|
||||||
|
{
|
||||||
|
Key,
|
||||||
|
}
|
||||||
|
}
|
||||||
72
Content.Shared/Access/Systems/SharedAccessOverriderSystem.cs
Normal file
72
Content.Shared/Access/Systems/SharedAccessOverriderSystem.cs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
using Content.Shared.Access.Components;
|
||||||
|
using Content.Shared.Containers.ItemSlots;
|
||||||
|
using Content.Shared.DoAfter;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Robust.Shared.GameStates;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.Access.Systems
|
||||||
|
{
|
||||||
|
[UsedImplicitly]
|
||||||
|
public abstract class SharedAccessOverriderSystem : EntitySystem
|
||||||
|
{
|
||||||
|
[Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!;
|
||||||
|
[Dependency] private readonly ILogManager _log = default!;
|
||||||
|
|
||||||
|
public const string Sawmill = "accessoverrider";
|
||||||
|
protected ISawmill _sawmill = default!;
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
_sawmill = _log.GetSawmill(Sawmill);
|
||||||
|
|
||||||
|
SubscribeLocalEvent<AccessOverriderComponent, ComponentInit>(OnComponentInit);
|
||||||
|
SubscribeLocalEvent<AccessOverriderComponent, ComponentRemove>(OnComponentRemove);
|
||||||
|
SubscribeLocalEvent<AccessOverriderComponent, ComponentGetState>(OnGetState);
|
||||||
|
SubscribeLocalEvent<AccessOverriderComponent, ComponentHandleState>(OnHandleState);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnHandleState(EntityUid uid, AccessOverriderComponent component, ref ComponentHandleState args)
|
||||||
|
{
|
||||||
|
if (args.Current is not AccessOverriderComponentState state) return;
|
||||||
|
component.AccessLevels = state.AccessLevels;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnGetState(EntityUid uid, AccessOverriderComponent component, ref ComponentGetState args)
|
||||||
|
{
|
||||||
|
args.State = new AccessOverriderComponentState(component.AccessLevels);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnComponentInit(EntityUid uid, AccessOverriderComponent component, ComponentInit args)
|
||||||
|
{
|
||||||
|
_itemSlotsSystem.AddItemSlot(uid, AccessOverriderComponent.PrivilegedIdCardSlotId, component.PrivilegedIdSlot);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnComponentRemove(EntityUid uid, AccessOverriderComponent component, ComponentRemove args)
|
||||||
|
{
|
||||||
|
_itemSlotsSystem.RemoveItemSlot(uid, component.PrivilegedIdSlot);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
private sealed class AccessOverriderComponentState : ComponentState
|
||||||
|
{
|
||||||
|
public List<string> AccessLevels;
|
||||||
|
|
||||||
|
public AccessOverriderComponentState(List<string> accessLevels)
|
||||||
|
{
|
||||||
|
AccessLevels = accessLevels;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class AccessOverriderDoAfterEvent : DoAfterEvent
|
||||||
|
{
|
||||||
|
public AccessOverriderDoAfterEvent()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override DoAfterEvent Clone() => this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
access-overrider-window-privileged-id = Privileged ID:
|
||||||
|
access-overrider-window-eject-button = Eject
|
||||||
|
access-overrider-window-insert-button = Insert
|
||||||
|
access-overrider-window-target-label = Connected device:
|
||||||
|
access-overrider-window-no-target = No connected device
|
||||||
|
access-overrider-window-missing-privileges = Access to this device cannot be modified. The inserted ID is missing the following privileges:
|
||||||
|
access-overrider-cannot-modify-access = You do not have sufficient privileges to modify this device!
|
||||||
|
access-overrider-out-of-range = The connected device is too far away
|
||||||
|
|
||||||
@@ -117,6 +117,7 @@
|
|||||||
- id: BoxEncryptionKeyService
|
- id: BoxEncryptionKeyService
|
||||||
- id: ClothingBackpackIan
|
- id: ClothingBackpackIan
|
||||||
prob: 0.5
|
prob: 0.5
|
||||||
|
- id: AccessConfigurator
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
id: LockerChiefEngineerFilledHardsuit
|
id: LockerChiefEngineerFilledHardsuit
|
||||||
@@ -142,6 +143,7 @@
|
|||||||
- id: JetpackVoidFilled
|
- id: JetpackVoidFilled
|
||||||
- id: ClothingHeadsetAltEngineering
|
- id: ClothingHeadsetAltEngineering
|
||||||
- id: BoxEncryptionKeyEngineering
|
- id: BoxEncryptionKeyEngineering
|
||||||
|
- id: AccessConfigurator
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
id: LockerChiefEngineerFilled
|
id: LockerChiefEngineerFilled
|
||||||
@@ -161,6 +163,7 @@
|
|||||||
- id: RubberStampCE
|
- id: RubberStampCE
|
||||||
- id: ClothingHeadsetAltEngineering
|
- id: ClothingHeadsetAltEngineering
|
||||||
- id: BoxEncryptionKeyEngineering
|
- id: BoxEncryptionKeyEngineering
|
||||||
|
- id: AccessConfigurator
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
id: LockerChiefMedicalOfficerFilledHardsuit
|
id: LockerChiefMedicalOfficerFilledHardsuit
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
- type: entity
|
||||||
|
parent: BaseItem
|
||||||
|
id: AccessConfigurator
|
||||||
|
name: access configurator
|
||||||
|
description: Used to modify the access level requirements for airlocks and other lockable devices.
|
||||||
|
components:
|
||||||
|
- type: EmitSoundOnLand
|
||||||
|
sound:
|
||||||
|
path: /Audio/Items/multitool_drop.ogg
|
||||||
|
- type: Sprite
|
||||||
|
sprite: Objects/Tools/access_configurator.rsi
|
||||||
|
state: icon
|
||||||
|
- type: Item
|
||||||
|
size: 5
|
||||||
|
- type: Clothing
|
||||||
|
sprite: Objects/Tools/access_configurator.rsi
|
||||||
|
quickEquip: false
|
||||||
|
slots:
|
||||||
|
- Belt
|
||||||
|
- type: AccessOverrider
|
||||||
|
accessLevels:
|
||||||
|
- Armory
|
||||||
|
- Atmospherics
|
||||||
|
- Bar
|
||||||
|
- Brig
|
||||||
|
- Detective
|
||||||
|
- Captain
|
||||||
|
- Cargo
|
||||||
|
- Chapel
|
||||||
|
- Chemistry
|
||||||
|
- ChiefEngineer
|
||||||
|
- ChiefMedicalOfficer
|
||||||
|
- Command
|
||||||
|
- Engineering
|
||||||
|
- External
|
||||||
|
- HeadOfPersonnel
|
||||||
|
- HeadOfSecurity
|
||||||
|
- Hydroponics
|
||||||
|
- Janitor
|
||||||
|
- Kitchen
|
||||||
|
- Maintenance
|
||||||
|
- Medical
|
||||||
|
- Quartermaster
|
||||||
|
- Research
|
||||||
|
- ResearchDirector
|
||||||
|
- Salvage
|
||||||
|
- Security
|
||||||
|
- Service
|
||||||
|
- Theatre
|
||||||
|
privilegedIdSlot:
|
||||||
|
name: id-card-console-privileged-id
|
||||||
|
ejectSound: /Audio/Machines/id_swipe.ogg
|
||||||
|
insertSound: /Audio/Weapons/Guns/MagIn/batrifle_magin.ogg
|
||||||
|
ejectOnBreak: true
|
||||||
|
swap: false
|
||||||
|
whitelist:
|
||||||
|
components:
|
||||||
|
- IdCard
|
||||||
|
denialSound:
|
||||||
|
path: /Audio/Machines/custom_deny.ogg
|
||||||
|
doAfter: 0.5
|
||||||
|
- type: UserInterface
|
||||||
|
interfaces:
|
||||||
|
- key: enum.AccessOverriderUiKey.Key
|
||||||
|
type: AccessOverriderBoundUserInterface
|
||||||
|
- type: ActivatableUI
|
||||||
|
key: enum.AccessOverriderUiKey.Key
|
||||||
|
requireHands: true
|
||||||
|
closeOnHandDeselect: false
|
||||||
|
singleUser: true
|
||||||
|
- type: ItemSlots
|
||||||
|
- type: ContainerContainer
|
||||||
|
containers:
|
||||||
|
AccessOverrider-privilegedId: !type:ContainerSlot
|
||||||
@@ -128,6 +128,7 @@
|
|||||||
mode: NoSprite
|
mode: NoSprite
|
||||||
- type: PaintableAirlock
|
- type: PaintableAirlock
|
||||||
group: Standard
|
group: Standard
|
||||||
|
- type: AccessReader
|
||||||
- type: StaticPrice
|
- type: StaticPrice
|
||||||
price: 150
|
price: 150
|
||||||
placement:
|
placement:
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"license": "CC-BY-SA-3.0",
|
||||||
|
"copyright": "Color-swap of the network configurator made by 20kdc (github) for ss14",
|
||||||
|
"size": {
|
||||||
|
"x": 32,
|
||||||
|
"y": 32
|
||||||
|
},
|
||||||
|
"states": [
|
||||||
|
{
|
||||||
|
"name": "icon",
|
||||||
|
"delays": [
|
||||||
|
[
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "inhand-left",
|
||||||
|
"directions": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "inhand-right",
|
||||||
|
"directions": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "equipped-BELT",
|
||||||
|
"directions": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user