Station records (#8720)

Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
This commit is contained in:
Flipp Syder
2022-08-08 22:10:01 -07:00
committed by GitHub
parent 75dfbdb57f
commit 3d36a6e1f6
35 changed files with 1888 additions and 9 deletions

View File

@@ -1,6 +1,7 @@
using Content.Client.Access.Components; using Content.Client.Access.Components;
using Content.Shared.Access.Systems; using Content.Shared.Access.Systems;
using Content.Shared.Containers.ItemSlots; using Content.Shared.Containers.ItemSlots;
using Content.Shared.CrewManifest;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using static Content.Shared.Access.Components.SharedIdCardConsoleComponent; using static Content.Shared.Access.Components.SharedIdCardConsoleComponent;
@@ -36,6 +37,7 @@ namespace Content.Client.Access.UI
_window = new IdCardConsoleWindow(this, _prototypeManager, accessLevels) {Title = _entityManager.GetComponent<MetaDataComponent>(Owner.Owner).EntityName}; _window = new IdCardConsoleWindow(this, _prototypeManager, accessLevels) {Title = _entityManager.GetComponent<MetaDataComponent>(Owner.Owner).EntityName};
_window.CrewManifestButton.OnPressed += _ => SendMessage(new CrewManifestOpenUiMessage());
_window.PrivilegedIdButton.OnPressed += _ => SendMessage(new ItemSlotButtonPressedEvent(PrivilegedIdCardSlotId)); _window.PrivilegedIdButton.OnPressed += _ => SendMessage(new ItemSlotButtonPressedEvent(PrivilegedIdCardSlotId));
_window.TargetIdButton.OnPressed += _ => SendMessage(new ItemSlotButtonPressedEvent(TargetIdCardSlotId)); _window.TargetIdButton.OnPressed += _ => SendMessage(new ItemSlotButtonPressedEvent(TargetIdCardSlotId));

View File

@@ -1,14 +1,19 @@
<DefaultWindow xmlns="https://spacestation14.io" <DefaultWindow xmlns="https://spacestation14.io"
MinSize="650 290"> MinSize="650 290">
<BoxContainer Orientation="Vertical"> <BoxContainer Orientation="Vertical">
<GridContainer Columns="3"> <GridContainer Columns="2">
<Label Text="{Loc 'id-card-console-window-privileged-id'}" /> <GridContainer Columns="3" HorizontalExpand="True">
<Button Name="PrivilegedIdButton" Access="Public"/> <Label Text="{Loc 'id-card-console-window-privileged-id'}" />
<Label Name="PrivilegedIdLabel" /> <Button Name="PrivilegedIdButton" Access="Public"/>
<Label Name="PrivilegedIdLabel" />
<Label Text="{Loc 'id-card-console-window-target-id'}" /> <Label Text="{Loc 'id-card-console-window-target-id'}" />
<Button Name="TargetIdButton" Access="Public"/> <Button Name="TargetIdButton" Access="Public"/>
<Label Name="TargetIdLabel" /> <Label Name="TargetIdLabel" />
</GridContainer>
<BoxContainer Orientation="Vertical">
<Button Name="CrewManifestButton" Access="Public" Text="{Loc 'crew-manifest-button-label'}" />
</BoxContainer>
</GridContainer> </GridContainer>
<Control MinSize="0 8" /> <Control MinSize="0 8" />
<GridContainer Columns="3" HSeparationOverride="4"> <GridContainer Columns="3" HSeparationOverride="4">
@@ -21,6 +26,10 @@
<Button Name="JobTitleSaveButton" Text="{Loc 'id-card-console-window-save-button'}" Disabled="True" /> <Button Name="JobTitleSaveButton" Text="{Loc 'id-card-console-window-save-button'}" Disabled="True" />
</GridContainer> </GridContainer>
<Control MinSize="0 8" /> <Control MinSize="0 8" />
<GridContainer Columns="2">
<Label Text="{Loc 'id-card-console-window-job-selection-label'}" />
<OptionButton Name="JobPresetOptionButton" />
</GridContainer>
<GridContainer Name="AccessLevelGrid" Columns="5" HorizontalAlignment="Center"> <GridContainer Name="AccessLevelGrid" Columns="5" HorizontalAlignment="Center">
<!-- Access level buttons are added here by the C# code --> <!-- Access level buttons are added here by the C# code -->

View File

@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using Content.Shared.Access; using Content.Shared.Access;
using Content.Shared.Access.Systems; using Content.Shared.Access.Systems;
using Content.Shared.Roles;
using Robust.Client.AutoGenerated; using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.CustomControls;
@@ -16,9 +17,12 @@ namespace Content.Client.Access.UI
[GenerateTypedNameReferences] [GenerateTypedNameReferences]
public sealed partial class IdCardConsoleWindow : DefaultWindow public sealed partial class IdCardConsoleWindow : DefaultWindow
{ {
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private readonly IdCardConsoleBoundUserInterface _owner; private readonly IdCardConsoleBoundUserInterface _owner;
private readonly Dictionary<string, Button> _accessButtons = new(); private readonly Dictionary<string, Button> _accessButtons = new();
private readonly List<string> _jobPrototypeIds = new();
private string? _lastFullName; private string? _lastFullName;
private string? _lastJobTitle; private string? _lastJobTitle;
@@ -26,6 +30,7 @@ namespace Content.Client.Access.UI
public IdCardConsoleWindow(IdCardConsoleBoundUserInterface owner, IPrototypeManager prototypeManager, List<string> accessLevels) public IdCardConsoleWindow(IdCardConsoleBoundUserInterface owner, IPrototypeManager prototypeManager, List<string> accessLevels)
{ {
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_owner = owner; _owner = owner;
@@ -43,6 +48,21 @@ namespace Content.Client.Access.UI
}; };
JobTitleSaveButton.OnPressed += _ => SubmitData(); JobTitleSaveButton.OnPressed += _ => SubmitData();
var jobs = _prototypeManager.EnumeratePrototypes<JobPrototype>();
foreach (var job in jobs)
{
if (!job.SetPreference)
{
continue;
}
_jobPrototypeIds.Add(job.ID);
JobPresetOptionButton.AddItem(Loc.GetString(job.Name), _jobPrototypeIds.Count - 1);
}
JobPresetOptionButton.OnItemSelected += SelectJobPreset;
foreach (var access in accessLevels) foreach (var access in accessLevels)
{ {
if (!prototypeManager.TryIndex<AccessLevelPrototype>(access, out var accessLevel)) if (!prototypeManager.TryIndex<AccessLevelPrototype>(access, out var accessLevel))
@@ -62,6 +82,56 @@ namespace Content.Client.Access.UI
} }
} }
private void ClearAllAccess()
{
foreach (var button in _accessButtons.Values)
{
if (button.Pressed)
{
button.Pressed = false;
}
}
}
private void SelectJobPreset(OptionButton.ItemSelectedEventArgs args)
{
if (!_prototypeManager.TryIndex(_jobPrototypeIds[args.Id], out JobPrototype? job))
{
return;
}
JobTitleLineEdit.Text = Loc.GetString(job.Name);
ClearAllAccess();
// this is a sussy way to do this
foreach (var access in job.Access)
{
if (_accessButtons.TryGetValue(access, out var button))
{
button.Pressed = true;
}
}
foreach (var group in job.AccessGroups)
{
if (!_prototypeManager.TryIndex(group, out AccessGroupPrototype? groupPrototype))
{
continue;
}
foreach (var access in groupPrototype.Tags)
{
if (_accessButtons.TryGetValue(access, out var button))
{
button.Pressed = true;
}
}
}
SubmitData();
}
public void UpdateState(IdCardConsoleBoundUserInterfaceState state) public void UpdateState(IdCardConsoleBoundUserInterfaceState state)
{ {
PrivilegedIdButton.Text = state.IsPrivilegedIdPresent PrivilegedIdButton.Text = state.IsPrivilegedIdPresent
@@ -100,6 +170,8 @@ namespace Content.Client.Access.UI
JobTitleSaveButton.Disabled = !interfaceEnabled || !jobTitleDirty; JobTitleSaveButton.Disabled = !interfaceEnabled || !jobTitleDirty;
JobPresetOptionButton.Disabled = !interfaceEnabled;
foreach (var (accessName, button) in _accessButtons) foreach (var (accessName, button) in _accessButtons)
{ {
button.Disabled = !interfaceEnabled; button.Disabled = !interfaceEnabled;

View File

@@ -0,0 +1,51 @@
using Content.Client.Eui;
using Content.Client.GameTicking.Managers;
using Content.Shared.CrewManifest;
using Content.Shared.Eui;
using JetBrains.Annotations;
namespace Content.Client.CrewManifest;
[UsedImplicitly]
public sealed class CrewManifestEui : BaseEui
{
private readonly ClientGameTicker _gameTicker;
private readonly CrewManifestUi _window;
public CrewManifestEui()
{
_gameTicker = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ClientGameTicker>();
_window = new();
_window.OnClose += () =>
{
SendMessage(new CrewManifestEuiClosed());
};
}
public override void Opened()
{
base.Opened();
_window.OpenCentered();
}
public override void Closed()
{
base.Closed();
_window.Close();
}
public override void HandleState(EuiStateBase state)
{
base.HandleState(state);
if (state is not CrewManifestEuiState cast)
{
return;
}
_window.Populate(cast.StationName, cast.Entries);
}
}

View File

@@ -0,0 +1,82 @@
using Content.Client.GameTicking.Managers;
using Content.Shared.CrewManifest;
using Content.Shared.Roles;
using Robust.Shared.Prototypes;
namespace Content.Client.CrewManifest;
public sealed class CrewManifestSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private Dictionary<string, Dictionary<string, int>> _jobDepartmentLookup = new();
private HashSet<string> _departments = new();
public IReadOnlySet<string> Departments => _departments;
public override void Initialize()
{
base.Initialize();
BuildDepartmentLookup();
_prototypeManager.PrototypesReloaded += OnPrototypesReload;
}
public override void Shutdown()
{
_prototypeManager.PrototypesReloaded -= OnPrototypesReload;
}
/// <summary>
/// Requests a crew manifest from the server.
/// </summary>
/// <param name="uid">EntityUid of the entity we're requesting the crew manifest from.</param>
public void RequestCrewManifest(EntityUid uid)
{
RaiseNetworkEvent(new RequestCrewManifestMessage(uid));
}
private void OnPrototypesReload(PrototypesReloadedEventArgs _)
{
_jobDepartmentLookup.Clear();
_departments.Clear();
BuildDepartmentLookup();
}
private void BuildDepartmentLookup()
{
foreach (var department in _prototypeManager.EnumeratePrototypes<DepartmentPrototype>())
{
_departments.Add(department.ID);
for (var i = 1; i <= department.Roles.Count; i++)
{
if (!_jobDepartmentLookup.TryGetValue(department.Roles[i - 1], out var departments))
{
departments = new();
_jobDepartmentLookup.Add(department.Roles[i - 1], departments);
}
departments.Add(department.ID, i);
}
}
}
public int GetDepartmentOrder(string department, string jobPrototype)
{
if (!Departments.Contains(department))
{
return -1;
}
if (!_jobDepartmentLookup.TryGetValue(jobPrototype, out var departments))
{
return -1;
}
return departments.TryGetValue(department, out var order)
? order
: -1;
}
}

View File

@@ -0,0 +1,21 @@
<DefaultWindow xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.HUD.UI"
Title="{Loc 'crew-manifest-window-title'}"
MinSize="450 750">
<BoxContainer Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True">
<ui:StripeBack Name="StationNameContainer">
<PanelContainer>
<Label Name="StationName" Align="Center" />
</PanelContainer>
</ui:StripeBack>
<BoxContainer HorizontalExpand="True" VerticalExpand="True">
<ScrollContainer HorizontalExpand="True" VerticalExpand="True">
<!-- this MIGHT have race conditions -->
<BoxContainer Name="CrewManifestListing" Orientation="Vertical" HorizontalExpand="True">
<Label Text="{Loc 'crew-manifest-no-valid-station'}" HorizontalExpand="True" />
</BoxContainer>
<!-- Crew manifest goes here. -->
</ScrollContainer>
</BoxContainer>
</BoxContainer>
</DefaultWindow>

View File

@@ -0,0 +1,186 @@
using System.Linq;
using Content.Shared.CCVar;
using Content.Shared.CrewManifest;
using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Client.Utility;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.CrewManifest;
[GenerateTypedNameReferences]
public sealed partial class CrewManifestUi : DefaultWindow
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
private readonly CrewManifestSystem _crewManifestSystem;
private EntityUid? _station;
public CrewManifestUi()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_crewManifestSystem = _entitySystemManager.GetEntitySystem<CrewManifestSystem>();
StationName.AddStyleClass("LabelBig");
}
public void Populate(string name, CrewManifestEntries? entries)
{
CrewManifestListing.DisposeAllChildren();
CrewManifestListing.RemoveAllChildren();
StationNameContainer.Visible = entries != null;
StationName.Text = name;
if (entries == null) return;
var entryList = SortEntries(entries);
foreach (var item in entryList)
{
CrewManifestListing.AddChild(new CrewManifestSection(item.section, item.entries, _resourceCache, _crewManifestSystem));
}
}
private List<(string section, List<CrewManifestEntry> entries)> SortEntries(CrewManifestEntries entries)
{
var entryDict = new Dictionary<string, List<CrewManifestEntry>>();
foreach (var entry in entries.Entries)
{
foreach (var department in _prototypeManager.EnumeratePrototypes<DepartmentPrototype>())
{
// this is a little expensive, and could be better
if (department.Roles.Contains(entry.JobPrototype))
{
entryDict.GetOrNew(department.ID).Add(entry);
}
}
}
var entryList = new List<(string section, List<CrewManifestEntry> entries)>();
foreach (var (section, listing) in entryDict)
{
entryList.Add((section, listing));
}
var sortOrder = _configManager.GetCVar(CCVars.CrewManifestOrdering).Split(",").ToList();
entryList.Sort((a, b) =>
{
var ai = sortOrder.IndexOf(a.section);
var bi = sortOrder.IndexOf(b.section);
// this is up here so -1 == -1 occurs first
if (ai == bi)
return 0;
if (ai == -1)
return -1;
if (bi == -1)
return 1;
return ai.CompareTo(bi);
});
return entryList;
}
private sealed class CrewManifestSection : BoxContainer
{
public CrewManifestSection(string sectionTitle, List<CrewManifestEntry> entries, IResourceCache cache, CrewManifestSystem crewManifestSystem)
{
Orientation = LayoutOrientation.Vertical;
HorizontalExpand = true;
AddChild(new Label()
{
StyleClasses = { "LabelBig" },
Text = Loc.GetString(sectionTitle)
});
entries.Sort((a, b) =>
{
var posA = crewManifestSystem.GetDepartmentOrder(sectionTitle, a.JobPrototype);
var posB = crewManifestSystem.GetDepartmentOrder(sectionTitle, b.JobPrototype);
return posA.CompareTo(posB);
});
var gridContainer = new GridContainer()
{
HorizontalExpand = true,
Columns = 2
};
AddChild(gridContainer);
var path = new ResourcePath("/Textures/Interface/Misc/job_icons.rsi");
cache.TryGetResource(path, out RSIResource? rsi);
foreach (var entry in entries)
{
var name = new Label()
{
HorizontalExpand = true,
Text = entry.Name
};
var titleContainer = new BoxContainer()
{
Orientation = LayoutOrientation.Horizontal,
HorizontalExpand = true
};
var title = new Label()
{
Text = Loc.GetString(entry.JobTitle)
};
if (rsi != null)
{
var icon = new TextureRect()
{
TextureScale = (2, 2),
Stretch = TextureRect.StretchMode.KeepCentered
};
if (rsi.RSI.TryGetState(entry.JobIcon, out _))
{
var specifier = new SpriteSpecifier.Rsi(path, entry.JobIcon);
icon.Texture = specifier.Frame0();
}
else if (rsi.RSI.TryGetState("Unknown", out _))
{
var specifier = new SpriteSpecifier.Rsi(path, "Unknown");
icon.Texture = specifier.Frame0();
}
titleContainer.AddChild(icon);
titleContainer.AddChild(title);
}
else
{
titleContainer.AddChild(title);
}
gridContainer.AddChild(name);
gridContainer.AddChild(titleContainer);
}
}
}
}

View File

@@ -1,12 +1,17 @@
using System.Linq; using System.Linq;
using Content.Client.CrewManifest;
using Content.Client.Eui;
using Content.Client.GameTicking.Managers; using Content.Client.GameTicking.Managers;
using Content.Client.HUD.UI; using Content.Client.HUD.UI;
using Content.Shared.CCVar;
using Content.Shared.CrewManifest;
using Content.Shared.Roles; using Content.Shared.Roles;
using Robust.Client.Console; using Robust.Client.Console;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.CustomControls;
using Robust.Client.Utility; using Robust.Client.Utility;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Utility; using Robust.Shared.Utility;
using static Robust.Client.UserInterface.Controls.BoxContainer; using static Robust.Client.UserInterface.Controls.BoxContainer;
@@ -17,6 +22,7 @@ namespace Content.Client.LateJoin
{ {
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IClientConsoleHost _consoleHost = default!; [Dependency] private readonly IClientConsoleHost _consoleHost = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
public event Action<(EntityUid, string)> SelectedId; public event Action<(EntityUid, string)> SelectedId;
@@ -109,6 +115,21 @@ namespace Content.Client.LateJoin
} }
} }
}); });
if (_configManager.GetCVar<bool>(CCVars.CrewManifestWithoutEntity))
{
var crewManifestButton = new Button()
{
Text = Loc.GetString("crew-manifest-button-label")
};
crewManifestButton.OnPressed += args =>
{
EntitySystem.Get<CrewManifestSystem>().RequestCrewManifest(id);
};
_base.AddChild(crewManifestButton);
}
var jobListScroll = new ScrollContainer() var jobListScroll = new ScrollContainer()
{ {
VerticalExpand = true, VerticalExpand = true,

View File

@@ -1,18 +1,24 @@
using Content.Client.Message; using Content.Client.Message;
using Content.Shared.CCVar;
using Content.Shared.Containers.ItemSlots; using Content.Shared.Containers.ItemSlots;
using Content.Shared.CrewManifest;
using Content.Shared.PDA; using Content.Shared.PDA;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Shared.Configuration;
namespace Content.Client.PDA namespace Content.Client.PDA
{ {
[UsedImplicitly] [UsedImplicitly]
public sealed class PDABoundUserInterface : BoundUserInterface public sealed class PDABoundUserInterface : BoundUserInterface
{ {
[Dependency] private readonly IConfigurationManager _configManager = default!;
private PDAMenu? _menu; private PDAMenu? _menu;
public PDABoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) public PDABoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
{ {
IoCManager.InjectDependencies(this);
} }
protected override void Open() protected override void Open()
@@ -27,6 +33,15 @@ namespace Content.Client.PDA
SendMessage(new PDAToggleFlashlightMessage()); SendMessage(new PDAToggleFlashlightMessage());
}; };
if (_configManager.GetCVar(CCVars.CrewManifestUnsecure))
{
_menu.CrewManifestButton.Visible = true;
_menu.CrewManifestButton.OnPressed += _ =>
{
SendMessage(new CrewManifestOpenUiMessage());
};
}
_menu.EjectIdButton.OnPressed += _ => _menu.EjectIdButton.OnPressed += _ =>
{ {
SendMessage(new ItemSlotButtonPressedEvent(PDAComponent.PDAIdSlotId)); SendMessage(new ItemSlotButtonPressedEvent(PDAComponent.PDAIdSlotId));

View File

@@ -37,6 +37,10 @@
Access="Public" Access="Public"
Text="{Loc 'comp-pda-ui-toggle-flashlight-button'}" Text="{Loc 'comp-pda-ui-toggle-flashlight-button'}"
ToggleMode="True" /> ToggleMode="True" />
<Button Name="CrewManifestButton"
Access="Public"
Text="{Loc 'crew-manifest-button-label'}"
Visible="False" />
<Button Name="ActivateUplinkButton" <Button Name="ActivateUplinkButton"
Access="Public" Access="Public"
Text="{Loc 'pda-bound-user-interface-uplink-tab-title'}" /> Text="{Loc 'pda-bound-user-interface-uplink-tab-title'}" />

View File

@@ -0,0 +1,47 @@
using Content.Shared.StationRecords;
using Robust.Client.GameObjects;
namespace Content.Client.StationRecords;
public sealed class GeneralStationRecordConsoleBoundUserInterface : BoundUserInterface
{
private GeneralStationRecordConsoleWindow? _window = default!;
public GeneralStationRecordConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
{}
protected override void Open()
{
base.Open();
_window = new();
_window.OnKeySelected += OnKeySelected;
_window.OnClose += Close;
_window.OpenCentered();
}
private void OnKeySelected(StationRecordKey? key)
{
SendMessage(new SelectGeneralStationRecord(key));
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (state is not GeneralStationRecordConsoleState cast)
{
return;
}
_window?.UpdateState(cast);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_window?.Close();
}
}

View File

@@ -0,0 +1,17 @@
<DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc 'general-station-record-console-window-title'}"
MinSize="750 500">
<BoxContainer>
<!-- Record listing -->
<BoxContainer Orientation="Vertical" Margin="5 5 5 5" MinWidth="250" VerticalExpand="True">
<Label Name="RecordListingStatus" Visible="False" />
<ScrollContainer VerticalExpand="True">
<ItemList Name="RecordListing" />
</ScrollContainer>
</BoxContainer>
<BoxContainer Orientation="Vertical" Margin="5 5 5 5">
<Label Name="RecordContainerStatus" Visible="False" Text="{Loc 'general-station-record-console-select-record-info'}"/>
<BoxContainer Name="RecordContainer" Orientation="Vertical" />
</BoxContainer>
</BoxContainer>
</DefaultWindow>

View File

@@ -0,0 +1,126 @@
using System.Linq;
using Content.Shared.StationRecords;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.StationRecords;
[GenerateTypedNameReferences]
public sealed partial class GeneralStationRecordConsoleWindow : DefaultWindow
{
public Action<StationRecordKey?>? OnKeySelected;
private bool _isPopulating;
public GeneralStationRecordConsoleWindow()
{
RobustXamlLoader.Load(this);
RecordListing.OnItemSelected += args =>
{
if (_isPopulating || RecordListing[args.ItemIndex].Metadata is not StationRecordKey cast)
{
return;
}
OnKeySelected?.Invoke(cast);
};
RecordListing.OnItemDeselected += _ =>
{
if (!_isPopulating)
OnKeySelected?.Invoke(null);
};
}
public void UpdateState(GeneralStationRecordConsoleState state)
{
if (state.RecordListing == null)
{
RecordListingStatus.Visible = true;
RecordListing.Visible = false;
RecordListingStatus.Text = Loc.GetString("general-station-record-console-empty-state");
return;
}
RecordListingStatus.Visible = false;
RecordListing.Visible = true;
PopulateRecordListing(state.RecordListing!, state.SelectedKey);
RecordContainerStatus.Visible = state.Record == null;
if (state.Record != null)
{
RecordContainerStatus.Visible = state.SelectedKey == null;
RecordContainerStatus.Text = state.SelectedKey == null
? Loc.GetString("general-station-record-console-no-record-found")
: Loc.GetString("general-station-record-console-select-record-info");
PopulateRecordContainer(state.Record);
}
else
{
RecordContainer.DisposeAllChildren();
RecordContainer.RemoveAllChildren();
}
}
private void PopulateRecordListing(Dictionary<StationRecordKey, string> listing, StationRecordKey? selected)
{
RecordListing.Clear();
RecordListing.ClearSelected();
_isPopulating = true;
foreach (var (key, name) in listing)
{
var item = RecordListing.AddItem(name);
item.Metadata = key;
if (selected != null && key.ID == selected.Value.ID)
{
item.Selected = true;
}
}
_isPopulating = false;
RecordListing.SortItemsByText();
}
private void PopulateRecordContainer(GeneralStationRecord record)
{
RecordContainer.DisposeAllChildren();
RecordContainer.RemoveAllChildren();
// sure
var recordControls = new Control[]
{
new Label()
{
Text = record.Name,
StyleClasses = { "LabelBig" }
},
new Label()
{
Text = Loc.GetString("general-station-record-console-record-age", ("age", record.Age.ToString()))
},
new Label()
{
Text = Loc.GetString("general-station-record-console-record-title", ("job", Loc.GetString(record.JobTitle)))
},
new Label()
{
Text = Loc.GetString("general-station-record-console-record-species", ("species", record.Species))
},
new Label()
{
Text = Loc.GetString("general-station-record-console-record-gender", ("gender", record.Gender.ToString()))
}
};
foreach (var control in recordControls)
{
RecordContainer.AddChild(control);
}
}
}

View File

@@ -1,10 +1,12 @@
using System.Linq; using System.Linq;
using Content.Server.Access.Systems; using Content.Server.Access.Systems;
using Content.Server.Administration.Logs; using Content.Server.Station.Systems;
using Content.Server.StationRecords;
using Content.Server.UserInterface; using Content.Server.UserInterface;
using Content.Shared.Access.Components; using Content.Shared.Access.Components;
using Content.Shared.Access.Systems; using Content.Shared.Access.Systems;
using Content.Shared.Containers.ItemSlots; using Content.Shared.StationRecords;
using Content.Server.Administration.Logs;
using Content.Shared.Database; using Content.Shared.Database;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
@@ -91,6 +93,25 @@ namespace Content.Server.Access.Components
_adminLogger.Add(LogType.Action, LogImpact.Medium, _adminLogger.Add(LogType.Action, LogImpact.Medium,
$"{_entities.ToPrettyString(player):player} has modified {_entities.ToPrettyString(targetIdEntity):entity} with the following accesses: [{string.Join(", ", newAccessList)}]"); $"{_entities.ToPrettyString(player):player} has modified {_entities.ToPrettyString(targetIdEntity):entity} with the following accesses: [{string.Join(", ", newAccessList)}]");
UpdateStationRecord(targetIdEntity, newFullName, newJobTitle);
}
private void UpdateStationRecord(EntityUid idCard, string newFullName, string newJobTitle)
{
var station = EntitySystem.Get<StationSystem>().GetOwningStation(Owner);
var recordSystem = EntitySystem.Get<StationRecordsSystem>();
if (station == null
|| !_entities.TryGetComponent(idCard, out StationRecordKeyStorageComponent? keyStorage)
|| keyStorage.Key == null
|| !recordSystem.TryGetRecord(station.Value, keyStorage.Key.Value, out GeneralStationRecord? record))
{
return;
}
record.Name = newFullName;
record.JobTitle = newJobTitle;
recordSystem.Synchronize(station.Value);
} }
public void UpdateUserInterface() public void UpdateUserInterface()

View File

@@ -0,0 +1,47 @@
using Content.Server.EUI;
using Content.Shared.CrewManifest;
using Content.Shared.Eui;
namespace Content.Server.CrewManifest;
public sealed class CrewManifestEui : BaseEui
{
private readonly CrewManifestSystem _crewManifest;
/// <summary>
/// Station this EUI instance is currently tracking.
/// </summary>
private readonly EntityUid _station;
/// <summary>
/// Current owner of this UI, if it has one. This is
/// to ensure that if a BUI is closed, the EUIs related
/// to the BUI are closed as well.
/// </summary>
public readonly EntityUid? Owner;
public CrewManifestEui(EntityUid station, EntityUid? owner, CrewManifestSystem crewManifestSystem)
{
_station = station;
Owner = owner;
_crewManifest = crewManifestSystem;
}
public override CrewManifestEuiState GetNewState()
{
var (name, entries) = _crewManifest.GetCrewManifest(_station);
return new(name, entries);
}
public override void HandleMessage(EuiMessageBase msg)
{
base.HandleMessage(msg);
switch (msg)
{
case CrewManifestEuiClosed:
_crewManifest.CloseEui(_station, Player, Owner);
break;
}
}
}

View File

@@ -0,0 +1,263 @@
using Content.Server.Administration;
using Content.Server.EUI;
using Content.Server.GameTicking;
using Content.Server.Station.Systems;
using Content.Server.StationRecords;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.CrewManifest;
using Content.Shared.GameTicking;
using Content.Shared.Roles;
using Content.Shared.StationRecords;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Prototypes;
namespace Content.Server.CrewManifest;
public sealed class CrewManifestSystem : EntitySystem
{
[Dependency] private readonly StationSystem _stationSystem = default!;
[Dependency] private readonly StationRecordsSystem _recordsSystem = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly EuiManager _euiManager = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
/// <summary>
/// Cached crew manifest entries. The alternative is to outright
/// rebuild the crew manifest every time the state is requested:
/// this is inefficient.
/// </summary>
private readonly Dictionary<EntityUid, CrewManifestEntries> _cachedEntries = new();
private readonly Dictionary<EntityUid, Dictionary<IPlayerSession, CrewManifestEui>> _openEuis = new();
public override void Initialize()
{
SubscribeLocalEvent<AfterGeneralRecordCreatedEvent>(AfterGeneralRecordCreated);
SubscribeLocalEvent<RecordModifiedEvent>(OnRecordModified);
SubscribeLocalEvent<CrewManifestViewerComponent, BoundUIClosedEvent>(OnBoundUiClose);
SubscribeLocalEvent<CrewManifestViewerComponent, CrewManifestOpenUiMessage>(OpenEuiFromBui);
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestart);
SubscribeNetworkEvent<RequestCrewManifestMessage>(OnRequestCrewManifest);
}
private void OnRoundRestart(RoundRestartCleanupEvent ev)
{
foreach (var (_, euis) in _openEuis)
{
foreach (var (_, eui) in euis)
{
eui.Close();
}
}
_openEuis.Clear();
_cachedEntries.Clear();
}
private void OnRequestCrewManifest(RequestCrewManifestMessage message, EntitySessionEventArgs args)
{
if (args.SenderSession is not IPlayerSession sessionCast
|| !_configManager.GetCVar(CCVars.CrewManifestWithoutEntity))
{
return;
}
OpenEui(message.Id, sessionCast);
}
// Not a big fan of this one. Rebuilds the crew manifest every time
// somebody spawns in, meaning that at round start, it rebuilds the crew manifest
// wrt the amount of players readied up.
private void AfterGeneralRecordCreated(AfterGeneralRecordCreatedEvent ev)
{
BuildCrewManifest(ev.Key.OriginStation);
UpdateEuis(ev.Key.OriginStation);
}
private void OnRecordModified(RecordModifiedEvent ev)
{
BuildCrewManifest(ev.Key.OriginStation);
UpdateEuis(ev.Key.OriginStation);
}
private void OnBoundUiClose(EntityUid uid, CrewManifestViewerComponent component, BoundUIClosedEvent ev)
{
var owningStation = _stationSystem.GetOwningStation(uid);
if (owningStation == null || ev.Session is not IPlayerSession sessionCast)
{
return;
}
CloseEui(owningStation.Value, sessionCast, uid);
}
/// <summary>
/// Gets the crew manifest for a given station, along with the name of the station.
/// </summary>
/// <param name="station">Entity uid of the station.</param>
/// <returns>The name and crew manifest entries (unordered) of the station.</returns>
public (string name, CrewManifestEntries? entries) GetCrewManifest(EntityUid station)
{
var valid = _cachedEntries.TryGetValue(station, out var manifest);
return (valid ? MetaData(station).EntityName : string.Empty, valid ? manifest : null);
}
private void UpdateEuis(EntityUid station)
{
if (_openEuis.TryGetValue(station, out var euis))
{
foreach (var eui in euis.Values)
{
eui.StateDirty();
}
}
}
private void OpenEuiFromBui(EntityUid uid, CrewManifestViewerComponent component, CrewManifestOpenUiMessage msg)
{
var owningStation = _stationSystem.GetOwningStation(uid);
if (owningStation == null || msg.Session is not IPlayerSession sessionCast)
{
return;
}
if (!_configManager.GetCVar(CCVars.CrewManifestUnsecure) && component.Unsecure)
{
return;
}
OpenEui(owningStation.Value, sessionCast, uid);
}
/// <summary>
/// Opens a crew manifest EUI for a given player.
/// </summary>
/// <param name="station">Station that we're displaying the crew manifest for.</param>
/// <param name="session">The player's session.</param>
/// <param name="owner">If this EUI should be 'owned' by an entity.</param>
public void OpenEui(EntityUid station, IPlayerSession session, EntityUid? owner = null)
{
if (!HasComp<StationRecordsComponent>(station))
{
return;
}
if (!_openEuis.TryGetValue(station, out var euis))
{
euis = new();
_openEuis.Add(station, euis);
}
if (euis.ContainsKey(session))
{
return;
}
var eui = new CrewManifestEui(station, owner, this);
euis.Add(session, eui);
_euiManager.OpenEui(eui, session);
eui.StateDirty();
}
/// <summary>
/// Closes an EUI for a given player.
/// </summary>
/// <param name="station">Station that we're displaying the crew manifest for.</param>
/// <param name="session">The player's session.</param>
/// <param name="owner">The owner of this EUI, if there was one.</param>
public void CloseEui(EntityUid station, IPlayerSession session, EntityUid? owner = null)
{
if (!HasComp<StationRecordsComponent>(station))
{
return;
}
if (!_openEuis.TryGetValue(station, out var euis)
|| !euis.TryGetValue(session, out var eui))
{
return;
}
if (eui.Owner == owner)
{
eui.Close();
euis.Remove(session);
}
if (euis.Count == 0)
{
_openEuis.Remove(station);
}
}
/// <summary>
/// Builds the crew manifest for a station. Stores it in the cache afterwards.
/// </summary>
/// <param name="station"></param>
private void BuildCrewManifest(EntityUid station)
{
var iter = _recordsSystem.GetRecordsOfType<GeneralStationRecord>(station);
var entries = new CrewManifestEntries();
foreach (var recordObject in iter)
{
var record = recordObject.Item2;
var entry = new CrewManifestEntry(record.Name, record.JobTitle, record.JobIcon, record.JobPrototype);
entries.Entries.Add(entry);
}
if (_cachedEntries.ContainsKey(station))
{
_cachedEntries[station] = entries;
}
else
{
_cachedEntries.Add(station, entries);
}
}
}
[AdminCommand(AdminFlags.Admin)]
public sealed class CrewManifestCommand : IConsoleCommand
{
public string Command => "crewmanifest";
public string Description => "Opens the crew manifest for the given station.";
public string Help => $"Usage: {Command} <entity uid>";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1)
{
shell.WriteLine($"Invalid argument count.\n{Help}");
return;
}
var entMan = IoCManager.Resolve<IEntityManager>();
if (!EntityUid.TryParse(args[0], out var uid))
{
shell.WriteLine($"{args[0]} is not a valid entity UID.");
return;
}
if (shell.Player == null || shell.Player is not IPlayerSession session)
{
shell.WriteLine("You must run this from a client.");
return;
}
var crewManifestSystem = entMan.EntitySysManager.GetEntitySystem<CrewManifestSystem>();
crewManifestSystem.OpenEui(uid, session);
}
}

View File

@@ -0,0 +1,12 @@
namespace Content.Server.CrewManifest;
[RegisterComponent]
public sealed class CrewManifestViewerComponent : Component
{
/// <summary>
/// If this manifest viewer is unsecure or not. If it is,
/// CCVars.CrewManifestUnsecure being false will
/// not allow this entity to be processed by CrewManifestSystem.
/// </summary>
[DataField("unsecure")] public bool Unsecure;
}

View File

@@ -0,0 +1,9 @@
using Content.Shared.StationRecords;
namespace Content.Server.StationRecords;
[RegisterComponent]
public sealed class GeneralStationRecordConsoleComponent : Component
{
public StationRecordKey? ActiveKey { get; set; }
}

View File

@@ -0,0 +1,13 @@
using Content.Shared.StationRecords;
namespace Content.Server.StationRecords;
[RegisterComponent]
public sealed class StationRecordKeyStorageComponent : Component
{
/// <summary>
/// The key stored in this component.
/// </summary>
[ViewVariables]
public StationRecordKey? Key;
}

View File

@@ -0,0 +1,11 @@
namespace Content.Server.StationRecords;
[RegisterComponent]
public sealed class StationRecordsComponent : Component
{
// Every single record in this station, by key.
// Essentially a columnar database, but I really suck
// at implementing that so
[ViewVariables]
public StationRecordSet Records = new();
}

View File

@@ -0,0 +1,158 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.StationRecords;
namespace Content.Server.StationRecords;
/// <summary>
/// Set of station records. StationRecordsComponent stores these.
/// Keyed by StationRecordKey, which should be obtained from
/// an entity that stores a reference to it.
/// </summary>
public sealed class StationRecordSet
{
private uint _currentRecordId;
private HashSet<StationRecordKey> _keys = new();
private HashSet<StationRecordKey> _recentlyAccessed = new();
[ViewVariables]
private Dictionary<Type, Dictionary<StationRecordKey, object>> _tables = new();
/// <summary>
/// Gets all records of a specific type stored in the record set.
/// </summary>
/// <typeparam name="T">The type of record to fetch.</typeparam>
/// <returns>An enumerable object that contains a pair of both a station key, and the record associated with it.</returns>
public IEnumerable<(StationRecordKey, T)> GetRecordsOfType<T>()
{
if (!_tables.ContainsKey(typeof(T)))
{
yield break;
}
foreach (var (key, entry) in _tables[typeof(T)])
{
if (entry is not T cast)
{
continue;
}
_recentlyAccessed.Add(key);
yield return (key, cast);
}
}
/// <summary>
/// Add a new record into this set of entries.
/// </summary>
/// <param name="station">Station that we're adding the record for.</param>
/// <returns>A key that represents the record in this set.</returns>
public StationRecordKey AddRecord(EntityUid station)
{
var key = new StationRecordKey(_currentRecordId++, station);
_keys.Add(key);
return key;
}
/// <summary>
/// Add an entry into a record.
/// </summary>
/// <param name="key">Key for the record.</param>
/// <param name="entry">Entry to add.</param>
/// <typeparam name="T">Type of the entry that's being added.</typeparam>
public void AddRecordEntry<T>(StationRecordKey key, T entry)
{
if (!_keys.Contains(key) || entry == null)
{
return;
}
if (!_tables.TryGetValue(typeof(T), out var table))
{
table = new();
_tables.Add(typeof(T), table);
}
table.Add(key, entry);
}
/// <summary>
/// Try to get an record entry by type, from this record key.
/// </summary>
/// <param name="key">The StationRecordKey to get the entries from.</param>
/// <param name="entry">The entry that is retrieved from the record set.</param>
/// <typeparam name="T">The type of entry to search for.</typeparam>
/// <returns>True if the record exists and was retrieved, false otherwise.</returns>
public bool TryGetRecordEntry<T>(StationRecordKey key, [NotNullWhen(true)] out T? entry)
{
entry = default;
if (!_keys.Contains(key)
|| !_tables.TryGetValue(typeof(T), out var table)
|| !table.TryGetValue(key, out var entryObject))
{
return false;
}
entry = (T) entryObject;
_recentlyAccessed.Add(key);
return true;
}
/// <summary>
/// Checks if the record associated with this key has an entry of a certain type.
/// </summary>
/// <param name="key">The record key.</param>
/// <typeparam name="T">Type to check.</typeparam>
/// <returns>True if the entry exists, false otherwise.</returns>
public bool HasRecordEntry<T>(StationRecordKey key)
{
return _keys.Contains(key)
&& _tables.TryGetValue(typeof(T), out var table)
&& table.ContainsKey(key);
}
/// <summary>
/// Get the recently accessed keys from this record set.
/// </summary>
/// <returns>All recently accessed keys from this record set.</returns>
public IEnumerable<StationRecordKey> GetRecentlyAccessed()
{
return _recentlyAccessed;
}
/// <summary>
/// Clears the recently accessed keys from the set.
/// </summary>
public void ClearRecentlyAccessed()
{
_recentlyAccessed.Clear();
}
/// <summary>
/// Removes all record entries related to this key from this set.
/// </summary>
/// <param name="key">The key to remove.</param>
/// <returns>True if successful, false otherwise.</returns>
public bool RemoveAllRecords(StationRecordKey key)
{
if (!_keys.Remove(key))
{
return false;
}
foreach (var table in _tables.Values)
{
table.Remove(key);
}
return true;
}
}

View File

@@ -0,0 +1,75 @@
using Content.Server.Station.Systems;
using Content.Shared.StationRecords;
using Robust.Server.GameObjects;
namespace Content.Server.StationRecords.Systems;
public sealed class GeneralStationRecordConsoleSystem : EntitySystem
{
[Dependency] private readonly UserInterfaceSystem _userInterface = default!;
[Dependency] private readonly StationSystem _stationSystem = default!;
[Dependency] private readonly StationRecordsSystem _stationRecordsSystem = default!;
public override void Initialize()
{
SubscribeLocalEvent<GeneralStationRecordConsoleComponent, BoundUIOpenedEvent>(UpdateUserInterface);
SubscribeLocalEvent<GeneralStationRecordConsoleComponent, SelectGeneralStationRecord>(OnKeySelected);
SubscribeLocalEvent<GeneralStationRecordConsoleComponent, RecordModifiedEvent>(UpdateUserInterface);
SubscribeLocalEvent<GeneralStationRecordConsoleComponent, AfterGeneralRecordCreatedEvent>(UpdateUserInterface);
}
private void UpdateUserInterface<T>(EntityUid uid, GeneralStationRecordConsoleComponent component, T ev)
{
UpdateUserInterface(uid, component);
}
private void OnKeySelected(EntityUid uid, GeneralStationRecordConsoleComponent component,
SelectGeneralStationRecord msg)
{
component.ActiveKey = msg.SelectedKey;
UpdateUserInterface(uid, component);
}
private void UpdateUserInterface(EntityUid uid, GeneralStationRecordConsoleComponent? console = null)
{
if (!Resolve(uid, ref console))
{
return;
}
var owningStation = _stationSystem.GetOwningStation(uid);
if (!TryComp<StationRecordsComponent>(owningStation, out var stationRecordsComponent))
{
_userInterface.GetUiOrNull(uid, GeneralStationRecordConsoleKey.Key)?.SetState(new GeneralStationRecordConsoleState(null, null, null));
return;
}
var enumerator = _stationRecordsSystem.GetRecordsOfType<GeneralStationRecord>(owningStation.Value, stationRecordsComponent);
var listing = new Dictionary<StationRecordKey, string>();
foreach (var pair in enumerator)
{
listing.Add(pair.Item1, pair.Item2.Name);
}
if (listing.Count == 0)
{
_userInterface.GetUiOrNull(uid, GeneralStationRecordConsoleKey.Key)?.SetState(new GeneralStationRecordConsoleState(null, null, null));
return;
}
GeneralStationRecord? record = null;
if (console.ActiveKey != null)
{
_stationRecordsSystem.TryGetRecord(owningStation.Value, console.ActiveKey.Value, out record,
stationRecordsComponent);
}
_userInterface
.GetUiOrNull(uid, GeneralStationRecordConsoleKey.Key)?
.SetState(new GeneralStationRecordConsoleState(console.ActiveKey, record, listing));
}
}

View File

@@ -0,0 +1,57 @@
using Content.Shared.StationRecords;
namespace Content.Server.StationRecords.Systems;
public sealed class StationRecordKeyStorageSystem : EntitySystem
{
/// <summary>
/// Assigns a station record key to an entity.
/// </summary>
/// <param name="uid"></param>
/// <param name="key"></param>
/// <param name="keyStorage"></param>
public void AssignKey(EntityUid uid, StationRecordKey key, StationRecordKeyStorageComponent? keyStorage = null)
{
if (!Resolve(uid, ref keyStorage))
{
return;
}
keyStorage.Key = key;
}
/// <summary>
/// Removes a station record key from an entity.
/// </summary>
/// <param name="uid"></param>
/// <param name="keyStorage"></param>
/// <returns></returns>
public StationRecordKey? RemoveKey(EntityUid uid, StationRecordKeyStorageComponent? keyStorage = null)
{
if (!Resolve(uid, ref keyStorage) || keyStorage.Key == null)
{
return null;
}
var key = keyStorage.Key;
keyStorage.Key = null;
return key;
}
/// <summary>
/// Checks if an entity currently contains a station record key.
/// </summary>
/// <param name="uid"></param>
/// <param name="keyStorage"></param>
/// <returns></returns>
public bool CheckKey(EntityUid uid, StationRecordKeyStorageComponent? keyStorage = null)
{
if (!Resolve(uid, ref keyStorage))
{
return false;
}
return keyStorage.Key != null;
}
}

View File

@@ -0,0 +1,282 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.Access.Systems;
using Content.Server.GameTicking;
using Content.Server.Station.Systems;
using Content.Server.StationRecords;
using Content.Server.StationRecords.Systems;
using Content.Shared.Access.Components;
using Content.Shared.Inventory;
using Content.Shared.PDA;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Content.Shared.StationRecords;
using Robust.Shared.Enums;
using Robust.Shared.Prototypes;
/// <summary>
/// Station records.
///
/// A station record is tied to an ID card, or anything that holds
/// a station record's key. This key will determine access to a
/// station record set's record entries, and it is imperative not
/// to lose the item that holds the key under any circumstance.
///
/// Records are mostly a roleplaying tool, but can have some
/// functionality as well (i.e., security records indicating that
/// a specific person holding an ID card with a linked key is
/// currently under warrant, showing a crew manifest with user
/// settable, custom titles).
///
/// General records are tied into this system, as most crewmembers
/// should have a general record - and most systems should probably
/// depend on this general record being created. This is subject
/// to change.
/// </summary>
public sealed class StationRecordsSystem : EntitySystem
{
[Dependency] private readonly InventorySystem _inventorySystem = default!;
[Dependency] private readonly StationRecordKeyStorageSystem _keyStorageSystem = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StationInitializedEvent>(OnStationInitialize);
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnPlayerSpawn);
}
private void OnStationInitialize(StationInitializedEvent args)
{
AddComp<StationRecordsComponent>(args.Station);
}
private void OnPlayerSpawn(PlayerSpawnCompleteEvent args)
{
CreateGeneralRecord(args.Station, args.Mob, args.Profile, args.JobId);
}
private void CreateGeneralRecord(EntityUid station, EntityUid player, HumanoidCharacterProfile profile,
string? jobId, StationRecordsComponent? records = null)
{
if (!Resolve(station, ref records)
|| String.IsNullOrEmpty(jobId)
|| !_prototypeManager.HasIndex<JobPrototype>(jobId))
{
return;
}
if (!_inventorySystem.TryGetSlotEntity(player, "id", out var idUid))
{
return;
}
CreateGeneralRecord(station, idUid.Value, profile.Name, profile.Age, profile.Species, profile.Gender, jobId, profile, records);
}
/// <summary>
/// Create a general record to store in a station's record set.
/// </summary>
/// <remarks>
/// This is tied into the record system, as any crew member's
/// records should generally be dependent on some generic
/// record with the bare minimum of information involved.
/// </remarks>
/// <param name="station">The entity uid of the station.</param>
/// <param name="idUid">The entity uid of an entity's ID card. Can be null.</param>
/// <param name="name">Name of the character.</param>
/// <param name="species">Species of the character.</param>
/// <param name="gender">Gender of the character.</param>
/// <param name="jobId">
/// The job to initially tie this record to. This must be a valid job loaded in, otherwise
/// this call will cause an exception. Ensure that a general record starts out with a job
/// that is currently a valid job prototype.
/// </param>
/// <param name="profile">
/// Profile for the related player. This is so that other systems can get further information
/// about the player character.
/// Optional - other systems should anticipate this.
/// </param>
/// <param name="records">Station records component.</param>
public void CreateGeneralRecord(EntityUid station, EntityUid? idUid, string name, int age, string species, Gender gender, string jobId, HumanoidCharacterProfile? profile = null,
StationRecordsComponent? records = null)
{
if (!Resolve(station, ref records))
{
return;
}
if (!_prototypeManager.TryIndex(jobId, out JobPrototype? jobPrototype))
{
throw new ArgumentException($"Invalid job prototype ID: {jobId}");
}
var record = new GeneralStationRecord()
{
Name = name,
Age = age,
JobTitle = jobPrototype.Name,
JobIcon = jobPrototype.Icon,
JobPrototype = jobId,
Species = species,
Gender = gender,
DisplayPriority = jobPrototype.Weight
};
var key = records.Records.AddRecord(station);
records.Records.AddRecordEntry(key, record);
// entry.Entries.Add(typeof(GeneralStationRecord), record);
if (idUid != null)
{
var keyStorageEntity = idUid;
if (TryComp(idUid, out PDAComponent? pdaComponent) && pdaComponent.ContainedID != null)
{
keyStorageEntity = pdaComponent.IdSlot.Item;
}
if (keyStorageEntity != null)
{
_keyStorageSystem.AssignKey(keyStorageEntity.Value, key);
}
}
RaiseLocalEvent(new AfterGeneralRecordCreatedEvent(key, record, profile));
}
/// <summary>
/// Removes a record from this station.
/// </summary>
/// <param name="station">Station to remove the record from.</param>
/// <param name="key">The key to remove.</param>
/// <param name="records">Station records component.</param>
/// <returns>True if the record was removed, false otherwise.</returns>
public bool RemoveRecord(EntityUid station, StationRecordKey key, StationRecordsComponent? records = null)
{
if (station != key.OriginStation || !Resolve(station, ref records))
{
return false;
}
RaiseLocalEvent(new RecordRemovedEvent(key));
return records.Records.RemoveAllRecords(key);
}
/// <summary>
/// Try to get a record from this station's record entries,
/// from the provided station record key. Will always return
/// null if the key does not match the station.
/// </summary>
/// <param name="station">Station to get the record from.</param>
/// <param name="key">Key to try and index from the record set.</param>
/// <param name="entry">The resulting entry.</param>
/// <param name="records">Station record component.</param>
/// <typeparam name="T">Type to get from the record set.</typeparam>
/// <returns>True if the record was obtained, false otherwise.</returns>
public bool TryGetRecord<T>(EntityUid station, StationRecordKey key, [NotNullWhen(true)] out T? entry, StationRecordsComponent? records = null)
{
entry = default;
if (key.OriginStation != station || !Resolve(station, ref records))
{
return false;
}
return records.Records.TryGetRecordEntry(key, out entry);
}
/// <summary>
/// Gets all records of a specific type from a station.
/// </summary>
/// <param name="station">The station to get the records from.</param>
/// <param name="records">Station records component.</param>
/// <typeparam name="T">Type of record to fetch</typeparam>
/// <returns>Enumerable of pairs with a station record key, and the entry in question of type T.</returns>
public IEnumerable<(StationRecordKey, T)> GetRecordsOfType<T>(EntityUid station, StationRecordsComponent? records = null)
{
if (!Resolve(station, ref records))
{
return new (StationRecordKey, T)[]{};
}
return records.Records.GetRecordsOfType<T>();
}
/// <summary>
/// Synchronizes a station's records with any systems that need it.
/// </summary>
/// <param name="station">The station to synchronize any recently accessed records with..</param>
/// <param name="records">Station records component.</param>
public void Synchronize(EntityUid station, StationRecordsComponent? records = null)
{
if (!Resolve(station, ref records))
{
return;
}
foreach (var key in records.Records.GetRecentlyAccessed())
{
RaiseLocalEvent(new RecordModifiedEvent(key));
}
records.Records.ClearRecentlyAccessed();
}
}
/// <summary>
/// Event raised after the player's general profile is created.
/// Systems that modify records on a station would have more use
/// listening to this event, as it contains the character's record key.
/// Also stores the general record reference, to save some time.
/// </summary>
public sealed class AfterGeneralRecordCreatedEvent : EntityEventArgs
{
public StationRecordKey Key { get; }
public GeneralStationRecord Record { get; }
/// <summary>
/// Profile for the related player. This is so that other systems can get further information
/// about the player character.
/// Optional - other systems should anticipate this.
/// </summary>
public HumanoidCharacterProfile? Profile { get; }
public AfterGeneralRecordCreatedEvent(StationRecordKey key, GeneralStationRecord record, HumanoidCharacterProfile? profile)
{
Key = key;
Record = record;
Profile = profile;
}
}
/// <summary>
/// Event raised after a record is removed. Only the key is given
/// when the record is removed, so that any relevant systems/components
/// that store record keys can then remove the key from their internal
/// fields.
/// </summary>
public sealed class RecordRemovedEvent : EntityEventArgs
{
public StationRecordKey Key { get; }
public RecordRemovedEvent(StationRecordKey key)
{
Key = key;
}
}
/// <summary>
/// Event raised after a record is modified. This is to
/// inform other systems that records stored in this key
/// may have changed.
/// </summary>
public sealed class RecordModifiedEvent : EntityEventArgs
{
public StationRecordKey Key { get; }
public RecordModifiedEvent(StationRecordKey key)
{
Key = key;
}
}

View File

@@ -955,6 +955,37 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<string> CentcommMap = public static readonly CVarDef<string> CentcommMap =
CVarDef.Create("shuttle.centcomm_map", "/Maps/centcomm.yml", CVar.SERVERONLY); CVarDef.Create("shuttle.centcomm_map", "/Maps/centcomm.yml", CVar.SERVERONLY);
/*
* Crew Manifests
*/
/// <summary>
/// Setting this allows a crew manifest to be opened from any window
/// that has a crew manifest button, and sends the correct message.
/// If this is false, only in-game entities will allow you to see
/// the crew manifest, if the functionality is coded in.
/// Having administrator priveledge ignores this, but will still
/// hide the button in UI windows.
/// </summary>
public static readonly CVarDef<bool> CrewManifestWithoutEntity =
CVarDef.Create("crewmanifest.no_entity", true, CVar.REPLICATED);
/// <summary>
/// Setting this allows the crew manifest to be viewed from 'unsecure'
/// entities, such as the PDA.
/// </summary>
public static readonly CVarDef<bool> CrewManifestUnsecure =
CVarDef.Create("crewmanifest.unsecure", true, CVar.REPLICATED);
/// <summary>
/// Dictates the order the crew manifest will appear in, in terms of its sections.
/// Sections not in this list will appear at the end of the list, in no
/// specific order.
/// </summary>
public static readonly CVarDef<string> CrewManifestOrdering =
CVarDef.Create("crewmanifest.ordering", "Command,Security,Science,Medical,Engineering,Cargo,Civilian,Unknown",
CVar.REPLICATED);
/* /*
* VIEWPORT * VIEWPORT
*/ */

View File

@@ -0,0 +1,75 @@
using Content.Shared.Eui;
using Robust.Shared.Serialization;
namespace Content.Shared.CrewManifest;
/// <summary>
/// A message to send to the server when requesting a crew manifest.
/// CrewManifestSystem will open an EUI that will send the crew manifest
/// to the player when it is updated.
/// </summary>
[Serializable, NetSerializable]
public sealed class RequestCrewManifestMessage : EntityEventArgs
{
public EntityUid Id { get; }
public RequestCrewManifestMessage(EntityUid id)
{
Id = id;
}
}
[Serializable, NetSerializable]
public sealed class CrewManifestEuiState : EuiStateBase
{
public string StationName { get; }
public CrewManifestEntries? Entries { get; }
public CrewManifestEuiState(string stationName, CrewManifestEntries? entries)
{
StationName = stationName;
Entries = entries;
}
}
[Serializable, NetSerializable]
public sealed class CrewManifestEuiClosed : EuiMessageBase
{}
[Serializable, NetSerializable]
public sealed class CrewManifestEntries
{
/// <summary>
/// Entries in the crew manifest. Goes by department ID.
/// </summary>
// public Dictionary<string, List<CrewManifestEntry>> Entries = new();
public List<CrewManifestEntry> Entries = new();
}
[Serializable, NetSerializable]
public sealed class CrewManifestEntry
{
public string Name { get; }
public string JobTitle { get; }
public string JobIcon { get; }
public string JobPrototype { get; }
public CrewManifestEntry(string name, string jobTitle, string jobIcon, string jobPrototype)
{
Name = name;
JobTitle = jobTitle;
JobIcon = jobIcon;
JobPrototype = jobPrototype;
}
}
/// <summary>
/// Tells the server to open a crew manifest UI from
/// this entity's point of view.
/// </summary>
[Serializable, NetSerializable]
public sealed class CrewManifestOpenUiMessage : BoundUserInterfaceMessage
{}

View File

@@ -0,0 +1,59 @@
using Robust.Shared.Enums;
using Robust.Shared.Serialization;
namespace Content.Shared.StationRecords;
/// <summary>
/// General station record. Indicates the crewmember's name and job.
/// </summary>
[Serializable, NetSerializable]
public sealed class GeneralStationRecord
{
/// <summary>
/// Name tied to this station record.
/// </summary>
[ViewVariables]
public string Name = string.Empty;
/// <summary>
/// Age of the person that this station record represents.
/// </summary>
[ViewVariables]
public int Age;
/// <summary>
/// Job title tied to this station record.
/// </summary>
[ViewVariables]
public string JobTitle = string.Empty;
/// <summary>
/// Job icon tied to this station record.
/// </summary>
[ViewVariables]
public string JobIcon = string.Empty;
[ViewVariables]
public string JobPrototype = string.Empty;
/// <summary>
/// Species tied to this station record.
/// </summary>
[ViewVariables]
public string Species = string.Empty;
/// <summary>
/// Gender identity tied to this station record.
/// </summary>
/// <remarks>Sex should be placed in a medical record, not a general record.</remarks>
[ViewVariables]
public Gender Gender = Gender.Neuter;
/// <summary>
/// The priority to display this record at.
/// This is taken from the 'weight' of a job prototype,
/// usually.
/// </summary>
[ViewVariables]
public int DisplayPriority;
}

View File

@@ -0,0 +1,54 @@
using Robust.Shared.Serialization;
namespace Content.Shared.StationRecords;
[Serializable, NetSerializable]
public enum GeneralStationRecordConsoleKey : byte
{
Key
}
/// <summary>
/// General station records console state. There are a few states:
/// - SelectedKey null, Record null, RecordListing null
/// - The station record database could not be accessed.
/// - SelectedKey null, Record null, RecordListing non-null
/// - Records are populated in the database, or at least the station has
/// the correct component.
/// - SelectedKey non-null, Record null, RecordListing non-null
/// - The selected key does not have a record tied to it.
/// - SelectedKey non-null, Record non-null, RecordListing non-null
/// - The selected key has a record tied to it, and the record has been sent.
///
/// Other states are erroneous.
/// </summary>
[Serializable, NetSerializable]
public sealed class GeneralStationRecordConsoleState : BoundUserInterfaceState
{
/// <summary>
/// Current selected key.
/// </summary>
public StationRecordKey? SelectedKey { get; }
public GeneralStationRecord? Record { get; }
public Dictionary<StationRecordKey, string>? RecordListing { get; }
public GeneralStationRecordConsoleState(StationRecordKey? key, GeneralStationRecord? record, Dictionary<StationRecordKey, string>? recordListing)
{
SelectedKey = key;
Record = record;
RecordListing = recordListing;
}
public bool IsEmpty() => SelectedKey == null && Record == null && RecordListing == null;
}
[Serializable, NetSerializable]
public sealed class SelectGeneralStationRecord : BoundUserInterfaceMessage
{
public StationRecordKey? SelectedKey { get; }
public SelectGeneralStationRecord(StationRecordKey? selectedKey)
{
SelectedKey = selectedKey;
}
}

View File

@@ -0,0 +1,21 @@
using Robust.Shared.Serialization;
namespace Content.Shared.StationRecords;
// Station record keys. These should be stored somewhere,
// preferably within an ID card.
[Serializable, NetSerializable]
public readonly struct StationRecordKey
{
[ViewVariables]
public uint ID { get; }
[ViewVariables]
public EntityUid OriginStation { get; }
public StationRecordKey(uint id, EntityUid originStation)
{
ID = id;
OriginStation = originStation;
}
}

View File

@@ -5,6 +5,7 @@ id-card-console-window-save-button = Save
id-card-console-window-job-title-label = Job title: id-card-console-window-job-title-label = Job title:
id-card-console-window-eject-button = Eject id-card-console-window-eject-button = Eject
id-card-console-window-insert-button = Insert id-card-console-window-insert-button = Insert
id-card-console-window-job-selection-label = Job presets:
access-id-card-console-component-no-hands-error = You have no hands. access-id-card-console-component-no-hands-error = You have no hands.
id-card-console-privileged-id = Privileged ID id-card-console-privileged-id = Privileged ID

View File

@@ -0,0 +1,3 @@
crew-manifest-window-title = Crew Manifest
crew-manifest-button-label = Crew Manifest
crew-manifest-no-valid-station = Invalid station, or empty manifest!

View File

@@ -0,0 +1,9 @@
general-station-record-console-window-title = Station Records Computer
general-station-record-console-select-record-info = Select a record on the left.
general-station-record-console-empty-state = No records found!
general-station-record-console-no-record-found = No record was found for the selected person.
general-station-record-console-record-age = Age: {$age}
general-station-record-console-record-title = Job: {$job}
general-station-record-console-record-species = Species: {$species}
general-station-record-console-record-gender = Gender: {$gender}

View File

@@ -65,6 +65,8 @@
whitelist: whitelist:
components: components:
- IdCard - IdCard
- type: CrewManifestViewer
unsecure: true
- type: Tag - type: Tag
tags: tags:
- DoorBumpOpener - DoorBumpOpener

View File

@@ -15,6 +15,7 @@
heldPrefix: default heldPrefix: default
- type: Access - type: Access
- type: IdCard - type: IdCard
- type: StationRecordKeyStorage
- type: Tag - type: Tag
tags: tags:
- DoorBumpOpener - DoorBumpOpener

View File

@@ -183,6 +183,27 @@
- type: Computer - type: Computer
board: CriminalRecordsComputerCircuitboard board: CriminalRecordsComputerCircuitboard
- type: entity
parent: BaseComputer
id: ComputerStationRecords
name: station records computer
description: This can be used to check station records.
components:
- type: GeneralStationRecordConsole
- type: UserInterface
interfaces:
- key: enum.GeneralStationRecordConsoleKey.Key
type: GeneralStationRecordConsoleBoundUserInterface
- type: ActivatableUI
key: enum.GeneralStationRecordConsoleKey.Key
- type: ActivatableUIRequiresPower
- type: PointLight
radius: 1.5
energy: 1.6
color: "#1f8c28"
- type: Computer
board: CriminalRecordsComputerCircuitboard
- type: entity - type: entity
parent: BaseComputer parent: BaseComputer
id: ComputerCrewMonitoring id: ComputerCrewMonitoring
@@ -282,6 +303,7 @@
interfaces: interfaces:
- key: enum.IdCardConsoleUiKey.Key - key: enum.IdCardConsoleUiKey.Key
type: IdCardConsoleBoundUserInterface type: IdCardConsoleBoundUserInterface
- type: CrewManifestViewer
- type: Appearance - type: Appearance
visuals: visuals:
- type: ComputerVisualizer - type: ComputerVisualizer