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:
chromiumboy
2023-08-08 13:30:46 -05:00
committed by GitHub
parent 4509312eea
commit 2df70799f8
16 changed files with 783 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
using Content.Shared.Access.Systems;
using JetBrains.Annotations;
namespace Content.Client.Access
{
[UsedImplicitly]
public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
{
}
}

View File

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

View 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>

View 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());
}
}
}

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

View 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,
}
}

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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
}
]
}