2025-04-13 08:51:35 -07:00
using System.Linq ;
using Content.Server.Chat.Systems ;
using Content.Server.Containers ;
2023-02-28 11:03:55 -05:00
using Content.Server.StationRecords.Systems ;
2023-02-05 21:02:28 +01:00
using Content.Shared.Access.Components ;
2025-04-13 08:51:35 -07:00
using static Content . Shared . Access . Components . IdCardConsoleComponent ;
2021-12-16 23:42:02 +13:00
using Content.Shared.Access.Systems ;
2025-04-13 08:51:35 -07:00
using Content.Shared.Access ;
2023-02-05 21:02:28 +01:00
using Content.Shared.Administration.Logs ;
2025-04-13 08:51:35 -07:00
using Content.Shared.Construction ;
using Content.Shared.Containers.ItemSlots ;
using Content.Shared.Damage ;
2023-02-05 21:02:28 +01:00
using Content.Shared.Database ;
using Content.Shared.Roles ;
using Content.Shared.StationRecords ;
2025-04-13 08:51:35 -07:00
using Content.Shared.Throwing ;
2021-11-24 20:03:07 +13:00
using JetBrains.Annotations ;
2023-02-05 21:02:28 +01:00
using Robust.Server.GameObjects ;
2021-11-24 20:03:07 +13:00
using Robust.Shared.Containers ;
2023-02-05 21:02:28 +01:00
using Robust.Shared.Prototypes ;
2025-04-13 08:51:35 -07:00
using Robust.Shared.Random ;
2021-11-24 20:03:07 +13:00
2023-02-05 21:02:28 +01:00
namespace Content.Server.Access.Systems ;
[UsedImplicitly]
public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
2021-11-24 20:03:07 +13:00
{
2023-02-05 21:02:28 +01:00
[Dependency] private readonly IPrototypeManager _prototype = default ! ;
[Dependency] private readonly StationRecordsSystem _record = default ! ;
[Dependency] private readonly UserInterfaceSystem _userInterface = default ! ;
[Dependency] private readonly AccessReaderSystem _accessReader = default ! ;
[Dependency] private readonly AccessSystem _access = default ! ;
[Dependency] private readonly IdCardSystem _idCard = default ! ;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default ! ;
2025-04-13 08:51:35 -07:00
[Dependency] private readonly SharedContainerSystem _container = default ! ;
[Dependency] private readonly ThrowingSystem _throwing = default ! ;
[Dependency] private readonly IRobustRandom _random = default ! ;
[Dependency] private readonly ChatSystem _chat = default ! ;
2023-02-05 21:02:28 +01:00
public override void Initialize ( )
{
base . Initialize ( ) ;
2023-04-08 13:15:52 -07:00
SubscribeLocalEvent < IdCardConsoleComponent , WriteToTargetIdMessage > ( OnWriteToTargetIdMessage ) ;
2023-02-05 21:02:28 +01:00
// one day, maybe bound user interfaces can be shared too.
2023-04-08 13:15:52 -07:00
SubscribeLocalEvent < IdCardConsoleComponent , ComponentStartup > ( UpdateUserInterface ) ;
SubscribeLocalEvent < IdCardConsoleComponent , EntInsertedIntoContainerMessage > ( UpdateUserInterface ) ;
SubscribeLocalEvent < IdCardConsoleComponent , EntRemovedFromContainerMessage > ( UpdateUserInterface ) ;
2025-04-13 08:51:35 -07:00
SubscribeLocalEvent < IdCardConsoleComponent , DamageChangedEvent > ( OnDamageChanged ) ;
// Intercept the event before anyone can do anything with it!
SubscribeLocalEvent < IdCardConsoleComponent , MachineDeconstructedEvent > ( OnMachineDeconstructed ,
before : [ typeof ( EmptyOnMachineDeconstructSystem ) , typeof ( ItemSlotsSystem ) ] ) ;
2023-02-05 21:02:28 +01:00
}
2023-04-08 13:15:52 -07:00
private void OnWriteToTargetIdMessage ( EntityUid uid , IdCardConsoleComponent component , WriteToTargetIdMessage args )
2023-02-05 21:02:28 +01:00
{
2024-04-26 18:16:24 +10:00
if ( args . Actor is not { Valid : true } player )
2023-02-05 21:02:28 +01:00
return ;
TryWriteToTargetId ( uid , args . FullName , args . JobTitle , args . AccessList , args . JobPrototype , player , component ) ;
UpdateUserInterface ( uid , component , args ) ;
}
2023-04-08 13:15:52 -07:00
private void UpdateUserInterface ( EntityUid uid , IdCardConsoleComponent component , EntityEventArgs args )
2023-02-05 21:02:28 +01:00
{
if ( ! component . Initialized )
return ;
2023-05-05 08:56:54 -05:00
var privilegedIdName = string . Empty ;
2024-04-01 09:06:13 +03:00
List < ProtoId < AccessLevelPrototype > > ? possibleAccess = null ;
2023-05-05 08:56:54 -05:00
if ( component . PrivilegedIdSlot . Item is { Valid : true } item )
{
2025-06-26 19:50:49 -04:00
privilegedIdName = Comp < MetaDataComponent > ( item ) . EntityName ;
2024-04-01 09:06:13 +03:00
possibleAccess = _accessReader . FindAccessTags ( item ) . ToList ( ) ;
2023-05-05 08:56:54 -05:00
}
2023-02-05 21:02:28 +01:00
IdCardConsoleBoundUserInterfaceState newState ;
// this could be prettier
if ( component . TargetIdSlot . Item is not { Valid : true } targetId )
{
newState = new IdCardConsoleBoundUserInterfaceState (
component . PrivilegedIdSlot . HasItem ,
PrivilegedIdIsAuthorized ( uid , component ) ,
false ,
null ,
null ,
null ,
2023-05-05 08:56:54 -05:00
possibleAccess ,
2023-02-05 21:02:28 +01:00
string . Empty ,
privilegedIdName ,
string . Empty ) ;
}
else
{
2025-06-26 19:50:49 -04:00
var targetIdComponent = Comp < IdCardComponent > ( targetId ) ;
var targetAccessComponent = Comp < AccessComponent > ( targetId ) ;
2023-02-05 21:02:28 +01:00
2025-04-14 17:02:27 +02:00
var jobProto = targetIdComponent . JobPrototype ? ? new ProtoId < AccessLevelPrototype > ( string . Empty ) ;
2024-02-04 23:29:35 +00:00
if ( TryComp < StationRecordKeyStorageComponent > ( targetId , out var keyStorage )
2025-04-14 17:02:27 +02:00
& & keyStorage . Key is { } key
2024-02-04 23:29:35 +00:00
& & _record . TryGetRecord < GeneralStationRecord > ( key , out var record ) )
2023-02-05 21:02:28 +01:00
{
jobProto = record . JobPrototype ;
}
newState = new IdCardConsoleBoundUserInterfaceState (
component . PrivilegedIdSlot . HasItem ,
PrivilegedIdIsAuthorized ( uid , component ) ,
true ,
targetIdComponent . FullName ,
2024-10-09 18:05:36 +03:00
targetIdComponent . LocalizedJobTitle ,
2024-04-01 09:06:13 +03:00
targetAccessComponent . Tags . ToList ( ) ,
2023-05-05 08:56:54 -05:00
possibleAccess ,
2023-02-05 21:02:28 +01:00
jobProto ,
2023-05-05 08:56:54 -05:00
privilegedIdName ,
2024-02-04 23:29:35 +00:00
Name ( targetId ) ) ;
2023-02-05 21:02:28 +01:00
}
2024-04-26 18:16:24 +10:00
_userInterface . SetUiState ( uid , IdCardConsoleUiKey . Key , newState ) ;
2023-02-05 21:02:28 +01:00
}
/// <summary>
/// Called whenever an access button is pressed, adding or removing that access from the target ID card.
2023-04-08 13:15:52 -07:00
/// Writes data passed from the UI into the ID stored in <see cref="IdCardConsoleComponent.TargetIdSlot"/>, if present.
2023-02-05 21:02:28 +01:00
/// </summary>
private void TryWriteToTargetId ( EntityUid uid ,
string newFullName ,
string newJobTitle ,
2024-04-01 09:06:13 +03:00
List < ProtoId < AccessLevelPrototype > > newAccessList ,
ProtoId < AccessLevelPrototype > newJobProto ,
2023-02-05 21:02:28 +01:00
EntityUid player ,
2023-04-08 13:15:52 -07:00
IdCardConsoleComponent ? component = null )
2021-11-24 20:03:07 +13:00
{
2023-02-05 21:02:28 +01:00
if ( ! Resolve ( uid , ref component ) )
return ;
if ( component . TargetIdSlot . Item is not { Valid : true } targetId | | ! PrivilegedIdIsAuthorized ( uid , component ) )
return ;
_idCard . TryChangeFullName ( targetId , newFullName , player : player ) ;
_idCard . TryChangeJobTitle ( targetId , newJobTitle , player : player ) ;
2023-07-29 10:25:27 +02:00
if ( _prototype . TryIndex < JobPrototype > ( newJobProto , out var job )
2024-06-03 12:12:21 -04:00
& & _prototype . TryIndex ( job . Icon , out var jobIcon ) )
2023-07-29 10:25:27 +02:00
{
_idCard . TryChangeJobIcon ( targetId , jobIcon , player : player ) ;
2024-02-10 04:17:25 -05:00
_idCard . TryChangeJobDepartment ( targetId , job ) ;
2023-07-29 10:25:27 +02:00
}
2024-05-25 20:08:15 +00:00
UpdateStationRecord ( uid , targetId , newFullName , newJobTitle , job ) ;
2025-04-14 17:02:27 +02:00
if ( ( ! TryComp < StationRecordKeyStorageComponent > ( targetId , out var keyStorage )
| | keyStorage . Key is not { } key
| | ! _record . TryGetRecord < GeneralStationRecord > ( key , out _ ) )
& & newJobProto ! = string . Empty )
{
Comp < IdCardComponent > ( targetId ) . JobPrototype = newJobProto ;
}
2024-05-25 20:08:15 +00:00
2023-02-05 21:02:28 +01:00
if ( ! newAccessList . TrueForAll ( x = > component . AccessLevels . Contains ( x ) ) )
2021-11-24 20:03:07 +13:00
{
2023-05-05 08:56:54 -05:00
_sawmill . Warning ( $"User {ToPrettyString(uid)} tried to write unknown access tag." ) ;
2023-02-05 21:02:28 +01:00
return ;
}
2024-04-01 09:06:13 +03:00
var oldTags = _access . TryGetTags ( targetId ) ? ? new List < ProtoId < AccessLevelPrototype > > ( ) ;
2023-02-05 21:02:28 +01:00
oldTags = oldTags . ToList ( ) ;
2021-11-24 20:03:07 +13:00
2023-05-05 08:56:54 -05:00
var privilegedId = component . PrivilegedIdSlot . Item ;
2023-02-05 21:02:28 +01:00
if ( oldTags . SequenceEqual ( newAccessList ) )
return ;
2023-05-05 08:56:54 -05:00
// I hate that C# doesn't have an option for this and don't desire to write this out the hard way.
// var difference = newAccessList.Difference(oldTags);
2023-07-29 10:25:27 +02:00
var difference = newAccessList . Union ( oldTags ) . Except ( newAccessList . Intersect ( oldTags ) ) . ToHashSet ( ) ;
2023-05-05 08:56:54 -05:00
// NULL SAFETY: PrivilegedIdIsAuthorized checked this earlier.
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 ;
}
2023-02-05 21:02:28 +01:00
var addedTags = newAccessList . Except ( oldTags ) . Select ( tag = > "+" + tag ) . ToList ( ) ;
var removedTags = oldTags . Except ( newAccessList ) . Select ( tag = > "-" + tag ) . ToList ( ) ;
_access . TrySetTags ( targetId , newAccessList ) ;
/ * TODO : ECS SharedIdCardConsoleComponent and then log on card ejection , together with the save .
This current implementation is pretty shit as it logs 27 entries ( 27 lines ) if someone decides to give themselves AA * /
2025-04-04 11:18:02 +02:00
_adminLogger . Add ( LogType . Action , LogImpact . Medium ,
2023-02-05 21:02:28 +01:00
$"{ToPrettyString(player):player} has modified {ToPrettyString(targetId):entity} with the following accesses: [{string.Join(" , ", addedTags.Union(removedTags))}] [{string.Join(" , ", newAccessList)}]" ) ;
}
/// <summary>
2023-04-08 13:15:52 -07:00
/// Returns true if there is an ID in <see cref="IdCardConsoleComponent.PrivilegedIdSlot"/> and said ID satisfies the requirements of <see cref="AccessReaderComponent"/>.
2023-02-05 21:02:28 +01:00
/// </summary>
2023-05-05 08:56:54 -05:00
/// <remarks>
/// Other code relies on the fact this returns false if privileged Id is null. Don't break that invariant.
/// </remarks>
2023-04-08 13:15:52 -07:00
private bool PrivilegedIdIsAuthorized ( EntityUid uid , IdCardConsoleComponent ? component = null )
2023-02-05 21:02:28 +01:00
{
if ( ! Resolve ( uid , ref component ) )
return true ;
2024-02-04 23:29:35 +00:00
if ( ! TryComp < AccessReaderComponent > ( uid , out var reader ) )
2023-02-05 21:02:28 +01:00
return true ;
var privilegedId = component . PrivilegedIdSlot . Item ;
2023-09-03 13:05:22 +12:00
return privilegedId ! = null & & _accessReader . IsAllowed ( privilegedId . Value , uid , reader ) ;
2023-02-05 21:02:28 +01:00
}
2024-04-01 09:06:13 +03:00
private void UpdateStationRecord ( EntityUid uid , EntityUid targetId , string newFullName , ProtoId < AccessLevelPrototype > newJobTitle , JobPrototype ? newJobProto )
2023-02-05 21:02:28 +01:00
{
2024-02-04 23:29:35 +00:00
if ( ! TryComp < StationRecordKeyStorageComponent > ( targetId , out var keyStorage )
2023-02-05 21:02:28 +01:00
| | keyStorage . Key is not { } key
2024-02-04 23:29:35 +00:00
| | ! _record . TryGetRecord < GeneralStationRecord > ( key , out var record ) )
2023-02-05 21:02:28 +01:00
{
return ;
2021-11-24 20:03:07 +13:00
}
2023-02-05 21:02:28 +01:00
record . Name = newFullName ;
record . JobTitle = newJobTitle ;
2023-07-29 10:25:27 +02:00
if ( newJobProto ! = null )
2023-02-05 21:02:28 +01:00
{
2023-07-29 10:25:27 +02:00
record . JobPrototype = newJobProto . ID ;
record . JobIcon = newJobProto . Icon ;
2023-02-05 21:02:28 +01:00
}
2024-02-04 23:29:35 +00:00
_record . Synchronize ( key ) ;
2021-11-24 20:03:07 +13:00
}
2025-04-13 08:51:35 -07:00
private void OnMachineDeconstructed ( Entity < IdCardConsoleComponent > entity , ref MachineDeconstructedEvent args )
{
TryDropAndThrowIds ( entity . AsNullable ( ) ) ;
}
private void OnDamageChanged ( Entity < IdCardConsoleComponent > entity , ref DamageChangedEvent args )
{
if ( TryDropAndThrowIds ( entity . AsNullable ( ) ) )
_chat . TrySendInGameICMessage ( entity , Loc . GetString ( "id-card-console-damaged" ) , InGameICChatType . Speak , true ) ;
}
#region PublicAPI
/// <summary>
/// Tries to drop any IDs stored in the console, and then tries to throw them away.
/// Returns true if anything was ejected and false otherwise.
/// </summary>
public bool TryDropAndThrowIds ( Entity < IdCardConsoleComponent ? , ItemSlotsComponent ? > ent )
{
if ( ! Resolve ( ent , ref ent . Comp1 , ref ent . Comp2 ) )
return false ;
var didEject = false ;
foreach ( var slot in ent . Comp2 . Slots . Values )
{
if ( slot . Item = = null | | slot . ContainerSlot = = null )
continue ;
var item = slot . Item . Value ;
if ( _container . Remove ( item , slot . ContainerSlot ) )
{
_throwing . TryThrow ( item , _random . NextVector2 ( ) , baseThrowSpeed : 5f ) ;
didEject = true ;
}
}
return didEject ;
}
#endregion
2021-11-24 20:03:07 +13:00
}