]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Wanted list cartridge (#31223)
authorЭдуард <36124833+Ertanic@users.noreply.github.com>
Thu, 19 Sep 2024 10:22:02 +0000 (13:22 +0300)
committerGitHub <noreply@github.com>
Thu, 19 Sep 2024 10:22:02 +0000 (12:22 +0200)
* WantedListCartridge has been added

* WantedListCartridge user interface works

* WantedListCartridge is added as standard in some PDAs

* The CriminalRecordsSystem can now also take into account who created the record

* Added offense history table

* Fix of missing loaderUid for a cartridge without installing the program

* Added personalized information about the target

* The crime history has been finalized

* Added StatusList

* The officer's name has been added to the automatic history

* WantedListCartridge has been added to the HOS locker

* WantedListCartridge has been removed from brigmedic's preset programs

* The StealConditionSystem now takes into account whether a cartridge is inserted or installed

* Added target to thief on WantedListCartridge

* Merge fix

* Removing copypaste

* Fix merge 2

* The sprite of WantedListCartridge has been changed

* Update pda.yml

* Fix scrollbar in the history table

* Upstream localization fix

* `StatusList` has been replaced by `ListContainer` with `TextureRect`

* Margin fix

22 files changed:
Content.Client/CartridgeLoader/Cartridges/WantedListUi.cs [new file with mode: 0644]
Content.Client/CartridgeLoader/Cartridges/WantedListUiFragment.cs [new file with mode: 0644]
Content.Client/CartridgeLoader/Cartridges/WantedListUiFragment.xaml [new file with mode: 0644]
Content.Server/CartridgeLoader/CartridgeLoaderSystem.cs
Content.Server/CartridgeLoader/Cartridges/WantedListCartridge.cs [new file with mode: 0644]
Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs
Content.Server/CriminalRecords/Systems/CriminalRecordsSystem.cs
Content.Server/Objectives/Systems/StealConditionSystem.cs
Content.Shared/CartridgeLoader/Cartridges/WantedListUiState.cs [new file with mode: 0644]
Content.Shared/CriminalRecords/CriminalRecord.cs
Content.Shared/CriminalRecords/Systems/SharedCriminalRecordsSystem.cs
Resources/Locale/en-US/cartridge-loader/cartridges.ftl
Resources/Locale/en-US/criminal-records/criminal-records.ftl
Resources/Locale/en-US/objectives/conditions/steal-target-groups.ftl
Resources/Prototypes/Catalog/Fills/Lockers/heads.yml
Resources/Prototypes/Entities/Objects/Devices/cartridges.yml
Resources/Prototypes/Entities/Objects/Devices/pda.yml
Resources/Prototypes/Objectives/objectiveGroups.yml
Resources/Prototypes/Objectives/stealTargetGroups.yml
Resources/Prototypes/Objectives/thief.yml
Resources/Textures/Objects/Devices/cartridge.rsi/cart-sec.png [new file with mode: 0644]
Resources/Textures/Objects/Devices/cartridge.rsi/meta.json

diff --git a/Content.Client/CartridgeLoader/Cartridges/WantedListUi.cs b/Content.Client/CartridgeLoader/Cartridges/WantedListUi.cs
new file mode 100644 (file)
index 0000000..3c97b8b
--- /dev/null
@@ -0,0 +1,30 @@
+using Content.Client.UserInterface.Fragments;
+using Content.Shared.CartridgeLoader.Cartridges;
+using Robust.Client.UserInterface;
+
+namespace Content.Client.CartridgeLoader.Cartridges;
+
+public sealed partial class WantedListUi : UIFragment
+{
+    private WantedListUiFragment? _fragment;
+
+    public override Control GetUIFragmentRoot()
+    {
+        return _fragment!;
+    }
+
+    public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner)
+    {
+        _fragment = new WantedListUiFragment();
+    }
+
+    public override void UpdateState(BoundUserInterfaceState state)
+    {
+        switch (state)
+        {
+            case WantedListUiState cast:
+                _fragment?.UpdateState(cast.Records);
+                break;
+        }
+    }
+}
diff --git a/Content.Client/CartridgeLoader/Cartridges/WantedListUiFragment.cs b/Content.Client/CartridgeLoader/Cartridges/WantedListUiFragment.cs
new file mode 100644 (file)
index 0000000..4137f6c
--- /dev/null
@@ -0,0 +1,240 @@
+using System.Linq;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.CriminalRecords.Systems;
+using Content.Shared.Security;
+using Content.Shared.StatusIcon;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Input;
+using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Client.CartridgeLoader.Cartridges;
+
+[GenerateTypedNameReferences]
+public sealed partial class WantedListUiFragment : BoxContainer
+{
+    [Dependency] private readonly IEntitySystemManager _entitySystem = default!;
+    [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+    private readonly SpriteSystem _spriteSystem;
+
+    private string? _selectedTargetName;
+    private List<WantedRecord> _wantedRecords = new();
+
+    public WantedListUiFragment()
+    {
+        RobustXamlLoader.Load(this);
+        IoCManager.InjectDependencies(this);
+        _spriteSystem = _entitySystem.GetEntitySystem<SpriteSystem>();
+
+        SearchBar.OnTextChanged += OnSearchBarTextChanged;
+    }
+
+    private void OnSearchBarTextChanged(LineEdit.LineEditEventArgs args)
+    {
+        var found = !String.IsNullOrWhiteSpace(args.Text)
+            ? _wantedRecords.FindAll(r =>
+                r.TargetInfo.Name.Contains(args.Text) ||
+                r.Status.ToString().Contains(args.Text, StringComparison.OrdinalIgnoreCase))
+            : _wantedRecords;
+
+        UpdateState(found, false);
+    }
+
+    public void UpdateState(List<WantedRecord> records, bool refresh = true)
+    {
+        if (records.Count == 0)
+        {
+            NoRecords.Visible = true;
+            RecordsList.Visible = false;
+            RecordUnselected.Visible = false;
+            PersonContainer.Visible = false;
+
+            _selectedTargetName = null;
+            if (refresh)
+                _wantedRecords.Clear();
+
+            RecordsList.PopulateList(new List<ListData>());
+
+            return;
+        }
+
+        NoRecords.Visible = false;
+        RecordsList.Visible = true;
+        RecordUnselected.Visible = true;
+        PersonContainer.Visible = false;
+
+        var dataList = records.Select(r => new StatusListData(r)).ToList();
+
+        RecordsList.GenerateItem = GenerateItem;
+        RecordsList.ItemPressed = OnItemSelected;
+        RecordsList.PopulateList(dataList);
+
+        if (refresh)
+            _wantedRecords = records;
+    }
+
+    private void OnItemSelected(BaseButton.ButtonEventArgs args, ListData data)
+    {
+        if (data is not StatusListData(var record))
+            return;
+
+        FormattedMessage GetLoc(string fluentId, params (string,object)[] args)
+        {
+            var msg = new FormattedMessage();
+            var fluent = Loc.GetString(fluentId, args);
+            msg.AddMarkupPermissive(fluent);
+            return msg;
+        }
+
+        // Set personal info
+        PersonName.Text = record.TargetInfo.Name;
+        TargetAge.SetMessage(GetLoc(
+            "wanted-list-age-label",
+            ("age", record.TargetInfo.Age)
+        ));
+        TargetJob.SetMessage(GetLoc(
+            "wanted-list-job-label",
+            ("job", record.TargetInfo.JobTitle.ToLower())
+        ));
+        TargetSpecies.SetMessage(GetLoc(
+            "wanted-list-species-label",
+            ("species", record.TargetInfo.Species.ToLower())
+        ));
+        TargetGender.SetMessage(GetLoc(
+            "wanted-list-gender-label",
+            ("gender", record.TargetInfo.Gender)
+        ));
+
+        // Set reason
+        WantedReason.SetMessage(GetLoc(
+            "wanted-list-reason-label",
+            ("reason", record.Reason ?? Loc.GetString("wanted-list-unknown-reason-label"))
+        ));
+
+        // Set status
+        PersonState.SetMessage(GetLoc(
+            "wanted-list-status-label",
+            ("status", record.Status.ToString().ToLower())
+        ));
+
+        // Set initiator
+        InitiatorName.SetMessage(GetLoc(
+            "wanted-list-initiator-label",
+            ("initiator", record.Initiator ?? Loc.GetString("wanted-list-unknown-initiator-label"))
+        ));
+
+        // History table
+        // Clear table if it exists
+        HistoryTable.RemoveAllChildren();
+
+        HistoryTable.AddChild(new Label()
+        {
+            Text = Loc.GetString("wanted-list-history-table-time-col"),
+            StyleClasses = { "LabelSmall" },
+            HorizontalAlignment = HAlignment.Center,
+        });
+        HistoryTable.AddChild(new Label()
+        {
+            Text = Loc.GetString("wanted-list-history-table-reason-col"),
+            StyleClasses = { "LabelSmall" },
+            HorizontalAlignment = HAlignment.Center,
+            HorizontalExpand = true,
+        });
+
+        HistoryTable.AddChild(new Label()
+        {
+            Text = Loc.GetString("wanted-list-history-table-initiator-col"),
+            StyleClasses = { "LabelSmall" },
+            HorizontalAlignment = HAlignment.Center,
+        });
+
+        if (record.History.Count > 0)
+        {
+            HistoryTable.Visible = true;
+
+            foreach (var history in record.History.OrderByDescending(h => h.AddTime))
+            {
+                HistoryTable.AddChild(new Label()
+                {
+                    Text = $"{history.AddTime.Hours:00}:{history.AddTime.Minutes:00}:{history.AddTime.Seconds:00}",
+                    StyleClasses = { "LabelSmall" },
+                    VerticalAlignment = VAlignment.Top,
+                });
+
+                HistoryTable.AddChild(new RichTextLabel()
+                {
+                    Text = $"[color=white]{history.Crime}[/color]",
+                    HorizontalExpand = true,
+                    VerticalAlignment = VAlignment.Top,
+                    StyleClasses = { "LabelSubText" },
+                    Margin = new(10f, 0f),
+                });
+
+                HistoryTable.AddChild(new RichTextLabel()
+                {
+                    Text = $"[color=white]{history.InitiatorName}[/color]",
+                    StyleClasses = { "LabelSubText" },
+                    VerticalAlignment = VAlignment.Top,
+                });
+            }
+        }
+
+        RecordUnselected.Visible = false;
+        PersonContainer.Visible = true;
+
+        // Save selected item
+        _selectedTargetName = record.TargetInfo.Name;
+    }
+
+    private void GenerateItem(ListData data, ListContainerButton button)
+    {
+        if (data is not StatusListData(var record))
+            return;
+
+        var box = new BoxContainer() { Orientation = LayoutOrientation.Horizontal, HorizontalExpand = true };
+        var label = new Label() { Text = record.TargetInfo.Name };
+        var rect = new TextureRect()
+        {
+            TextureScale = new(2.2f),
+            VerticalAlignment = VAlignment.Center,
+            HorizontalAlignment = HAlignment.Center,
+            Margin = new(0f, 0f, 6f, 0f),
+        };
+
+        if (record.Status is not SecurityStatus.None)
+        {
+            var proto = "SecurityIcon" + record.Status switch
+            {
+                SecurityStatus.Detained => "Incarcerated",
+                _ => record.Status.ToString(),
+            };
+
+            if (_prototypeManager.TryIndex<SecurityIconPrototype>(proto, out var prototype))
+            {
+                rect.Texture = _spriteSystem.Frame0(prototype.Icon);
+            }
+        }
+
+        box.AddChild(rect);
+        box.AddChild(label);
+        button.AddChild(box);
+        button.AddStyleClass(ListContainer.StyleClassListContainerButton);
+
+        if (record.TargetInfo.Name.Equals(_selectedTargetName))
+        {
+            button.Pressed = true;
+            // For some reason the event is not called when `Pressed` changed, call it manually.
+            OnItemSelected(
+                new(button, new(new(), BoundKeyState.Down, new(), false, new(), new())),
+                data);
+        }
+    }
+}
+
+internal record StatusListData(WantedRecord Record) : ListData;
diff --git a/Content.Client/CartridgeLoader/Cartridges/WantedListUiFragment.xaml b/Content.Client/CartridgeLoader/Cartridges/WantedListUiFragment.xaml
new file mode 100644 (file)
index 0000000..7b5d116
--- /dev/null
@@ -0,0 +1,50 @@
+<cartridges:WantedListUiFragment xmlns:cartridges="clr-namespace:Content.Client.CartridgeLoader.Cartridges"
+        xmlns="https://spacestation14.io"
+        xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+        Orientation="Vertical"
+        VerticalExpand="True"
+        HorizontalExpand="True">
+
+    <LineEdit Name="SearchBar" PlaceHolder="{Loc 'wanted-list-search-placeholder'}"/>
+
+    <BoxContainer Name="MainContainer" Orientation="Horizontal" HorizontalExpand="True" VerticalExpand="True">
+        <Label Name="NoRecords" Text="{Loc 'wanted-list-label-no-records'}" Align="Center" VAlign="Center" HorizontalExpand="True" FontColorOverride="DarkGray"/>
+
+        <!-- Any attempts to set dimensions for ListContainer breaks the renderer, I have to roughly set sizes and margins in other controllers. -->
+        <controls:ListContainer
+            Name="RecordsList"
+            HorizontalAlignment="Left"
+            VerticalExpand="True"
+            Visible="False"
+            Toggle="True"
+            Group="True"
+            SetWidth="192" />
+
+        <Label Name="RecordUnselected"
+               Text="{Loc 'criminal-records-console-select-record-info'}"
+               Align="Center"
+               FontColorOverride="DarkGray"
+               Visible="False"
+               HorizontalExpand="True" />
+        <BoxContainer Name="PersonContainer" Orientation="Vertical" HorizontalExpand="True" SetWidth="334" Margin="5 0 77 0">
+            <BoxContainer Orientation="Horizontal" HorizontalExpand="True">
+                <Label Name="PersonName" StyleClasses="LabelBig" />
+                <RichTextLabel Name="PersonState" HorizontalAlignment="Right" HorizontalExpand="True" />
+            </BoxContainer>
+            <PanelContainer StyleClasses="LowDivider" Margin="0 5 0 5"/>
+            <ScrollContainer VerticalExpand="True" HScrollEnabled="False">
+                <BoxContainer Name="DataContainer" Orientation="Vertical">
+                    <RichTextLabel Name="TargetAge" />
+                    <RichTextLabel Name="TargetJob" />
+                    <RichTextLabel Name="TargetSpecies" />
+                    <RichTextLabel Name="TargetGender" />
+                    <PanelContainer StyleClasses="LowDivider" Margin="0 5 0 5"/>
+                    <RichTextLabel Name="InitiatorName" VerticalAlignment="Stretch"/>
+                    <RichTextLabel Name="WantedReason" VerticalAlignment="Stretch"/>
+                    <PanelContainer StyleClasses="LowDivider" Margin="0 5 0 5" />
+                    <controls:TableContainer Name="HistoryTable" Columns="3" Visible="False" HorizontalAlignment="Stretch" />
+                </BoxContainer>
+            </ScrollContainer>
+        </BoxContainer>
+    </BoxContainer>
+</cartridges:WantedListUiFragment>
index cd422328c3e183ff4f6a299a02608e00593cd8e6..7caec6150ede0f3ee0d94998781fc738ff37ecf0 100644 (file)
@@ -340,6 +340,9 @@ public sealed class CartridgeLoaderSystem : SharedCartridgeLoaderSystem
         if (args.Container.ID != InstalledContainerId && args.Container.ID != loader.CartridgeSlot.ID)
             return;
 
+        if (TryComp(args.Entity, out CartridgeComponent? cartridge))
+            cartridge.LoaderUid = uid;
+
         RaiseLocalEvent(args.Entity, new CartridgeAddedEvent(uid));
         base.OnItemInserted(uid, loader, args);
     }
@@ -360,6 +363,9 @@ public sealed class CartridgeLoaderSystem : SharedCartridgeLoaderSystem
         if (deactivate)
             RaiseLocalEvent(args.Entity, new CartridgeDeactivatedEvent(uid));
 
+        if (TryComp(args.Entity, out CartridgeComponent? cartridge))
+            cartridge.LoaderUid = null;
+
         RaiseLocalEvent(args.Entity, new CartridgeRemovedEvent(uid));
         base.OnItemRemoved(uid, loader, args);
 
diff --git a/Content.Server/CartridgeLoader/Cartridges/WantedListCartridge.cs b/Content.Server/CartridgeLoader/Cartridges/WantedListCartridge.cs
new file mode 100644 (file)
index 0000000..08eef62
--- /dev/null
@@ -0,0 +1,8 @@
+using Content.Shared.Security;
+
+namespace Content.Server.CartridgeLoader.Cartridges;
+
+[RegisterComponent]
+public sealed partial class WantedListCartridgeComponent : Component
+{
+}
index c5f1d159f31c43f10608adb56e868d8321e24180..ca1d45e6449466344a89e52c0e467c9fed4a180d 100644 (file)
@@ -68,6 +68,13 @@ public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleS
         }
     }
 
+    private void GetOfficer(EntityUid uid, out string officer)
+    {
+        var tryGetIdentityShortInfoEvent = new TryGetIdentityShortInfoEvent(null, uid);
+        RaiseLocalEvent(tryGetIdentityShortInfoEvent);
+        officer = tryGetIdentityShortInfoEvent.Title ?? Loc.GetString("criminal-records-console-unknown-officer");
+    }
+
     private void OnChangeStatus(Entity<CriminalRecordsConsoleComponent> ent, ref CriminalRecordChangeStatus msg)
     {
         // prevent malf client violating wanted/reason nullability
@@ -90,29 +97,22 @@ public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleS
                 return;
         }
 
+        var oldStatus = record.Status;
+
+        var name = _records.RecordName(key.Value);
+        GetOfficer(mob.Value, out var officer);
+
         // when arresting someone add it to history automatically
         // fallback exists if the player was not set to wanted beforehand
         if (msg.Status == SecurityStatus.Detained)
         {
             var oldReason = record.Reason ?? Loc.GetString("criminal-records-console-unspecified-reason");
             var history = Loc.GetString("criminal-records-console-auto-history", ("reason", oldReason));
-            _criminalRecords.TryAddHistory(key.Value, history);
+            _criminalRecords.TryAddHistory(key.Value, history, officer);
         }
 
-        var oldStatus = record.Status;
-
         // will probably never fail given the checks above
-        _criminalRecords.TryChangeStatus(key.Value, msg.Status, msg.Reason);
-
-        var name = _records.RecordName(key.Value);
-        var officer = Loc.GetString("criminal-records-console-unknown-officer");
-
-        var tryGetIdentityShortInfoEvent = new TryGetIdentityShortInfoEvent(null, mob.Value);
-        RaiseLocalEvent(tryGetIdentityShortInfoEvent);
-        if (tryGetIdentityShortInfoEvent.Title != null)
-        {
-            officer = tryGetIdentityShortInfoEvent.Title;
-        }
+        _criminalRecords.TryChangeStatus(key.Value, msg.Status, msg.Reason, officer);
 
         (string, object)[] args;
         if (reason != null)
@@ -152,14 +152,16 @@ public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleS
 
     private void OnAddHistory(Entity<CriminalRecordsConsoleComponent> ent, ref CriminalRecordAddHistory msg)
     {
-        if (!CheckSelected(ent, msg.Actor, out _, out var key))
+        if (!CheckSelected(ent, msg.Actor, out var mob, out var key))
             return;
 
         var line = msg.Line.Trim();
         if (line.Length < 1 || line.Length > ent.Comp.MaxStringLength)
             return;
 
-        if (!_criminalRecords.TryAddHistory(key.Value, line))
+        GetOfficer(mob.Value, out var officer);
+
+        if (!_criminalRecords.TryAddHistory(key.Value, line, officer))
             return;
 
         // no radio message since its not crucial to officers patrolling
index a65fb0be9e1a7021ce32218eff2089aa96b83f28..7c65ce8c248c584cd00aeea565deb25c36db4d3e 100644 (file)
@@ -1,10 +1,15 @@
-using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Content.Server.CartridgeLoader;
+using Content.Server.CartridgeLoader.Cartridges;
 using Content.Server.StationRecords.Systems;
 using Content.Shared.CriminalRecords;
 using Content.Shared.CriminalRecords.Systems;
 using Content.Shared.Security;
 using Content.Shared.StationRecords;
 using Content.Server.GameTicking;
+using Content.Server.Station.Systems;
+using Content.Shared.CartridgeLoader;
+using Content.Shared.CartridgeLoader.Cartridges;
 
 namespace Content.Server.CriminalRecords.Systems;
 
@@ -20,12 +25,18 @@ public sealed class CriminalRecordsSystem : SharedCriminalRecordsSystem
 {
     [Dependency] private readonly GameTicker _ticker = default!;
     [Dependency] private readonly StationRecordsSystem _records = default!;
+    [Dependency] private readonly StationSystem _station = default!;
+    [Dependency] private readonly CartridgeLoaderSystem _cartridge = default!;
 
     public override void Initialize()
     {
         base.Initialize();
 
         SubscribeLocalEvent<AfterGeneralRecordCreatedEvent>(OnGeneralRecordCreated);
+        SubscribeLocalEvent<WantedListCartridgeComponent, CriminalRecordChangedEvent>(OnRecordChanged);
+        SubscribeLocalEvent<WantedListCartridgeComponent, CartridgeUiReadyEvent>(OnCartridgeUiReady);
+        SubscribeLocalEvent<WantedListCartridgeComponent, CriminalHistoryAddedEvent>(OnHistoryAdded);
+        SubscribeLocalEvent<WantedListCartridgeComponent, CriminalHistoryRemovedEvent>(OnHistoryRemoved);
     }
 
     private void OnGeneralRecordCreated(AfterGeneralRecordCreatedEvent ev)
@@ -39,14 +50,14 @@ public sealed class CriminalRecordsSystem : SharedCriminalRecordsSystem
     /// Reason should only be passed if status is Wanted, nullability isn't checked.
     /// </summary>
     /// <returns>True if the status is changed, false if not</returns>
-    public bool TryChangeStatus(StationRecordKey key, SecurityStatus status, string? reason)
+    public bool TryChangeStatus(StationRecordKey key, SecurityStatus status, string? reason, string? initiatorName = null)
     {
         // don't do anything if its the same status
         if (!_records.TryGetRecord<CriminalRecord>(key, out var record)
             || status == record.Status)
             return false;
 
-        OverwriteStatus(key, record, status, reason);
+        OverwriteStatus(key, record, status, reason, initiatorName);
 
         return true;
     }
@@ -54,16 +65,24 @@ public sealed class CriminalRecordsSystem : SharedCriminalRecordsSystem
     /// <summary>
     /// Sets the status without checking previous status or reason nullability.
     /// </summary>
-    public void OverwriteStatus(StationRecordKey key, CriminalRecord record, SecurityStatus status, string? reason)
+    public void OverwriteStatus(StationRecordKey key, CriminalRecord record, SecurityStatus status, string? reason, string? initiatorName = null)
     {
         record.Status = status;
         record.Reason = reason;
+        record.InitiatorName = initiatorName;
 
         var name = _records.RecordName(key);
         if (name != string.Empty)
             UpdateCriminalIdentity(name, status);
 
         _records.Synchronize(key);
+
+        var args = new CriminalRecordChangedEvent(record);
+        var query = EntityQueryEnumerator<WantedListCartridgeComponent>();
+        while (query.MoveNext(out var readerUid, out _))
+        {
+            RaiseLocalEvent(readerUid, ref args);
+        }
     }
 
     /// <summary>
@@ -76,15 +95,23 @@ public sealed class CriminalRecordsSystem : SharedCriminalRecordsSystem
             return false;
 
         record.History.Add(entry);
+
+        var args = new CriminalHistoryAddedEvent(entry);
+        var query = EntityQueryEnumerator<WantedListCartridgeComponent>();
+        while (query.MoveNext(out var readerUid, out _))
+        {
+            RaiseLocalEvent(readerUid, ref args);
+        }
+
         return true;
     }
 
     /// <summary>
     /// Creates and tries to add a history entry using the current time.
     /// </summary>
-    public bool TryAddHistory(StationRecordKey key, string line)
+    public bool TryAddHistory(StationRecordKey key, string line, string? initiatorName = null)
     {
-        var entry = new CrimeHistory(_ticker.RoundDuration(), line);
+        var entry = new CrimeHistory(_ticker.RoundDuration(), line, initiatorName);
         return TryAddHistory(key, entry);
     }
 
@@ -100,7 +127,58 @@ public sealed class CriminalRecordsSystem : SharedCriminalRecordsSystem
         if (index >= record.History.Count)
             return false;
 
+        var history = record.History[(int)index];
         record.History.RemoveAt((int) index);
+
+        var args = new CriminalHistoryRemovedEvent(history);
+        var query = EntityQueryEnumerator<WantedListCartridgeComponent>();
+        while (query.MoveNext(out var readerUid, out _))
+        {
+            RaiseLocalEvent(readerUid, ref args);
+        }
+
         return true;
     }
+
+    private void OnRecordChanged(Entity<WantedListCartridgeComponent> ent, ref CriminalRecordChangedEvent args) =>
+        StateChanged(ent);
+
+    private void OnHistoryAdded(Entity<WantedListCartridgeComponent> ent, ref CriminalHistoryAddedEvent args) =>
+        StateChanged(ent);
+
+    private void OnHistoryRemoved(Entity<WantedListCartridgeComponent> ent, ref CriminalHistoryRemovedEvent args) =>
+        StateChanged(ent);
+
+    private void StateChanged(Entity<WantedListCartridgeComponent> ent)
+    {
+        if (Comp<CartridgeComponent>(ent).LoaderUid is not { } loaderUid)
+            return;
+
+        UpdateReaderUi(ent, loaderUid);
+    }
+
+    private void OnCartridgeUiReady(Entity<WantedListCartridgeComponent> ent, ref CartridgeUiReadyEvent args)
+    {
+        UpdateReaderUi(ent, args.Loader);
+    }
+
+    private void UpdateReaderUi(Entity<WantedListCartridgeComponent> ent, EntityUid loaderUid)
+    {
+        if (_station.GetOwningStation(ent) is not { } station)
+            return;
+
+        var records = _records.GetRecordsOfType<CriminalRecord>(station)
+            .Where(cr => cr.Item2.Status is not SecurityStatus.None || cr.Item2.History.Count > 0)
+            .Select(cr =>
+            {
+                var (i, r) = cr;
+                var key = new StationRecordKey(i, station);
+                // Hopefully it will work smoothly.....
+                _records.TryGetRecord(key, out GeneralStationRecord? generalRecord);
+                return new WantedRecord(generalRecord!, r.Status, r.Reason, r.InitiatorName, r.History);
+            });
+        var state = new WantedListUiState(records.ToList());
+
+        _cartridge.UpdateCartridgeUiState(loaderUid, state);
+    }
 }
index be34a80fe348505532d1ba7bcdeacb0a25fc07e4..e2d81e011cf5e286291b4fb06e7db772c2cd6315 100644 (file)
@@ -1,5 +1,6 @@
 using Content.Server.Objectives.Components;
 using Content.Server.Objectives.Components.Targets;
+using Content.Shared.CartridgeLoader;
 using Content.Shared.Mind;
 using Content.Shared.Objectives.Components;
 using Content.Shared.Objectives.Systems;
@@ -172,6 +173,11 @@ public sealed class StealConditionSystem : EntitySystem
         if (target.StealGroup != condition.StealGroup)
             return 0;
 
+        // check if cartridge is installed
+        if (TryComp<CartridgeComponent>(entity, out var cartridge) &&
+            cartridge.InstallationStatus is not InstallationStatus.Cartridge)
+            return 0;
+
         // check if needed target alive
         if (condition.CheckAlive)
         {
diff --git a/Content.Shared/CartridgeLoader/Cartridges/WantedListUiState.cs b/Content.Shared/CartridgeLoader/Cartridges/WantedListUiState.cs
new file mode 100644 (file)
index 0000000..9d55e0c
--- /dev/null
@@ -0,0 +1,11 @@
+using Content.Shared.CriminalRecords;
+using Content.Shared.CriminalRecords.Systems;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.CartridgeLoader.Cartridges;
+
+[Serializable, NetSerializable]
+public sealed class WantedListUiState(List<WantedRecord> records) : BoundUserInterfaceState
+{
+    public List<WantedRecord> Records = records;
+}
index 0fe23d4395419b89663c5e1d76e15fe089840881..5a023a9188c63bc0182efcbcbde2ad3e52b095e6 100644 (file)
@@ -23,6 +23,12 @@ public sealed record CriminalRecord
     [DataField]
     public string? Reason;
 
+    /// <summary>
+    /// The name of the person who changed the status.
+    /// </summary>
+    [DataField]
+    public string? InitiatorName;
+
     /// <summary>
     /// Criminal history of the person.
     /// This should have charges and time served added after someone is detained.
@@ -35,4 +41,4 @@ public sealed record CriminalRecord
 /// A line of criminal activity and the time it was added at.
 /// </summary>
 [Serializable, NetSerializable]
-public record struct CrimeHistory(TimeSpan AddTime, string Crime);
+public record struct CrimeHistory(TimeSpan AddTime, string Crime, string? InitiatorName);
index 96b33ab91bd308a80dfc1e7ef52e661eeafa9d1a..d665d32f1ed21db266e1813d33714a2d7e1a7d90 100644 (file)
@@ -2,6 +2,8 @@ using Content.Shared.IdentityManagement;
 using Content.Shared.IdentityManagement.Components;
 using Content.Shared.Security;
 using Content.Shared.Security.Components;
+using Content.Shared.StationRecords;
+using Robust.Shared.Serialization;
 
 namespace Content.Shared.CriminalRecords.Systems;
 
@@ -50,3 +52,22 @@ public abstract class SharedCriminalRecordsSystem : EntitySystem
             Dirty(characterUid, record);
     }
 }
+
+[Serializable, NetSerializable]
+public struct WantedRecord(GeneralStationRecord targetInfo, SecurityStatus status, string? reason, string? initiator, List<CrimeHistory> history)
+{
+    public GeneralStationRecord TargetInfo = targetInfo;
+    public SecurityStatus Status = status;
+    public string? Reason = reason;
+    public string? Initiator = initiator;
+    public List<CrimeHistory> History = history;
+};
+
+[ByRefEvent]
+public record struct CriminalRecordChangedEvent(CriminalRecord Record);
+
+[ByRefEvent]
+public record struct CriminalHistoryAddedEvent(CrimeHistory History);
+
+[ByRefEvent]
+public record struct CriminalHistoryRemovedEvent(CrimeHistory History);
index f5cda2f2a18b21b884e3d61fcddc5b704f0f81f1..2db27f5be09aec062848ec48e5206cc5188c8790 100644 (file)
@@ -19,3 +19,32 @@ log-probe-scan = Downloaded logs from {$device}!
 log-probe-label-time = Time
 log-probe-label-accessor = Accessed by
 log-probe-label-number = #
+
+# Wanted list cartridge
+wanted-list-program-name = Wanted list
+wanted-list-label-no-records = It's all right, cowboy
+wanted-list-search-placeholder = Search by name and status
+
+wanted-list-age-label = [color=darkgray]Age:[/color] [color=white]{$age}[/color]
+wanted-list-job-label = [color=darkgray]Job:[/color] [color=white]{$job}[/color]
+wanted-list-species-label = [color=darkgray]Species:[/color] [color=white]{$species}[/color]
+wanted-list-gender-label = [color=darkgray]Gender:[/color] [color=white]{$gender}[/color]
+
+wanted-list-reason-label = [color=darkgray]Reason:[/color] [color=white]{$reason}[/color]
+wanted-list-unknown-reason-label = unknown reason
+
+wanted-list-initiator-label = [color=darkgray]Initiator:[/color] [color=white]{$initiator}[/color]
+wanted-list-unknown-initiator-label = unknown initiator
+
+wanted-list-status-label = [color=darkgray]status:[/color] {$status ->
+        [suspected] [color=yellow]suspected[/color]
+        [wanted] [color=red]wanted[/color]
+        [detained] [color=#b18644]detained[/color]
+        [paroled] [color=green]paroled[/color]
+        [discharged] [color=green]discharged[/color]
+        *[other] none
+    }
+
+wanted-list-history-table-time-col = Time
+wanted-list-history-table-reason-col = Crime
+wanted-list-history-table-initiator-col = Initiator
index 6d6a97300c23976858fac970c40a4ef9600ae105..2a7c09912fae1a07719b5e8086346eda82266fc3 100644 (file)
@@ -39,7 +39,7 @@ criminal-records-console-released = {$name} has been released by {$officer}.
 criminal-records-console-not-wanted = {$officer} cleared the wanted status of {$name}.
 criminal-records-console-paroled = {$name} has been released on parole by {$officer}.
 criminal-records-console-not-parole = {$officer} cleared the parole status of {$name}.
-criminal-records-console-unknown-officer = <unknown officer>
+criminal-records-console-unknown-officer = <unknown>
 
 ## Filters
 
index 91b3c92b1c333cf7ebcd1862851e3c704ae849f6..689e2e7808eb9867159f6a4247cc4a05c3cb38a6 100644 (file)
@@ -40,6 +40,7 @@ steal-target-groups-clothing-eyes-hud-beer = beer goggles
 steal-target-groups-bible = bible
 steal-target-groups-clothing-neck-goldmedal = gold medal of crewmanship
 steal-target-groups-clothing-neck-clownmedal = clown medal
+steal-target-groups-wanted-list-cartridge = wanted list cartridge
 
 # Thief structures
 steal-target-groups-teg = teg generator part
index d3189630160086dfb17611ce5cf5224b97070ff6..31ebad6183703277b6e933e7bd759e14a31cadf8 100644 (file)
     - id: RubberStampHos
     - id: SecurityTechFabCircuitboard
     - id: WeaponDisabler
+    - id: WantedListCartridge
 
 # Hardsuit table, used for suit storage as well
 - type: entityTable
index f9581149e2146bf3854f025184715f718e913e31..91493f48cd1f50f07946fde6e34a67db21675851 100644 (file)
     - type: GuideHelp
       guides:
       - Forensics
+
+- type: entity
+  parent: BaseItem
+  id: WantedListCartridge
+  name: Wanted list cartridge
+  description: A program to get a list of wanted persons.
+  components:
+  - type: Sprite
+    sprite: Objects/Devices/cartridge.rsi
+    state: cart-sec
+  - type: Icon
+    sprite: Objects/Devices/cartridge.rsi
+    state: cart-sec
+  - type: UIFragment
+    ui: !type:WantedListUi
+  - type: Cartridge
+    programName: wanted-list-program-name
+    icon:
+      sprite: Objects/Misc/books.rsi
+      state: icon_magnifier
+  - type: WantedListCartridge
+  - type: StealTarget
+    stealGroup: WantedListCartridge
index 40f6f77e12d28df8867fc3aa706283d8996e0e9d..48e7a28debebae9ce3296cecba33cdadde1af977 100644 (file)
   - type: Speech
     speechVerb: Robotic
 
+- type: entity
+  id: BaseSecurityPDA
+  abstract: true
+  components:
+  - type: CartridgeLoader
+    uiKey: enum.PdaUiKey.Key
+    preinstalled:
+    - CrewManifestCartridge
+    - NotekeeperCartridge
+    - NewsReaderCartridge
+    - WantedListCartridge
+
 - type: entity
   parent: BasePDA
   id: BaseMedicalPDA
     state: pda-library
 
 - type: entity
-  parent: BasePDA
+  parent: [BaseSecurityPDA, BasePDA]
   id: LawyerPDA
   name: lawyer PDA
   description: For lawyers to poach dubious clients.
     state: pda-janitor
 
 - type: entity
-  parent: BasePDA
+  parent: [BaseSecurityPDA, BasePDA]
   id: CaptainPDA
   name: captain PDA
   description: Surprisingly no different from your PDA.
     state: pda-science
 
 - type: entity
-  parent: BasePDA
+  parent: [BaseSecurityPDA, BasePDA]
   id: HoSPDA
   name: head of security PDA
   description: Whosoever bears this PDA is the law.
     state: pda-hos
 
 - type: entity
-  parent: BasePDA
+  parent: [BaseSecurityPDA, BasePDA]
   id: WardenPDA
   name: warden PDA
   description: The OS appears to have been jailbroken.
     state: pda-warden
 
 - type: entity
-  parent: BasePDA
+  parent: [BaseSecurityPDA, BasePDA]
   id: SecurityPDA
   name: security PDA
   description: Red to hide the stains of passenger blood.
     state: pda-security
 
 - type: entity
-  parent: BasePDA
+  parent: [BaseSecurityPDA, BasePDA]
   id: CentcomPDA
   name: CentComm PDA
   description: Light green sign of walking bureaucracy.
       - NotekeeperCartridge
       - NewsReaderCartridge
       - LogProbeCartridge
+      - WantedListCartridge
 
 - type: entity
   parent: CentcomPDA
           - Cartridge
 
 - type: entity
-  parent: BasePDA
+  parent: [BaseSecurityPDA, BasePDA]
   id: ERTLeaderPDA
   name: ERT Leader PDA
   suffix: Leader
     state: pda-boxer
 
 - type: entity
-  parent: BasePDA
+  parent: [BaseSecurityPDA, BasePDA]
   id: DetectivePDA
   name: detective PDA
   description: Smells like rain... pouring down the rooftops...
     state: pda-seniorphysician
 
 - type: entity
-  parent: BasePDA
+  parent: [BaseSecurityPDA, BasePDA]
   id: SeniorOfficerPDA
   name: senior officer PDA
   description: Beaten, battered and broken, but just barely useable.
index 481ca0f93d84ef293a177b72608c68fc94110532..e72de0d94a93c95ea09b165b8276ad2f480f803f 100644 (file)
@@ -73,6 +73,7 @@
     ForensicScannerStealObjective: 1                    #sec
     FlippoEngravedLighterStealObjective: 0.5
     ClothingHeadHatWardenStealObjective: 1
+    WantedListCartridgeStealObjective: 1
     ClothingOuterHardsuitVoidParamedStealObjective: 1   #med
     MedicalTechFabCircuitboardStealObjective: 1
     ClothingHeadsetAltMedicalStealObjective: 1
index 48f56e2bfcd76d1bf775441c3741315e0436b30a..09619bf9868eea414ccd934b49d5cbd932a40fc3 100644 (file)
     sprite: Clothing/Neck/Medals/clownmedal.rsi
     state: icon
 
+- type: stealTargetGroup
+  id: WantedListCartridge
+  name: steal-target-groups-wanted-list-cartridge
+  sprite:
+    sprite: Objects/Devices/cartridge.rsi
+    state: cart-sec
+
 #Thief structures
 
 - type: stealTargetGroup
index f8e44d831e94e0777c8f51434f6fc41bb906e064..7a46d0f5e9b8e028850a756632f5efc6ae81ceda 100644 (file)
   - type: Objective
     difficulty: 1.2
 
+- type: entity
+  parent: BaseThiefStealObjective
+  id: WantedListCartridgeStealObjective
+  components:
+  - type: StealCondition
+    stealGroup: WantedListCartridge
+  - type: Objective
+    difficulty: 1
+
 - type: entity                                      #Medical subgroup
   parent: BaseThiefStealObjective
   id: ClothingOuterHardsuitVoidParamedStealObjective
diff --git a/Resources/Textures/Objects/Devices/cartridge.rsi/cart-sec.png b/Resources/Textures/Objects/Devices/cartridge.rsi/cart-sec.png
new file mode 100644 (file)
index 0000000..6a31970
Binary files /dev/null and b/Resources/Textures/Objects/Devices/cartridge.rsi/cart-sec.png differ
index f3b02a2b2eab4c84b3bce989a43f63dcac7d5d71..d5fad560062193d545e2ef74d512a91d26d5b1b1 100644 (file)
@@ -1,7 +1,7 @@
 {
   "version": 1,
   "license": "CC-BY-SA-3.0",
-  "copyright": "Taken from vgstation at https://github.com/vgstation-coders/vgstation13/commit/1cdfb0230cc96d0ba751fa002d04f8aa2f25ad7d and tgstation at tgstation at https://github.com/tgstation/tgstation/commit/0c15d9dbcf0f2beb230eba5d9d889ef2d1945bb8, cart-log made by Skarletto (github)",
+  "copyright": "Taken from vgstation at https://github.com/vgstation-coders/vgstation13/commit/1cdfb0230cc96d0ba751fa002d04f8aa2f25ad7d and tgstation at tgstation at https://github.com/tgstation/tgstation/commit/0c15d9dbcf0f2beb230eba5d9d889ef2d1945bb8, cart-log made by Skarletto (github), cart-sec made by dieselmohawk (discord)",
   "size": {
     "x": 32,
     "y": 32
@@ -79,6 +79,9 @@
     {
       "name": "cart-y"
     },
+    {
+      "name": "cart-sec"
+    },
     {
       "name": "insert_overlay"
     }