Port Height & Records Computer from Cosmatic Drift (#2236)
* Port character records from CD * Make species' base scales respected by CD heights * Hide the height editor in the humanoid profile editor --------- Co-authored-by: Janet Blackquill <uhhadd@gmail.com>
This commit is contained in:
parent
13c9095009
commit
f024f46b0b
|
|
@ -3,6 +3,7 @@
|
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
xmlns:at="clr-namespace:Content.Client.Administration.UI.Tabs.AdminTab"
|
||||
xmlns:cdAdmin="clr-namespace:Content.Client._CD.Admin.UI"
|
||||
Margin="4"
|
||||
MinSize="50 50">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
|
|
@ -16,6 +17,8 @@
|
|||
<cc:UICommandButton Command="callshuttle" Text="{Loc admin-player-actions-window-shuttle}" WindowType="{x:Type at:AdminShuttleWindow}"/>
|
||||
<cc:CommandButton Command="adminlogs" Text="{Loc admin-player-actions-window-admin-logs}"/>
|
||||
<cc:CommandButton Command="faxui" Text="{Loc admin-player-actions-window-admin-fax}"/>
|
||||
<!-- CD: records purge button -->
|
||||
<cc:UICommandButton Command="purgecharacterrecords" Text="{Loc admin-player-actions-window-cd-record-purge}" WindowType="{x:Type cdAdmin:ModifyCharacterRecords}"/>
|
||||
</GridContainer>
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ using Content.Shared.Preferences;
|
|||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Numerics; // CD - Character Records
|
||||
using Robust.Client.Console; // CD - Character Records
|
||||
|
||||
namespace Content.Client.Humanoid;
|
||||
|
||||
|
|
@ -30,6 +32,13 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
|
|||
UpdateLayers(component, sprite);
|
||||
ApplyMarkingSet(component, sprite);
|
||||
|
||||
// Begin CD - Character Records
|
||||
var speciesPrototype = _prototypeManager.Index<SpeciesPrototype>(component.Species);
|
||||
var height = Math.Clamp(MathF.Round(component.Height, 2), speciesPrototype.MinHeight, speciesPrototype.MaxHeight); // should NOT be locked, at all
|
||||
|
||||
sprite.Scale = speciesPrototype.BaseScale * new Vector2(speciesPrototype.ScaleHeight ? height : 1f, height); // DV - CD Character Records shouldn't nuke species heights
|
||||
// End CD - Character Records
|
||||
|
||||
sprite[sprite.LayerMapReserveBlank(HumanoidVisualLayers.Eyes)].Color = component.EyeColor;
|
||||
}
|
||||
|
||||
|
|
@ -199,6 +208,7 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
|
|||
humanoid.Species = profile.Species;
|
||||
humanoid.SkinColor = profile.Appearance.SkinColor;
|
||||
humanoid.EyeColor = profile.Appearance.EyeColor;
|
||||
humanoid.Height = profile.Height; // CD - Character Records
|
||||
|
||||
UpdateSprite(humanoid, Comp<SpriteComponent>(uid));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,6 +67,15 @@
|
|||
<Control HorizontalExpand="True"/>
|
||||
<LineEdit Name="AgeEdit" MinSize="40 0" HorizontalAlignment="Right" />
|
||||
</BoxContainer>
|
||||
<!-- Begin CD - Character Records -->
|
||||
<BoxContainer HorizontalExpand="True" Visible="False"> <!-- DeltaV - we haven't decided on height yet -->
|
||||
<Label Text="{Loc 'humanoid-profile-editor-height-label'}" />
|
||||
<Control HorizontalExpand="True" />
|
||||
<Slider Name="CDHeightSlider" HorizontalAlignment="Right" SetWidth="300" MinValue="0.0" MaxValue="1.0"/>
|
||||
<LineEdit HorizontalAlignment="Right" Name="CDHeight" MinSize="60 0" Text="1.0" />
|
||||
<Button Name="CDHeightReset" Text="{Loc 'humanoid-profile-editor-reset-height-button'}" HorizontalAlignment="Right"/>
|
||||
</BoxContainer>
|
||||
<!-- End CD - Character Records -->
|
||||
<!-- Sex -->
|
||||
<BoxContainer HorizontalExpand="True">
|
||||
<Label Text="{Loc 'humanoid-profile-editor-sex-label'}" />
|
||||
|
|
|
|||
|
|
@ -33,6 +33,11 @@ using Robust.Shared.Enums;
|
|||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using Direction = Robust.Shared.Maths.Direction;
|
||||
// Begin CD - Character Records
|
||||
using System.Globalization;
|
||||
using Content.Client._CD.Records.UI;
|
||||
using Content.Shared._CD.Records;
|
||||
// End CD - Character Records
|
||||
|
||||
namespace Content.Client.Lobby.UI
|
||||
{
|
||||
|
|
@ -96,6 +101,12 @@ namespace Content.Client.Lobby.UI
|
|||
|
||||
private bool _isDirty;
|
||||
|
||||
// Begin CD - Station Records
|
||||
private float _defaultHeight = 1f;
|
||||
|
||||
private readonly RecordEditorGui _recordsTab;
|
||||
// End CD - Station Records
|
||||
|
||||
[ValidatePrototypeId<GuideEntryPrototype>]
|
||||
private const string DefaultSpeciesGuidebook = "Species";
|
||||
|
||||
|
|
@ -220,6 +231,43 @@ namespace Content.Client.Lobby.UI
|
|||
OnSkinColorOnValueChanged();
|
||||
};
|
||||
|
||||
// Begin CD - Character Records
|
||||
#region CDHeight
|
||||
|
||||
CDHeight.OnTextChanged += args =>
|
||||
{
|
||||
if (Profile is null || !float.TryParse(args.Text, out var newHeight))
|
||||
return;
|
||||
|
||||
var prototype = _prototypeManager.Index<SpeciesPrototype>(Profile.Species);
|
||||
newHeight = MathF.Round(Math.Clamp(newHeight, prototype.MinHeight, prototype.MaxHeight), 2);
|
||||
|
||||
// The percentage between the start and end numbers, aka "inverse lerp"
|
||||
var sliderPercent = (newHeight - prototype.MinHeight) /
|
||||
(prototype.MaxHeight - prototype.MinHeight);
|
||||
CDHeightSlider.Value = sliderPercent;
|
||||
|
||||
SetProfileHeight(newHeight);
|
||||
};
|
||||
|
||||
CDHeightReset.OnPressed += _ =>
|
||||
{
|
||||
CDHeight.SetText(_defaultHeight.ToString(CultureInfo.InvariantCulture), true);
|
||||
};
|
||||
|
||||
CDHeightSlider.OnValueChanged += _ =>
|
||||
{
|
||||
if (Profile is null)
|
||||
return;
|
||||
var prototype = _prototypeManager.Index<SpeciesPrototype>(Profile.Species);
|
||||
var newHeight = MathF.Round(MathHelper.Lerp(prototype.MinHeight, prototype.MaxHeight, CDHeightSlider.Value), 2);
|
||||
CDHeight.Text = newHeight.ToString(CultureInfo.InvariantCulture);
|
||||
SetProfileHeight(newHeight);
|
||||
};
|
||||
|
||||
#endregion CDHeight
|
||||
// End CD - Character Records
|
||||
|
||||
#region Skin
|
||||
|
||||
Skin.OnValueChanged += _ =>
|
||||
|
|
@ -413,6 +461,16 @@ namespace Content.Client.Lobby.UI
|
|||
|
||||
#endregion Markings
|
||||
|
||||
// Begin CD - Character Records
|
||||
#region CosmaticRecords
|
||||
|
||||
_recordsTab = new RecordEditorGui(UpdateProfileRecords);
|
||||
TabContainer.AddChild(_recordsTab);
|
||||
TabContainer.SetTabTitle(TabContainer.ChildCount - 1, Loc.GetString("humanoid-profile-editor-cd-records-tab"));
|
||||
|
||||
#endregion CosmaticRecords
|
||||
// End CD - Character Records
|
||||
|
||||
RefreshFlavorText();
|
||||
|
||||
#region Dummy
|
||||
|
|
@ -754,6 +812,11 @@ namespace Content.Client.Lobby.UI
|
|||
UpdateCMarkingsHair();
|
||||
UpdateCMarkingsFacialHair();
|
||||
|
||||
// Begin CD - Character Records
|
||||
UpdateHeightControls();
|
||||
_recordsTab.Update(profile);
|
||||
// End CD - Character Records
|
||||
|
||||
RefreshAntags();
|
||||
RefreshJobs();
|
||||
RefreshLoadouts();
|
||||
|
|
@ -1050,6 +1113,16 @@ namespace Content.Client.Lobby.UI
|
|||
UpdateJobPriorities();
|
||||
}
|
||||
|
||||
// Start CD - Character Records
|
||||
private void UpdateProfileRecords(PlayerProvidedCharacterRecords records)
|
||||
{
|
||||
if (Profile is null)
|
||||
return;
|
||||
Profile = Profile.WithCDCharacterRecords(records);
|
||||
IsDirty = true;
|
||||
}
|
||||
// End CD - Character Records
|
||||
|
||||
private void OnFlavorTextChange(string content)
|
||||
{
|
||||
if (Profile is null)
|
||||
|
|
@ -1231,6 +1304,15 @@ namespace Content.Client.Lobby.UI
|
|||
_entManager.System<MetaDataSystem>().SetEntityName(PreviewDummy, newName);
|
||||
}
|
||||
|
||||
// Begin CD - Character Records
|
||||
private void SetProfileHeight(float height)
|
||||
{
|
||||
Profile = Profile?.WithHeight(height);
|
||||
SetDirty();
|
||||
ReloadProfilePreview();
|
||||
}
|
||||
// End CD - Character Records
|
||||
|
||||
private void SetSpawnPriority(SpawnPriorityPreference newSpawnPriority)
|
||||
{
|
||||
Profile = Profile?.WithSpawnPriorityPreference(newSpawnPriority);
|
||||
|
|
@ -1416,6 +1498,26 @@ namespace Content.Client.Lobby.UI
|
|||
PronounsButton.SelectId((int) Profile.Gender);
|
||||
}
|
||||
|
||||
// Begin CD - Character Records
|
||||
private void UpdateHeightControls()
|
||||
{
|
||||
if (Profile == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var species = _species.Find(x => x.ID == Profile.Species);
|
||||
if (species != null)
|
||||
_defaultHeight = species.DefaultHeight;
|
||||
|
||||
var prototype = _prototypeManager.Index<SpeciesPrototype>(Profile.Species);
|
||||
var sliderPercent = (Profile.Height - prototype.MinHeight) /
|
||||
(prototype.MaxHeight - prototype.MinHeight);
|
||||
CDHeightSlider.Value = sliderPercent;
|
||||
CDHeight.Text = Profile.Height.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
// End CD - Character Records
|
||||
|
||||
private void UpdateSpawnPriorityControls()
|
||||
{
|
||||
if (Profile == null)
|
||||
|
|
@ -1558,6 +1660,8 @@ namespace Content.Client.Lobby.UI
|
|||
var name = HumanoidCharacterProfile.GetName(Profile.Species, Profile.Gender);
|
||||
SetName(name);
|
||||
UpdateNameEdit();
|
||||
|
||||
_recordsTab.Update(Profile); // CD - Character Records
|
||||
}
|
||||
|
||||
private async void ExportImage()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
Title="{Loc cd-actions-admin-modify-records}"
|
||||
MinSize="300 300">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<LineEdit Name="EntityEdit" PlaceHolder="Entity" HorizontalExpand="True" />
|
||||
<OptionButton Name="EntityEntryType" HorizontalExpand="True" />
|
||||
<LineEdit Name="EntityEntryIndex" PlaceHolder="Index"/>
|
||||
<BoxContainer Orientation="Horizontal" >
|
||||
<cc:CommandButton Name="PurgeCommand" Text="{Loc cd-actions-admin-modify-reset}"/>
|
||||
<cc:CommandButton Name="DelCommand" Text="{Loc cd-actions-admin-modify-del-entry}"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
using Content.Shared._CD.Records;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client._CD.Admin.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ModifyCharacterRecords : DefaultWindow
|
||||
{
|
||||
public ModifyCharacterRecords()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
foreach (var v in Enum.GetValues<CharacterRecordType>())
|
||||
{
|
||||
EntityEntryType.AddItem(v.ToString());
|
||||
}
|
||||
|
||||
EntityEntryType.OnItemSelected += args =>
|
||||
{
|
||||
EntityEntryType.SelectId(args.Id);
|
||||
UpdateCommands();
|
||||
};
|
||||
|
||||
EntityEdit.OnTextChanged += _ => UpdateCommands();
|
||||
EntityEntryIndex.OnTextChanged += _ => UpdateCommands();
|
||||
}
|
||||
|
||||
private void UpdateCommands()
|
||||
{
|
||||
if (!int.TryParse(EntityEdit.Text, out var uid))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!int.TryParse(EntityEntryIndex.Text, out var idx))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var ty = (CharacterRecordType)EntityEntryType.SelectedId;
|
||||
|
||||
PurgeCommand.Command = $"purgecharacterrecords {uid}";
|
||||
DelCommand.Command = $"delrecordentry {uid} {ty.ToString()} {idx}";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
using Content.Shared.CriminalRecords.Components;
|
||||
using Content.Shared.CriminalRecords;
|
||||
using Content.Shared.StationRecords;
|
||||
using Content.Shared._CD.Records;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Client._CD.Records.UI;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class CharacterRecordConsoleBoundUserInterface(EntityUid owner, Enum key) : BoundUserInterface(owner, key)
|
||||
{
|
||||
[ViewVariables] private CharacterRecordViewer? _window;
|
||||
|
||||
protected override void UpdateState(BoundUserInterfaceState baseState)
|
||||
{
|
||||
base.UpdateState(baseState);
|
||||
if (baseState is not CharacterRecordConsoleState state)
|
||||
return;
|
||||
|
||||
if (_window?.IsSecurity() ?? false)
|
||||
{
|
||||
var comp = EntMan.GetComponent<CriminalRecordsConsoleComponent>(Owner);
|
||||
_window!.SecurityWantedStatusMaxLength = comp.MaxStringLength;
|
||||
}
|
||||
|
||||
_window?.UpdateState(state);
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
|
||||
_window = new();
|
||||
_window.OnClose += Close;
|
||||
_window.OnListingItemSelected += meta =>
|
||||
{
|
||||
SendMessage(new CharacterRecordConsoleSelectMsg(meta?.CharacterRecordKey));
|
||||
|
||||
// If we are a security records console, we also need to inform the criminal records
|
||||
// system of our state.
|
||||
if (_window.IsSecurity() && meta?.StationRecordKey != null)
|
||||
{
|
||||
SendMessage(new SelectStationRecord(meta.Value.StationRecordKey.Value));
|
||||
_window.SetSecurityStatusEnabled(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the user does not have criminal records for some reason, we should not be able
|
||||
// to set their wanted status
|
||||
_window.SetSecurityStatusEnabled(false);
|
||||
}
|
||||
};
|
||||
|
||||
_window.OnFiltersChanged += (ty, txt) =>
|
||||
{
|
||||
SendMessage(txt == null
|
||||
? new CharacterRecordsConsoleFilterMsg(null)
|
||||
: new CharacterRecordsConsoleFilterMsg(new StationRecordsFilter(ty, txt)));
|
||||
};
|
||||
|
||||
_window.OnSetSecurityStatus += (status, reason) =>
|
||||
{
|
||||
SendMessage(new CriminalRecordChangeStatus(status, reason));
|
||||
};
|
||||
|
||||
_window.OpenCentered();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
_window?.Close();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:records="clr-namespace:Content.Client._CD.Records.UI"
|
||||
MinSize="850 750"
|
||||
SetSize="850 750" >
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<!-- Search bar -->
|
||||
<BoxContainer Margin="5 5 5 10" HorizontalExpand="true" VerticalAlignment="Center">
|
||||
<OptionButton Name="RecordFilterType" MinWidth="200" Margin="0 0 10 0" Visible="False"/>
|
||||
<!-- Yes, we do steal some localizations, should be fine -->
|
||||
<LineEdit Name="RecordFiltersValue"
|
||||
PlaceHolder="{Loc 'general-station-record-for-filter-line-placeholder'}" HorizontalExpand="True"/>
|
||||
<Button Name="RecordFilters" Text="{Loc 'general-station-record-console-search-records'}"/>
|
||||
<Button Name="RecordFiltersReset" Text="{Loc 'general-station-record-console-reset-filters'}"/>
|
||||
</BoxContainer>
|
||||
<BoxContainer VerticalExpand="True">
|
||||
<!-- Character listing -->
|
||||
<BoxContainer Orientation="Vertical" Margin="5" MinWidth="250" MaxWidth="250">
|
||||
<Label Name="CharacterListingStatus" Visible="False" />
|
||||
<ScrollContainer VerticalExpand="True">
|
||||
<ItemList Name="CharacterListing" />
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
<!-- Record box -->
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" Margin="5 5 10 10">
|
||||
<Label Name="RecordContainerStatus" Visible="False" Text="{Loc 'cd-record-viewer-no-record-selected'}"/>
|
||||
<BoxContainer Name="RecordContainer" Orientation="Vertical" Visible="False" >
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
|
||||
<!-- Common -->
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
|
||||
<Label Name="RecordContainerName" StyleClasses="LabelBig" />
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<Label Text="{Loc 'cd-character-records-viewer-record-age'}"/>
|
||||
<Control HorizontalExpand="True" />
|
||||
<Label Name="RecordContainerAge" Align="Right" />
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<Label Text="{Loc 'cd-character-records-viewer-record-job'}"/>
|
||||
<Control HorizontalExpand="True" />
|
||||
<Label Name="RecordContainerJob" Align="Right" />
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<Label Text="{Loc 'cd-character-records-viewer-record-gender'}"/>
|
||||
<Control HorizontalExpand="True" />
|
||||
<Label Name="RecordContainerGender" Align="Right" />
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<Label Text="{Loc 'cd-character-records-viewer-record-species'}"/>
|
||||
<Control HorizontalExpand="True" />
|
||||
<Label Name="RecordContainerSpecies" Align="Right" />
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<Label Text="{Loc 'humanoid-profile-editor-cd-records-height'}"/>
|
||||
<Control HorizontalExpand="True" />
|
||||
<Label Name="RecordContainerHeight" Align="Right" />
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<Label Text="{Loc 'humanoid-profile-editor-cd-records-weight'}"/>
|
||||
<Control HorizontalExpand="True" />
|
||||
<Label Name="RecordContainerWeight" Align="Right"/>
|
||||
</BoxContainer>
|
||||
<records:RecordLongItemDisplay Name="RecordContainerContactName" Title="{Loc 'humanoid-profile-editor-cd-records-contact-name'}"/>
|
||||
</BoxContainer>
|
||||
<!-- Employment -->
|
||||
<BoxContainer Name="RecordContainerEmployment" Orientation="Vertical" HorizontalExpand="True" Visible="False" >
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<Label Text="{Loc 'humanoid-profile-editor-cd-records-work-authorization'}"/>
|
||||
<Control HorizontalExpand="True" />
|
||||
<Label Name="RecordContainerWorkAuth" Align="Right" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<!-- Medical -->
|
||||
<BoxContainer Name="RecordContainerMedical" Orientation="Vertical" HorizontalExpand="True" Visible="False" >
|
||||
<records:RecordLongItemDisplay Name="RecordContainerAllergies" Title="{Loc 'humanoid-profile-editor-cd-records-allergies'}"/>
|
||||
<records:RecordLongItemDisplay Name="RecordContainerDrugAllergies" Title="{Loc 'humanoid-profile-editor-cd-records-drug-allergies'}"/>
|
||||
<records:RecordLongItemDisplay Name="RecordContainerPostmortem" Title="{Loc 'humanoid-profile-editor-cd-records-postmortem'}"/>
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<Label Text="{Loc 'cd-character-records-viewer-record-med-sex'}"/>
|
||||
<Control HorizontalExpand="True" />
|
||||
<Label Name="RecordContainerSex" Align="Right" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<!-- Security -->
|
||||
<BoxContainer Name="RecordContainerSecurity" Orientation="Vertical" HorizontalExpand="True" Visible="False" >
|
||||
<records:RecordLongItemDisplay Name="RecordContainerIdentFeatures" Title="{Loc 'humanoid-profile-editor-cd-records-identifying-features'}"/>
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<Label Text="{Loc 'cd-character-records-viewer-record-sec-fingerprint'}"/>
|
||||
<Control HorizontalExpand="True" />
|
||||
<Label Name="RecordContainerFingerprint" Align="Right" />
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<Label Text="{Loc 'cd-character-records-viewer-record-sec-dna'}"/>
|
||||
<Control HorizontalExpand="True" />
|
||||
<Label Name="RecordContainerDNA" Align="Right" />
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal" Margin="5 5 5 5">
|
||||
<Label Text="{Loc 'criminal-records-console-status'}" FontColorOverride="DarkGray"/>
|
||||
<OptionButton Name="StatusOptionButton"/>
|
||||
<Control MinWidth="5"/>
|
||||
<Label Name="RecordContainerWantedReason" Visible="False" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<!-- Entry viewer -->
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" Margin="10" SeparationOverride="5">
|
||||
<ItemList Name="RecordEntryList" HorizontalExpand="True" MinHeight="200" />
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Button Name="RecordEntryViewButton" Text="{Loc 'cd-character-records-viewer-view-entry'}"/>
|
||||
<!-- Admin console entry type selector -->
|
||||
<OptionButton Name="RecordEntryViewType" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
|
|
@ -0,0 +1,450 @@
|
|||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Security;
|
||||
using Content.Shared.StationRecords;
|
||||
using Content.Shared._CD.Records;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Client._CD.Records.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class CharacterRecordViewer : FancyWindow
|
||||
{
|
||||
public struct CharacterListMetadata
|
||||
{
|
||||
public uint CharacterRecordKey;
|
||||
public uint? StationRecordKey;
|
||||
}
|
||||
|
||||
public static readonly Color BackgroundColor = Color.FromHex("#25252a"); // Dark grey
|
||||
public static readonly Color ContentPanelColor = Color.FromHex("#1a1a1a"); // Darker grey for content areas
|
||||
public static readonly Color BorderColor = Color.FromHex("#404040"); // Light grey border
|
||||
public static readonly Color ErrorColor = Color.FromHex("#ff0000"); // Red for validation errors
|
||||
|
||||
public event Action<CharacterListMetadata?>? OnListingItemSelected;
|
||||
public event Action<StationRecordFilterType, string?>? OnFiltersChanged;
|
||||
|
||||
private bool _isPopulating;
|
||||
private StationRecordFilterType _filterType;
|
||||
|
||||
private RecordConsoleType? _type;
|
||||
|
||||
private readonly RecordEntryViewPopup _entryView = new();
|
||||
private List<PlayerProvidedCharacterRecords.RecordEntry>? _entries;
|
||||
|
||||
private DialogWindow? _wantedReasonDialog;
|
||||
|
||||
/// <summary>
|
||||
/// The key to the record of the currently selected item in the listing.
|
||||
/// </summary>
|
||||
private uint? _selectedListingKey;
|
||||
|
||||
/// <summary>
|
||||
/// The key to the record that is currently visible.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This may differ from <see cref="_selectedListingKey"/> because this contents has not been updated yet to reflect the new selection.
|
||||
/// </remarks>
|
||||
private uint? _openRecordKey;
|
||||
public event Action<SecurityStatus, string?>? OnSetSecurityStatus;
|
||||
|
||||
public uint? SecurityWantedStatusMaxLength;
|
||||
|
||||
public CharacterRecordViewer()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
// There is no reason why we can't just steal the StationRecordFilter class.
|
||||
// If wizden adds a new kind of filtering we want to replicate it here.
|
||||
foreach (var item in Enum.GetValues<StationRecordFilterType>())
|
||||
{
|
||||
RecordFilterType.AddItem(GetTypeFilterLocals(item), (int)item);
|
||||
}
|
||||
|
||||
// Again, if wizden changes something about Criminal Records, we want to replicate the
|
||||
// functionality here.
|
||||
foreach (var status in Enum.GetValues<SecurityStatus>())
|
||||
{
|
||||
var name = Loc.GetString($"criminal-records-status-{status.ToString().ToLower()}");
|
||||
StatusOptionButton.AddItem(name, (int)status);
|
||||
}
|
||||
|
||||
CharacterListing.OnItemSelected += _ =>
|
||||
{
|
||||
if (!CharacterListing.GetSelected().Any())
|
||||
return;
|
||||
var selected = CharacterListing.GetSelected().First();
|
||||
var meta = (CharacterListMetadata)selected.Metadata!;
|
||||
_selectedListingKey = meta.CharacterRecordKey;
|
||||
if (!_isPopulating)
|
||||
OnListingItemSelected?.Invoke(meta);
|
||||
};
|
||||
|
||||
CharacterListing.OnItemDeselected += _ =>
|
||||
{
|
||||
// When we populate the records, we clear the contents of the listing.
|
||||
// This could cause a deselection but we don't want to really deselect because it would
|
||||
// interrupt what the player is doing.
|
||||
if (!_isPopulating)
|
||||
OnListingItemSelected?.Invoke(null);
|
||||
_selectedListingKey = null;
|
||||
};
|
||||
|
||||
RecordFilters.OnPressed += _ =>
|
||||
{
|
||||
OnFiltersChanged?.Invoke(_filterType, RecordFiltersValue.Text);
|
||||
};
|
||||
|
||||
RecordFiltersReset.OnPressed += _ =>
|
||||
{
|
||||
OnFiltersChanged?.Invoke(StationRecordFilterType.Name, null);
|
||||
RecordFiltersValue.Clear();
|
||||
};
|
||||
|
||||
RecordFiltersValue.OnTextEntered += text =>
|
||||
{
|
||||
OnFiltersChanged?.Invoke(_filterType, text.Text);
|
||||
};
|
||||
|
||||
RecordFilterType.OnItemSelected += eventArgs =>
|
||||
{
|
||||
var type = (StationRecordFilterType)eventArgs.Id;
|
||||
_filterType = type;
|
||||
RecordFilterType.SelectId(eventArgs.Id);
|
||||
};
|
||||
|
||||
RecordEntryViewButton.OnPressed += _ =>
|
||||
{
|
||||
if (_entries == null || !RecordEntryList.GetSelected().Any())
|
||||
return;
|
||||
var idx = RecordEntryList.IndexOf(RecordEntryList.GetSelected().First());
|
||||
_entryView.SetContents(_entries[idx]);
|
||||
_entryView.Open();
|
||||
};
|
||||
|
||||
StatusOptionButton.OnItemSelected += args =>
|
||||
{
|
||||
var status = (SecurityStatus)args.Id;
|
||||
// This should reflect SetStatus in CriminalRecordsConsoleWindow.xaml.cs
|
||||
if (status == SecurityStatus.Wanted || status == SecurityStatus.Suspected)
|
||||
SetStatusWithReason(status);
|
||||
else
|
||||
OnSetSecurityStatus?.Invoke(status, null);
|
||||
};
|
||||
|
||||
OnClose += () => _entryView.Close();
|
||||
|
||||
// Admin console entry type selector
|
||||
RecordEntryViewType.AddItem(Loc.GetString("department-Security"));
|
||||
RecordEntryViewType.AddItem(Loc.GetString("department-Medical"));
|
||||
RecordEntryViewType.AddItem(Loc.GetString("humanoid-profile-editor-cd-records-employment"));
|
||||
RecordEntryViewType.OnItemSelected += args =>
|
||||
{
|
||||
if (args.Id == RecordEntryViewType.SelectedId)
|
||||
return;
|
||||
RecordEntryViewType.SelectId(args.Id);
|
||||
// This is a hack to get the server to send us another packet with the new entries
|
||||
OnFiltersChanged?.Invoke(_filterType, RecordFiltersValue.Text);
|
||||
};
|
||||
}
|
||||
|
||||
// If we are using wizden's class we might as well use their localization.
|
||||
private string GetTypeFilterLocals(StationRecordFilterType type)
|
||||
{
|
||||
return Loc.GetString($"general-station-record-{type.ToString().ToLower()}-filter");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Select the record in the listing for the given key.
|
||||
/// </summary>
|
||||
/// <param name="key">The index of the record in the dictionary</param>
|
||||
private void SelectRecordKey(uint? key)
|
||||
{
|
||||
if (_selectedListingKey == key)
|
||||
return;
|
||||
_selectedListingKey = key;
|
||||
|
||||
_isPopulating = true;
|
||||
|
||||
CharacterListing.ClearSelected();
|
||||
|
||||
// I wish there was a better way of doing this
|
||||
if (key != null)
|
||||
{
|
||||
foreach (var item in CharacterListing)
|
||||
{
|
||||
if (((CharacterListMetadata) item.Metadata!).CharacterRecordKey == key)
|
||||
{
|
||||
item.Selected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_isPopulating = false;
|
||||
}
|
||||
|
||||
private bool CharacterListNeedsRepopulating(IReadOnlyDictionary<uint, CharacterRecordConsoleState.CharacterInfo> newKeys)
|
||||
{
|
||||
var newCount = newKeys.Count;
|
||||
if (newCount != CharacterListing.Count)
|
||||
return true;
|
||||
|
||||
// Given that there is the same number of keys in the dictionary as in items in the listing, they are not equal
|
||||
// if and only if there exists a key in the listing that is not in the dictionary
|
||||
foreach (var item in CharacterListing)
|
||||
{
|
||||
var key = ((CharacterListMetadata)item.Metadata!).CharacterRecordKey;
|
||||
if (!newKeys.ContainsKey(key))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void UpdateState(CharacterRecordConsoleState state)
|
||||
{
|
||||
#region Visibility
|
||||
|
||||
RecordEntryViewType.Visible = false;
|
||||
_type = state.ConsoleType;
|
||||
|
||||
// Disable listing if we don't have one selected
|
||||
if (state.CharacterList == null)
|
||||
{
|
||||
CharacterListingStatus.Visible = true;
|
||||
CharacterListing.Visible = false;
|
||||
CharacterListingStatus.Text = Loc.GetString("cd-record-viewer-empty-state");
|
||||
RecordContainer.Visible = false;
|
||||
RecordContainerStatus.Visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
CharacterListingStatus.Visible = false;
|
||||
CharacterListing.Visible = true;
|
||||
|
||||
// Enable extended filtering only for admin and security consoles
|
||||
switch (_type)
|
||||
{
|
||||
case RecordConsoleType.Employment:
|
||||
RecordFilterType.Visible = false;
|
||||
RecordFilterType.SelectId((int)StationRecordFilterType.Name);
|
||||
|
||||
Title = Loc.GetString("cd-character-records-viewer-title-employ");
|
||||
break;
|
||||
case RecordConsoleType.Medical:
|
||||
RecordFilterType.Visible = false;
|
||||
RecordFilterType.SelectId((int)StationRecordFilterType.Name);
|
||||
|
||||
Title = Loc.GetString("cd-character-records-viewer-title-med");
|
||||
break;
|
||||
case RecordConsoleType.Security:
|
||||
RecordFilterType.Visible = true;
|
||||
|
||||
Title = Loc.GetString("cd-character-records-viewer-title-sec");
|
||||
break;
|
||||
case RecordConsoleType.Admin:
|
||||
RecordFilterType.Visible = true;
|
||||
Title = "Admin records console";
|
||||
RecordEntryViewType.Visible = true;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PopulateListing
|
||||
|
||||
if (state.Filter != null)
|
||||
{
|
||||
RecordFiltersValue.SetText(state.Filter.Value);
|
||||
RecordFilterType.SelectId((int) state.Filter.Type);
|
||||
}
|
||||
|
||||
if (CharacterListNeedsRepopulating(state.CharacterList))
|
||||
{
|
||||
_isPopulating = true;
|
||||
|
||||
CharacterListing.Clear();
|
||||
|
||||
// Add the records to the listing in a sorted order. There is probably are faster way of doing this, but
|
||||
// this is not really a hot code path.
|
||||
state.CharacterList
|
||||
// The items in this tuple are as follows: (name of character, CharacterListMetadata)
|
||||
.Select(r
|
||||
=> (CharacterName: r.Value.CharacterDisplayName, new CharacterListMetadata() { CharacterRecordKey = r.Key, StationRecordKey = r.Value.StationRecordKey}))
|
||||
.OrderBy(r => r.Item1)
|
||||
.ToList()
|
||||
.ForEach(r => CharacterListing.AddItem(r.Item1, metadata: r.Item2));
|
||||
|
||||
_isPopulating = false;
|
||||
}
|
||||
|
||||
SelectRecordKey(state.SelectedIndex);
|
||||
|
||||
#endregion
|
||||
|
||||
#region FillRecordContainer
|
||||
|
||||
// Enable container if we have a record selected
|
||||
if (state.SelectedRecord == null)
|
||||
{
|
||||
RecordContainerStatus.Visible = true;
|
||||
RecordContainer.Visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
RecordContainerStatus.Visible = false;
|
||||
RecordContainer.Visible = true;
|
||||
|
||||
// Do not needlessly reload the record if not needed. This is mainly done to prevent a bug in the admin record viewer.
|
||||
if (state.SelectedIndex == _openRecordKey)
|
||||
return;
|
||||
_openRecordKey = state.SelectedIndex;
|
||||
|
||||
var record = state.SelectedRecord!;
|
||||
var cr = record.PRecords;
|
||||
|
||||
// Basic info
|
||||
RecordContainerName.Text = record.Name;
|
||||
RecordContainerAge.Text = record.Age.ToString();
|
||||
RecordContainerJob.Text = record.JobTitle; /* At some point in the future we might want to display the icon */
|
||||
RecordContainerGender.Text = record.Gender.ToString();
|
||||
RecordContainerSpecies.Text = record.Species;
|
||||
RecordContainerHeight.Text = cr.Height + " " + UnitConversion.GetImperialDisplayLength(cr.Height);
|
||||
RecordContainerWeight.Text = cr.Weight + " " + UnitConversion.GetImperialDisplayMass(cr.Weight);
|
||||
RecordContainerContactName.SetValue(cr.EmergencyContactName);
|
||||
|
||||
RecordContainerEmployment.Visible = false;
|
||||
RecordContainerMedical.Visible = false;
|
||||
RecordContainerSecurity.Visible = false;
|
||||
|
||||
switch (_type)
|
||||
{
|
||||
case RecordConsoleType.Employment:
|
||||
SetEntries(cr.EmploymentEntries);
|
||||
UpdateRecordBoxEmployment(record);
|
||||
break;
|
||||
case RecordConsoleType.Medical:
|
||||
SetEntries(cr.MedicalEntries);
|
||||
UpdateRecordBoxMedical(record);
|
||||
break;
|
||||
case RecordConsoleType.Security:
|
||||
SetEntries(cr.SecurityEntries);
|
||||
UpdateRecordBoxSecurity(record, state.SelectedSecurityStatus);
|
||||
break;
|
||||
case RecordConsoleType.Admin:
|
||||
UpdateRecordBoxEmployment(record);
|
||||
UpdateRecordBoxMedical(record);
|
||||
UpdateRecordBoxSecurity(record, state.SelectedSecurityStatus);
|
||||
switch ((RecordConsoleType) RecordEntryViewType.SelectedId)
|
||||
{
|
||||
case RecordConsoleType.Employment:
|
||||
SetEntries(cr.EmploymentEntries, true);
|
||||
break;
|
||||
case RecordConsoleType.Medical:
|
||||
SetEntries(cr.MedicalEntries, true);
|
||||
break;
|
||||
case RecordConsoleType.Security:
|
||||
SetEntries(cr.SecurityEntries, true);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
|
||||
private void SetEntries(List<PlayerProvidedCharacterRecords.RecordEntry> entries, bool addIndex = false)
|
||||
{
|
||||
_entries = entries;
|
||||
RecordEntryList.Clear();
|
||||
var i = 0;
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
RecordEntryList.AddItem(addIndex ? $"({i.ToString()}) " + entry.Title : entry.Title);
|
||||
++i;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateRecordBoxEmployment(FullCharacterRecords record)
|
||||
{
|
||||
RecordContainerEmployment.Visible = true;
|
||||
RecordContainerWorkAuth.Text = record.PRecords.HasWorkAuthorization ? "yes" : "no";
|
||||
}
|
||||
|
||||
private void UpdateRecordBoxMedical(FullCharacterRecords record)
|
||||
{
|
||||
RecordContainerMedical.Visible = true;
|
||||
var cr = record.PRecords;
|
||||
RecordContainerMedical.Visible = true;
|
||||
RecordContainerAllergies.SetValue(cr.Allergies);
|
||||
RecordContainerDrugAllergies.SetValue(cr.DrugAllergies);
|
||||
RecordContainerPostmortem.SetValue(cr.PostmortemInstructions);
|
||||
RecordContainerSex.Text = record.Sex.ToString();
|
||||
}
|
||||
|
||||
private void UpdateRecordBoxSecurity(FullCharacterRecords record, (SecurityStatus, string?)? criminal)
|
||||
{
|
||||
RecordContainerSecurity.Visible = true;
|
||||
RecordContainerIdentFeatures.SetValue(record.PRecords.IdentifyingFeatures);
|
||||
RecordContainerFingerprint.Text = record.Fingerprint ?? Loc.GetString("cd-character-records-viewer-unknown");
|
||||
RecordContainerDNA.Text = record.DNA ?? Loc.GetString("cd-character-records-viewer-unknown");
|
||||
|
||||
RecordContainerWantedReason.Visible = false;
|
||||
if (criminal != null)
|
||||
{
|
||||
var (stat, reason) = criminal.Value;
|
||||
StatusOptionButton.Select((int)stat);
|
||||
RecordContainerWantedReason.Text = reason;
|
||||
RecordContainerWantedReason.Visible = reason != null;
|
||||
}
|
||||
}
|
||||
|
||||
// This is copied almost verbatim from CriminalRecordsConsoleWindow.xaml.cs
|
||||
private void SetStatusWithReason(SecurityStatus status)
|
||||
{
|
||||
if (_wantedReasonDialog != null)
|
||||
{
|
||||
_wantedReasonDialog.MoveToFront();
|
||||
return;
|
||||
}
|
||||
|
||||
const string field = "reason";
|
||||
var title = Loc.GetString("criminal-records-status-" + status.ToString().ToLower());
|
||||
var placeholder = Loc.GetString("cd-character-records-viewer-setwanted-placeholder");
|
||||
var prompt = Loc.GetString("criminal-records-console-reason");
|
||||
var entry = new QuickDialogEntry(field, QuickDialogEntryType.LongText, prompt, placeholder);
|
||||
var entries = new List<QuickDialogEntry>() { entry };
|
||||
_wantedReasonDialog = new DialogWindow(title, entries);
|
||||
|
||||
_wantedReasonDialog.OnConfirmed += responses =>
|
||||
{
|
||||
var reason = responses[field];
|
||||
if (reason.Length < 1 || reason.Length > SecurityWantedStatusMaxLength)
|
||||
return;
|
||||
|
||||
OnSetSecurityStatus?.Invoke(status, reason);
|
||||
};
|
||||
|
||||
_wantedReasonDialog.OnClose += () => { _wantedReasonDialog = null; };
|
||||
}
|
||||
public bool IsSecurity()
|
||||
{
|
||||
return _type == RecordConsoleType.Security || _type == RecordConsoleType.Admin;
|
||||
}
|
||||
|
||||
public void SetSecurityStatusEnabled(bool setting)
|
||||
{
|
||||
for (var i = 0; i < StatusOptionButton.ItemCount; ++i)
|
||||
{
|
||||
StatusOptionButton.SetItemDisabled(i, !setting);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<Control xmlns="https://spacestation14.io">
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
|
||||
<ItemList Name="EntrySelector" HorizontalExpand="True" VerticalExpand="True" MinSize="300 200" />
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Button Name="AddButton" StyleClasses="OpenBoth" Text="{Loc 'humanoid-profile-editor-cd-records-add-entry'}" />
|
||||
<Button Name="EditButton" StyleClasses="OpenBoth" Text="{Loc 'humanoid-profile-editor-cd-records-edit-entry'}" />
|
||||
<Button Name="ViewButton" StyleClasses="OpenBoth" Text="{Loc 'humanoid-profile-editor-cd-records-view-entry'}" />
|
||||
<Button Name="RemoveButton" StyleClasses="OpenLeft" Text="{Loc 'humanoid-profile-editor-cd-records-remove-entry'}" />
|
||||
<Control HorizontalExpand="True" />
|
||||
<Button Name="UpButton" HorizontalAlignment="Right" StyleClasses="OpenRight" Text="{Loc 'humanoid-profile-editor-cd-records-up'}" />
|
||||
<Button Name="DownButton" HorizontalAlignment="Right" StyleClasses="OpenBoth" Text="{Loc 'humanoid-profile-editor-cd-records-down'}" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
using Content.Shared._CD.Records;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Client.UserInterface;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Client._CD.Records.UI;
|
||||
|
||||
/// <summary>
|
||||
/// The box that contains the list of entities in the record editor. We create one for each record type
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class RecordEditorEntrySelector : Control
|
||||
{
|
||||
private List<PlayerProvidedCharacterRecords.RecordEntry> _entries = new();
|
||||
|
||||
public event Action<RecordEditorEntryUpdateArgs>? OnUpdateEntries;
|
||||
|
||||
private readonly RecordEntryEditPopup _editPopup = new();
|
||||
private readonly RecordEntryViewPopup _entryViewPopup = new();
|
||||
private int _editIdx;
|
||||
|
||||
public RecordEditorEntrySelector()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
AddButton.OnPressed += _ =>
|
||||
{
|
||||
_editIdx = _entries.Count;
|
||||
_editPopup.SetContents(new PlayerProvidedCharacterRecords.RecordEntry("", "", ""));
|
||||
_editPopup.Open();
|
||||
};
|
||||
|
||||
EditButton.OnPressed += _ =>
|
||||
{
|
||||
if (!EntrySelector.GetSelected().Any())
|
||||
return;
|
||||
_editIdx = EntrySelector.IndexOf(EntrySelector.GetSelected().First());
|
||||
_editPopup.SetContents(_entries[_editIdx]);
|
||||
_editPopup.Open();
|
||||
};
|
||||
|
||||
ViewButton.OnPressed += _ =>
|
||||
{
|
||||
if (!EntrySelector.GetSelected().Any())
|
||||
return;
|
||||
var idx = EntrySelector.IndexOf(EntrySelector.GetSelected().First());
|
||||
_entryViewPopup.SetContents(_entries[idx]);
|
||||
_entryViewPopup.Open();
|
||||
};
|
||||
|
||||
RemoveButton.OnPressed += _ =>
|
||||
{
|
||||
if (!EntrySelector.GetSelected().Any())
|
||||
return;
|
||||
// Remove the entry, being careful to set the index correctly
|
||||
var idx = EntrySelector.IndexOf(EntrySelector.GetSelected().First());
|
||||
EntrySelector.RemoveAt(idx);
|
||||
_entries.RemoveAt(idx);
|
||||
if (idx == _editIdx)
|
||||
_editPopup.Close();
|
||||
_editIdx--;
|
||||
OnUpdateEntries?.Invoke(new RecordEditorEntryUpdateArgs(_entries));
|
||||
};
|
||||
|
||||
UpButton.OnPressed += _ =>
|
||||
{
|
||||
if (!EntrySelector.GetSelected().Any())
|
||||
return;
|
||||
var idx = EntrySelector.IndexOf(EntrySelector.GetSelected().First());
|
||||
if (idx < 1)
|
||||
return;
|
||||
(_entries[idx], _entries[idx - 1]) = (_entries[idx - 1], _entries[idx]);
|
||||
(EntrySelector[idx], EntrySelector[idx - 1]) = (EntrySelector[idx - 1], EntrySelector[idx]);
|
||||
OnUpdateEntries?.Invoke(new RecordEditorEntryUpdateArgs(_entries));
|
||||
};
|
||||
|
||||
DownButton.OnPressed += _ =>
|
||||
{
|
||||
if (!EntrySelector.GetSelected().Any())
|
||||
return;
|
||||
var idx = EntrySelector.IndexOf(EntrySelector.GetSelected().First());
|
||||
if (idx >= EntrySelector.Count - 1)
|
||||
return;
|
||||
(_entries[idx], _entries[idx + 1]) = (_entries[idx + 1], _entries[idx]);
|
||||
(EntrySelector[idx], EntrySelector[idx + 1]) = (EntrySelector[idx + 1], EntrySelector[idx]);
|
||||
OnUpdateEntries?.Invoke(new RecordEditorEntryUpdateArgs(_entries));
|
||||
};
|
||||
|
||||
_editPopup.SaveButton.OnPressed += _ =>
|
||||
{
|
||||
if (_editIdx >= _entries.Count)
|
||||
{
|
||||
var rec = _editPopup.GetContents();
|
||||
_entries.Add(rec);
|
||||
EntrySelector.AddItem(rec.Title);
|
||||
}
|
||||
else
|
||||
{
|
||||
_entries[_editIdx] = _editPopup.GetContents();
|
||||
EntrySelector[_editIdx].Text = _entries[_editIdx].Title;
|
||||
}
|
||||
OnUpdateEntries?.Invoke(new RecordEditorEntryUpdateArgs(_entries));
|
||||
};
|
||||
OnVisibilityChanged += _ =>
|
||||
{
|
||||
_editPopup.Close();
|
||||
};
|
||||
}
|
||||
|
||||
public void UpdateContents(List<PlayerProvidedCharacterRecords.RecordEntry> entries)
|
||||
{
|
||||
_entries = entries;
|
||||
RefreshSelector();
|
||||
}
|
||||
|
||||
private void RefreshSelector()
|
||||
{
|
||||
EntrySelector.Clear();
|
||||
foreach (var entry in _entries)
|
||||
{
|
||||
EntrySelector.AddItem(entry.Title);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class RecordEditorEntryUpdateArgs
|
||||
{
|
||||
public List<PlayerProvidedCharacterRecords.RecordEntry> Entries { get; private set; }
|
||||
|
||||
public RecordEditorEntryUpdateArgs(List<PlayerProvidedCharacterRecords.RecordEntry> entries)
|
||||
{
|
||||
Entries = entries;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
<Control xmlns="https://spacestation14.io"
|
||||
xmlns:cdrecords="clr-namespace:Content.Client._CD.Records.UI">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<ScrollContainer VerticalExpand="True">
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" Margin="10">
|
||||
<!-- Height, Weight -->
|
||||
<GridContainer Columns="2">
|
||||
<BoxContainer HorizontalExpand="True" SeparationOverride="2">
|
||||
<Label Text="{Loc 'humanoid-profile-editor-cd-records-height'}" />
|
||||
<Control HorizontalExpand="True" MinSize="5 0" />
|
||||
<LineEdit Name="HeightEdit" HorizontalAlignment="Right" MinSize="60 0" />
|
||||
<Label Name="HeightImperialLabel" MinWidth="60" />
|
||||
</BoxContainer>
|
||||
<BoxContainer HorizontalExpand="True" SeparationOverride="2">
|
||||
<Label Text="{Loc 'humanoid-profile-editor-cd-records-weight'}" />
|
||||
<Control HorizontalExpand="True" MinSize="5 0" />
|
||||
<LineEdit Name="WeightEdit" HorizontalAlignment="Right" MinSize="60 0" />
|
||||
<Label Name="WeightImperialLabel" MinWidth="70"/>
|
||||
</BoxContainer>
|
||||
</GridContainer>
|
||||
<!-- Emergency Contact -->
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc 'humanoid-profile-editor-cd-records-contact-name'}" />
|
||||
<Control HorizontalExpand="True" />
|
||||
<LineEdit Name="ContactNameEdit" HorizontalAlignment="Right" MinSize="350 0"/>
|
||||
</BoxContainer>
|
||||
<Control MinSize="0 20"/>
|
||||
<!-- Employment stuff -->
|
||||
<BoxContainer HorizontalExpand="True">
|
||||
<Label Text="{Loc 'humanoid-profile-editor-cd-records-work-authorization'}" />
|
||||
<Control HorizontalExpand="True" MinSize="5 0" />
|
||||
<CheckBox Name="WorkAuthCheckBox" HorizontalAlignment="Right"/>
|
||||
</BoxContainer>
|
||||
<Control MinSize="0 20"/>
|
||||
<!-- Security stuff -->
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc 'humanoid-profile-editor-cd-records-identifying-features'}" />
|
||||
<Control HorizontalExpand="True" />
|
||||
<LineEdit Name="IdentifyingFeaturesEdit" HorizontalAlignment="Right" MinSize="350 0"/>
|
||||
</BoxContainer>
|
||||
<Control MinSize="0 20"/>
|
||||
<!-- Medical stuff -->
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc 'humanoid-profile-editor-cd-records-allergies'}" />
|
||||
<Control HorizontalExpand="True" />
|
||||
<LineEdit Name="AllergiesEdit" HorizontalAlignment="Right" MinSize="350 0"/>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc 'humanoid-profile-editor-cd-records-drug-allergies'}" />
|
||||
<Control HorizontalExpand="True" />
|
||||
<LineEdit Name="DrugAllergiesEdit" HorizontalAlignment="Right" MinSize="350 0"/>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc 'humanoid-profile-editor-cd-records-postmortem'}" />
|
||||
<Control HorizontalExpand="True" />
|
||||
<LineEdit Name="PostmortemEdit" HorizontalAlignment="Right" MinSize="350 0"/>
|
||||
</BoxContainer>
|
||||
<Control MinSize="0 20"/>
|
||||
<!-- Entry editor -->
|
||||
<TabContainer Name="EntryEditorTabs" VerticalExpand="True" HorizontalExpand="True" Margin="10">
|
||||
<cdrecords:RecordEditorEntrySelector Name="EmploymentEntrySelector"/>
|
||||
<cdrecords:RecordEditorEntrySelector Name="MedicalEntrySelector"/>
|
||||
<cdrecords:RecordEditorEntrySelector Name="SecurityEntrySelector"/>
|
||||
</TabContainer>
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
using Content.Shared.Preferences;
|
||||
using Content.Shared._CD.Records;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client._CD.Records.UI;
|
||||
|
||||
/// <summary>
|
||||
/// The record editor tab that gets "injected" into the character editor.
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class RecordEditorGui : Control
|
||||
{
|
||||
/// <summary>
|
||||
/// Delegate that tells the editor to save records when the save button is pressed
|
||||
/// </summary>
|
||||
private readonly Action<PlayerProvidedCharacterRecords> _updateProfileRecords;
|
||||
private PlayerProvidedCharacterRecords _records = default!;
|
||||
|
||||
public RecordEditorGui(Action<PlayerProvidedCharacterRecords> updateProfileRecords)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
_updateProfileRecords = updateProfileRecords;
|
||||
|
||||
#region General
|
||||
|
||||
HeightEdit.OnTextChanged += args =>
|
||||
{
|
||||
if (!int.TryParse(args.Text, out var newHeight))
|
||||
return;
|
||||
UpdateImperialHeight(newHeight);
|
||||
UpdateRecords(_records.WithHeight(newHeight));
|
||||
};
|
||||
|
||||
WeightEdit.OnTextChanged += args =>
|
||||
{
|
||||
if (!int.TryParse(args.Text, out var newWeight))
|
||||
return;
|
||||
UpdateImperialWeight(newWeight);
|
||||
UpdateRecords(_records.WithWeight(newWeight));
|
||||
};
|
||||
|
||||
ContactNameEdit.OnTextChanged += args =>
|
||||
{
|
||||
UpdateRecords(_records.WithContactName(args.Text));
|
||||
};
|
||||
|
||||
#endregion
|
||||
|
||||
#region Employment
|
||||
|
||||
WorkAuthCheckBox.OnToggled += args =>
|
||||
{
|
||||
UpdateRecords(_records.WithWorkAuth(args.Pressed));
|
||||
};
|
||||
|
||||
#endregion
|
||||
|
||||
#region Security
|
||||
|
||||
IdentifyingFeaturesEdit.OnTextChanged += args =>
|
||||
{
|
||||
UpdateRecords(_records.WithIdentifyingFeatures(args.Text));
|
||||
};
|
||||
|
||||
#endregion
|
||||
|
||||
#region Medical
|
||||
|
||||
AllergiesEdit.OnTextChanged += args =>
|
||||
{
|
||||
UpdateRecords(_records.WithAllergies(args.Text));
|
||||
};
|
||||
|
||||
DrugAllergiesEdit.OnTextChanged += args =>
|
||||
{
|
||||
UpdateRecords(_records.WithDrugAllergies(args.Text));
|
||||
};
|
||||
|
||||
PostmortemEdit.OnTextChanged += args =>
|
||||
{
|
||||
UpdateRecords(_records.WithPostmortemInstructions(args.Text));
|
||||
};
|
||||
|
||||
#endregion
|
||||
|
||||
#region Entries
|
||||
|
||||
EntryEditorTabs.SetTabTitle(0, Loc.GetString("humanoid-profile-editor-cd-records-employment"));
|
||||
EntryEditorTabs.SetTabTitle(1, Loc.GetString("department-Medical"));
|
||||
EntryEditorTabs.SetTabTitle(2, Loc.GetString("department-Security"));
|
||||
|
||||
EmploymentEntrySelector.OnUpdateEntries += args =>
|
||||
{
|
||||
UpdateRecords(_records.WithEmploymentEntries(args.Entries));
|
||||
};
|
||||
|
||||
MedicalEntrySelector.OnUpdateEntries += args =>
|
||||
{
|
||||
UpdateRecords(_records.WithMedicalEntries(args.Entries));
|
||||
};
|
||||
|
||||
SecurityEntrySelector.OnUpdateEntries += args =>
|
||||
{
|
||||
UpdateRecords(_records.WithSecurityEntries(args.Entries));
|
||||
};
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public void Update(HumanoidCharacterProfile? profile)
|
||||
{
|
||||
_records = profile?.CDCharacterRecords ?? PlayerProvidedCharacterRecords.DefaultRecords();
|
||||
EmploymentEntrySelector.UpdateContents(_records.EmploymentEntries);
|
||||
MedicalEntrySelector.UpdateContents(_records.MedicalEntries);
|
||||
SecurityEntrySelector.UpdateContents(_records.SecurityEntries);
|
||||
UpdateWidgets();
|
||||
}
|
||||
|
||||
private void UpdateRecords(PlayerProvidedCharacterRecords records)
|
||||
{
|
||||
records.EnsureValid();
|
||||
_records = records;
|
||||
_updateProfileRecords(_records);
|
||||
UpdateWidgets();
|
||||
}
|
||||
|
||||
private void UpdateWidgets()
|
||||
{
|
||||
HeightEdit.SetText(_records.Height.ToString());
|
||||
UpdateImperialHeight(_records.Height);
|
||||
WeightEdit.SetText(_records.Weight.ToString());
|
||||
UpdateImperialWeight(_records.Weight);
|
||||
ContactNameEdit.SetText(_records.EmergencyContactName);
|
||||
|
||||
WorkAuthCheckBox.Pressed = _records.HasWorkAuthorization;
|
||||
|
||||
IdentifyingFeaturesEdit.SetText(_records.IdentifyingFeatures);
|
||||
|
||||
AllergiesEdit.SetText(_records.Allergies);
|
||||
DrugAllergiesEdit.SetText(_records.DrugAllergies);
|
||||
PostmortemEdit.SetText(_records.PostmortemInstructions);
|
||||
}
|
||||
|
||||
private void UpdateImperialHeight(int newHeight)
|
||||
{
|
||||
HeightImperialLabel.Text = UnitConversion.GetImperialDisplayLength(newHeight);
|
||||
}
|
||||
|
||||
private void UpdateImperialWeight(int newWeight)
|
||||
{
|
||||
WeightImperialLabel.Text = UnitConversion.GetImperialDisplayMass(newWeight);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
MinSize="500 700"
|
||||
SetSize="600 700"
|
||||
Title="{Loc 'cd-records-entry-edit-popup-title'}">
|
||||
<BoxContainer Margin="5 2 5 5" Orientation="Vertical" HorizontalExpand="True" SeparationOverride="2">
|
||||
<!-- Header section -->
|
||||
<PanelContainer StyleClasses="AngleRect">
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" Margin="4" SeparationOverride="8">
|
||||
<LineEdit Name="TitleEdit" HorizontalExpand="True" PlaceHolder="{Loc 'cd-records-entry-edit-popup-title-placeholder'}"/>
|
||||
<LineEdit Name="InvolvedEdit" HorizontalExpand="True" PlaceHolder="{Loc 'cd-records-entry-edit-popup-involved-placeholder'}"/>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
|
||||
<!-- Content section with styled background -->
|
||||
<PanelContainer Name="ContentPanel" VerticalExpand="True">
|
||||
<TextEdit Name="DescriptionEdit"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
Margin="8 4 8 8"/>
|
||||
</PanelContainer>
|
||||
|
||||
<!-- Save button and validation section -->
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" SeparationOverride="4">
|
||||
<PanelContainer StyleClasses="AngleRect">
|
||||
<Button Access="Public"
|
||||
Name="SaveButton"
|
||||
Text="{Loc 'cd-records-entry-edit-popup-save'}"
|
||||
Margin="4"/>
|
||||
</PanelContainer>
|
||||
<Label Name="ValidationLabel"
|
||||
HorizontalExpand="True"
|
||||
HorizontalAlignment="Center"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared._CD.Records;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Client._CD.Records.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class RecordEntryEditPopup : FancyWindow
|
||||
{
|
||||
private bool _isValid;
|
||||
|
||||
private record struct ValidationRule(
|
||||
Func<bool> IsInvalid,
|
||||
string LocalizationKey,
|
||||
(string, object)[]? Parameters = null);
|
||||
|
||||
public RecordEntryEditPopup()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
ValidationLabel.FontColorOverride = CharacterRecordViewer.ErrorColor;
|
||||
ValidationLabel.Visible = false;
|
||||
|
||||
// Style the content panel
|
||||
var styleBox = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = CharacterRecordViewer.ContentPanelColor,
|
||||
BorderColor = CharacterRecordViewer.BorderColor,
|
||||
BorderThickness = new Thickness(1),
|
||||
};
|
||||
|
||||
ContentPanel.PanelOverride = styleBox;
|
||||
|
||||
SetupEventHandlers();
|
||||
}
|
||||
|
||||
private void SetupEventHandlers()
|
||||
{
|
||||
TitleEdit.OnTextChanged += args =>
|
||||
{
|
||||
if (args.Text.Length > PlayerProvidedCharacterRecords.TextMedLen)
|
||||
{
|
||||
TitleEdit.Text = args.Text[..PlayerProvidedCharacterRecords.TextMedLen];
|
||||
}
|
||||
ValidateFields();
|
||||
};
|
||||
|
||||
InvolvedEdit.OnTextChanged += args =>
|
||||
{
|
||||
if (args.Text.Length > PlayerProvidedCharacterRecords.TextMedLen)
|
||||
{
|
||||
InvolvedEdit.Text = args.Text[..PlayerProvidedCharacterRecords.TextMedLen];
|
||||
}
|
||||
ValidateFields();
|
||||
};
|
||||
|
||||
DescriptionEdit.OnTextChanged += _ => ValidateFields();
|
||||
|
||||
SaveButton.OnPressed += _ =>
|
||||
{
|
||||
if (!_isValid)
|
||||
return;
|
||||
Close();
|
||||
};
|
||||
|
||||
DescriptionEdit.Placeholder =
|
||||
new Rope.Leaf(Loc.GetString("cd-records-entry-edit-popup-description-placeholder"));
|
||||
|
||||
TitleEdit.PlaceHolder = Loc.GetString("cd-records-entry-edit-popup-title-placeholder");
|
||||
InvolvedEdit.PlaceHolder = Loc.GetString("cd-records-entry-edit-popup-involved-placeholder");
|
||||
}
|
||||
|
||||
private void ValidateFields()
|
||||
{
|
||||
var descriptionText = Rope.Collapse(DescriptionEdit.TextRope);
|
||||
var descriptionLength = descriptionText.Length;
|
||||
|
||||
// Validation rules in priority order
|
||||
// Overcomplicated into oblivion just because I didn't like how the else if statements looked
|
||||
var rules = new[]
|
||||
{
|
||||
new ValidationRule(
|
||||
() => string.IsNullOrWhiteSpace(TitleEdit.Text),
|
||||
"cd-records-entry-edit-popup-title-required"),
|
||||
|
||||
new ValidationRule(
|
||||
() => string.IsNullOrWhiteSpace(InvolvedEdit.Text),
|
||||
"cd-records-entry-edit-popup-involved-required"),
|
||||
|
||||
new ValidationRule(
|
||||
() => string.IsNullOrWhiteSpace(descriptionText),
|
||||
"cd-records-entry-edit-popup-description-required"),
|
||||
|
||||
new ValidationRule(
|
||||
() => descriptionLength > PlayerProvidedCharacterRecords.TextVeryLargeLen,
|
||||
"cd-records-entry-edit-popup-description-too-long",
|
||||
new[]
|
||||
{
|
||||
("current", (object)descriptionLength),
|
||||
("max", PlayerProvidedCharacterRecords.TextVeryLargeLen)
|
||||
}),
|
||||
};
|
||||
|
||||
// Find first failing validation rule
|
||||
var failedRule = rules.FirstOrDefault(rule => rule.IsInvalid());
|
||||
|
||||
if (failedRule.IsInvalid != null)
|
||||
{
|
||||
ValidationLabel.Text = failedRule.Parameters != null
|
||||
? Loc.GetString(failedRule.LocalizationKey, failedRule.Parameters)
|
||||
: Loc.GetString(failedRule.LocalizationKey);
|
||||
ValidationLabel.Visible = true;
|
||||
_isValid = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
ValidationLabel.Visible = false;
|
||||
_isValid = true;
|
||||
}
|
||||
|
||||
SaveButton.Disabled = !_isValid;
|
||||
}
|
||||
|
||||
public PlayerProvidedCharacterRecords.RecordEntry GetContents()
|
||||
{
|
||||
var desc = Rope.Collapse(DescriptionEdit.TextRope).Trim();
|
||||
return new PlayerProvidedCharacterRecords.RecordEntry(
|
||||
TitleEdit.Text.Trim(),
|
||||
InvolvedEdit.Text.Trim(),
|
||||
desc);
|
||||
}
|
||||
|
||||
public void SetContents(PlayerProvidedCharacterRecords.RecordEntry entry)
|
||||
{
|
||||
TitleEdit.Text = entry.Title;
|
||||
InvolvedEdit.Text = entry.Involved;
|
||||
DescriptionEdit.TextRope = new Rope.Leaf(entry.Description);
|
||||
ValidateFields();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
MinSize="500 700"
|
||||
SetSize="600 700"
|
||||
Title="{Loc 'cd-records-entry-view-popup-title'}">
|
||||
<BoxContainer Margin="5 2 5 5" Orientation="Vertical" HorizontalExpand="True" SeparationOverride="2">
|
||||
<!-- Header section -->
|
||||
<PanelContainer StyleClasses="AngleRect">
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" Margin="4">
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<Label Text="{Loc 'cd-character-records-entry-view-title'}" StyleClasses="LabelBig"/>
|
||||
<Control MinWidth="5"/>
|
||||
<RichTextLabel HorizontalAlignment="Right" Name="EntryTitle" HorizontalExpand="True" StyleClasses="LabelBig" />
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<Label Text="{Loc 'cd-character-records-entry-view-involved'}" StyleClasses="LabelBig"/>
|
||||
<Control MinWidth="5"/>
|
||||
<RichTextLabel HorizontalAlignment="Right" Name="EntryInvolved" HorizontalExpand="True" StyleClasses="LabelBig"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
|
||||
<!-- Content section with styled background -->
|
||||
<PanelContainer Name="ContentPanel" VerticalExpand="True">
|
||||
<ScrollContainer HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
HScrollEnabled="False">
|
||||
<BoxContainer Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
Margin="8 4 8 8">
|
||||
<RichTextLabel Name="EntryDesc"
|
||||
HorizontalExpand="True"
|
||||
Margin="4"/>
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared._CD.Records;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.RichText;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client._CD.Records.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class RecordEntryViewPopup : FancyWindow
|
||||
{
|
||||
// Font tags bad
|
||||
private static readonly Type[] AllowedTags =
|
||||
[
|
||||
typeof(BoldItalicTag),
|
||||
typeof(BoldTag),
|
||||
typeof(BulletTag),
|
||||
typeof(ColorTag),
|
||||
typeof(HeadingTag),
|
||||
typeof(ItalicTag),
|
||||
];
|
||||
|
||||
public RecordEntryViewPopup()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
// Create a styled box for the content panel
|
||||
var styleBox = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = CharacterRecordViewer.ContentPanelColor,
|
||||
BorderColor = CharacterRecordViewer.BorderColor,
|
||||
BorderThickness = new Thickness(1),
|
||||
};
|
||||
|
||||
ContentPanel.PanelOverride = styleBox;
|
||||
}
|
||||
|
||||
public void SetContents(PlayerProvidedCharacterRecords.RecordEntry entry)
|
||||
{
|
||||
EntryTitle.SetMessage(entry.Title);
|
||||
EntryInvolved.SetMessage(entry.Involved);
|
||||
EntryDesc.SetMessage(FormattedMessage.FromMarkupPermissive(entry.Description.Trim()), AllowedTags);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
namespace Content.Client._CD.Records.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Widget that displays the record on one line if it is short enough, and on two lines if it is
|
||||
/// too long. This should only be used if you know the length may be long enough to break things when
|
||||
/// using a normal Label.
|
||||
/// </summary>
|
||||
public sealed class RecordLongItemDisplay : BoxContainer
|
||||
{
|
||||
private const int MaxShortLength = 32;
|
||||
|
||||
public string? Title
|
||||
{
|
||||
get => _titleLabel.Text;
|
||||
set => _titleLabel.Text = value;
|
||||
}
|
||||
|
||||
// Row containing the title and short value
|
||||
private readonly BoxContainer _firstRow = new()
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
HorizontalExpand = true
|
||||
};
|
||||
// Row containing the long value
|
||||
private readonly BoxContainer _secondRow = new()
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
HorizontalExpand = true,
|
||||
Visible = false,
|
||||
};
|
||||
private readonly Label _titleLabel = new();
|
||||
private readonly Label _shortContents = new() { Visible = true, Align = Label.AlignMode.Right };
|
||||
private readonly RichTextLabel _longContents = new() { HorizontalExpand = true };
|
||||
|
||||
public RecordLongItemDisplay()
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical;
|
||||
_firstRow.AddChild(_titleLabel);
|
||||
_firstRow.AddChild(new Control() { HorizontalExpand = true });
|
||||
_firstRow.AddChild(_shortContents);
|
||||
AddChild(_firstRow);
|
||||
_secondRow.AddChild(new Control() { HorizontalExpand = true, SizeFlagsStretchRatio = 0.15f});
|
||||
_secondRow.AddChild(_longContents);
|
||||
AddChild(_secondRow);
|
||||
}
|
||||
|
||||
public void SetValue(string s)
|
||||
{
|
||||
if (s.Length > MaxShortLength)
|
||||
{
|
||||
_longContents.SetMessage(s);
|
||||
_secondRow.Visible = true;
|
||||
_shortContents.Visible = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_shortContents.Text = s;
|
||||
_shortContents.Visible = true;
|
||||
_secondRow.Visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
namespace Content.Client._CD.Records.UI;
|
||||
|
||||
public static class UnitConversion
|
||||
{
|
||||
public static string GetImperialDisplayLength(int lengthCm)
|
||||
{
|
||||
var heightIn = (int) Math.Round(lengthCm * 0.3937007874 /* cm to in*/);
|
||||
return $"({heightIn / 12}'{heightIn % 12}'')";
|
||||
}
|
||||
|
||||
public static string GetImperialDisplayMass(int massKg)
|
||||
{
|
||||
var weightLbs = (int) Math.Round(massKg * 2.2046226218 /* kg to lbs */);
|
||||
return $"({weightLbs} lbs)";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
// File to store as much CD related database things outside of Model.cs
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Content.Server.Database;
|
||||
|
||||
public static class CDModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores CD Character data separately from the main Profile. This is done to work around a bug
|
||||
/// in EFCore migrations.
|
||||
/// <p />
|
||||
/// There is no way of forcing a dependent table to exist in EFCore (according to MS).
|
||||
/// You must always account for the possibility of this table not existing.
|
||||
/// </summary>
|
||||
public class CDProfile
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public int ProfileId { get; set; }
|
||||
public Profile Profile { get; set; } = null!;
|
||||
|
||||
public float Height { get; set; } = 1f;
|
||||
|
||||
[Column("character_records", TypeName = "jsonb")]
|
||||
public JsonDocument? CharacterRecords { get; set; }
|
||||
|
||||
public List<CharacterRecordEntry> CharacterRecordEntries { get; set; } = new();
|
||||
|
||||
}
|
||||
public enum DbRecordEntryType : byte
|
||||
{
|
||||
Medical = 0, Security = 1, Employment = 2
|
||||
}
|
||||
|
||||
[Table("cd_character_record_entries"), Index(nameof(Id))]
|
||||
public sealed class CharacterRecordEntry
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string Title { get; set; } = null!;
|
||||
|
||||
public string Involved { get; set; } = null!;
|
||||
|
||||
public string Description { get; set; } = null!;
|
||||
|
||||
public DbRecordEntryType Type { get; set; }
|
||||
|
||||
public int CDProfileId { get; set; }
|
||||
public CDProfile CDProfile { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
2216
Content.Server.Database/Migrations/Postgres/20250303043202_CosmaticDriftCharacterRecords.Designer.cs
generated
Normal file
2216
Content.Server.Database/Migrations/Postgres/20250303043202_CosmaticDriftCharacterRecords.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,86 @@
|
|||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Content.Server.Database.Migrations.Postgres
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class CosmaticDriftCharacterRecords : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "cdprofile",
|
||||
columns: table => new
|
||||
{
|
||||
cdprofile_id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
profile_id = table.Column<int>(type: "integer", nullable: false),
|
||||
height = table.Column<float>(type: "real", nullable: false),
|
||||
character_records = table.Column<JsonDocument>(type: "jsonb", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_cdprofile", x => x.cdprofile_id);
|
||||
table.ForeignKey(
|
||||
name: "FK_cdprofile_profile_profile_id",
|
||||
column: x => x.profile_id,
|
||||
principalTable: "profile",
|
||||
principalColumn: "profile_id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "cd_character_record_entries",
|
||||
columns: table => new
|
||||
{
|
||||
cd_character_record_entries_id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
title = table.Column<string>(type: "text", nullable: false),
|
||||
involved = table.Column<string>(type: "text", nullable: false),
|
||||
description = table.Column<string>(type: "text", nullable: false),
|
||||
type = table.Column<byte>(type: "smallint", nullable: false),
|
||||
cdprofile_id = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_cd_character_record_entries", x => x.cd_character_record_entries_id);
|
||||
table.ForeignKey(
|
||||
name: "FK_cd_character_record_entries_cdprofile_cdprofile_id",
|
||||
column: x => x.cdprofile_id,
|
||||
principalTable: "cdprofile",
|
||||
principalColumn: "cdprofile_id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_cd_character_record_entries_cd_character_record_entries_id",
|
||||
table: "cd_character_record_entries",
|
||||
column: "cd_character_record_entries_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_cd_character_record_entries_cdprofile_id",
|
||||
table: "cd_character_record_entries",
|
||||
column: "cdprofile_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_cdprofile_profile_id",
|
||||
table: "cdprofile",
|
||||
column: "profile_id",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "cd_character_record_entries");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "cdprofile");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -578,6 +578,79 @@ namespace Content.Server.Database.Migrations.Postgres
|
|||
b.ToTable("blacklist", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.CDModel+CDProfile", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("cdprofile_id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<JsonDocument>("CharacterRecords")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("character_records");
|
||||
|
||||
b.Property<float>("Height")
|
||||
.HasColumnType("real")
|
||||
.HasColumnName("height");
|
||||
|
||||
b.Property<int>("ProfileId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("profile_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("PK_cdprofile");
|
||||
|
||||
b.HasIndex("ProfileId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("cdprofile", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.CDModel+CharacterRecordEntry", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("cd_character_record_entries_id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("CDProfileId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("cdprofile_id");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<string>("Involved")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("involved");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.Property<byte>("Type")
|
||||
.HasColumnType("smallint")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("PK_cd_character_record_entries");
|
||||
|
||||
b.HasIndex("CDProfileId");
|
||||
|
||||
b.HasIndex("Id")
|
||||
.HasDatabaseName("IX_cd_character_record_entries_cd_character_record_entries_id");
|
||||
|
||||
b.ToTable("cd_character_record_entries", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
|
@ -1656,6 +1729,30 @@ namespace Content.Server.Database.Migrations.Postgres
|
|||
b.Navigation("Profile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.CDModel+CDProfile", b =>
|
||||
{
|
||||
b.HasOne("Content.Server.Database.Profile", "Profile")
|
||||
.WithOne("CDProfile")
|
||||
.HasForeignKey("Content.Server.Database.CDModel+CDProfile", "ProfileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("FK_cdprofile_profile_profile_id");
|
||||
|
||||
b.Navigation("Profile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.CDModel+CharacterRecordEntry", b =>
|
||||
{
|
||||
b.HasOne("Content.Server.Database.CDModel+CDProfile", "CDProfile")
|
||||
.WithMany("CharacterRecordEntries")
|
||||
.HasForeignKey("CDProfileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("FK_cd_character_record_entries_cdprofile_cdprofile_id");
|
||||
|
||||
b.Navigation("CDProfile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
|
||||
{
|
||||
b.HasOne("Content.Server.Database.Server", "Server")
|
||||
|
|
@ -2015,6 +2112,11 @@ namespace Content.Server.Database.Migrations.Postgres
|
|||
b.Navigation("Flags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.CDModel+CDProfile", b =>
|
||||
{
|
||||
b.Navigation("CharacterRecordEntries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
|
||||
{
|
||||
b.Navigation("BanHits");
|
||||
|
|
@ -2068,6 +2170,8 @@ namespace Content.Server.Database.Migrations.Postgres
|
|||
{
|
||||
b.Navigation("Antags");
|
||||
|
||||
b.Navigation("CDProfile");
|
||||
|
||||
b.Navigation("Jobs");
|
||||
|
||||
b.Navigation("Loadouts");
|
||||
|
|
|
|||
2137
Content.Server.Database/Migrations/Sqlite/20250303043130_CosmaticDriftCharacterRecords.Designer.cs
generated
Normal file
2137
Content.Server.Database/Migrations/Sqlite/20250303043130_CosmaticDriftCharacterRecords.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,84 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Content.Server.Database.Migrations.Sqlite
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class CosmaticDriftCharacterRecords : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "cdprofile",
|
||||
columns: table => new
|
||||
{
|
||||
cdprofile_id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
profile_id = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
height = table.Column<float>(type: "REAL", nullable: false),
|
||||
character_records = table.Column<byte[]>(type: "jsonb", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_cdprofile", x => x.cdprofile_id);
|
||||
table.ForeignKey(
|
||||
name: "FK_cdprofile_profile_profile_id",
|
||||
column: x => x.profile_id,
|
||||
principalTable: "profile",
|
||||
principalColumn: "profile_id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "cd_character_record_entries",
|
||||
columns: table => new
|
||||
{
|
||||
cd_character_record_entries_id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
title = table.Column<string>(type: "TEXT", nullable: false),
|
||||
involved = table.Column<string>(type: "TEXT", nullable: false),
|
||||
description = table.Column<string>(type: "TEXT", nullable: false),
|
||||
type = table.Column<byte>(type: "INTEGER", nullable: false),
|
||||
cdprofile_id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_cd_character_record_entries", x => x.cd_character_record_entries_id);
|
||||
table.ForeignKey(
|
||||
name: "FK_cd_character_record_entries_cdprofile_cdprofile_id",
|
||||
column: x => x.cdprofile_id,
|
||||
principalTable: "cdprofile",
|
||||
principalColumn: "cdprofile_id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_cd_character_record_entries_cd_character_record_entries_id",
|
||||
table: "cd_character_record_entries",
|
||||
column: "cd_character_record_entries_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_cd_character_record_entries_cdprofile_id",
|
||||
table: "cd_character_record_entries",
|
||||
column: "cdprofile_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_cdprofile_profile_id",
|
||||
table: "cdprofile",
|
||||
column: "profile_id",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "cd_character_record_entries");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "cdprofile");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -547,6 +547,76 @@ namespace Content.Server.Database.Migrations.Sqlite
|
|||
b.ToTable("blacklist", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.CDModel+CDProfile", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("cdprofile_id");
|
||||
|
||||
b.Property<byte[]>("CharacterRecords")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("character_records");
|
||||
|
||||
b.Property<float>("Height")
|
||||
.HasColumnType("REAL")
|
||||
.HasColumnName("height");
|
||||
|
||||
b.Property<int>("ProfileId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("profile_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("PK_cdprofile");
|
||||
|
||||
b.HasIndex("ProfileId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("cdprofile", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.CDModel+CharacterRecordEntry", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("cd_character_record_entries_id");
|
||||
|
||||
b.Property<int>("CDProfileId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("cdprofile_id");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<string>("Involved")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("involved");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.Property<byte>("Type")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("PK_cd_character_record_entries");
|
||||
|
||||
b.HasIndex("CDProfileId")
|
||||
.HasDatabaseName("IX_cd_character_record_entries_cdprofile_id");
|
||||
|
||||
b.HasIndex("Id")
|
||||
.HasDatabaseName("IX_cd_character_record_entries_cd_character_record_entries_id");
|
||||
|
||||
b.ToTable("cd_character_record_entries", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
|
@ -1580,6 +1650,30 @@ namespace Content.Server.Database.Migrations.Sqlite
|
|||
b.Navigation("Profile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.CDModel+CDProfile", b =>
|
||||
{
|
||||
b.HasOne("Content.Server.Database.Profile", "Profile")
|
||||
.WithOne("CDProfile")
|
||||
.HasForeignKey("Content.Server.Database.CDModel+CDProfile", "ProfileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("FK_cdprofile_profile_profile_id");
|
||||
|
||||
b.Navigation("Profile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.CDModel+CharacterRecordEntry", b =>
|
||||
{
|
||||
b.HasOne("Content.Server.Database.CDModel+CDProfile", "CDProfile")
|
||||
.WithMany("CharacterRecordEntries")
|
||||
.HasForeignKey("CDProfileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("FK_cd_character_record_entries_cdprofile_cdprofile_id");
|
||||
|
||||
b.Navigation("CDProfile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
|
||||
{
|
||||
b.HasOne("Content.Server.Database.Server", "Server")
|
||||
|
|
@ -1939,6 +2033,11 @@ namespace Content.Server.Database.Migrations.Sqlite
|
|||
b.Navigation("Flags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.CDModel+CDProfile", b =>
|
||||
{
|
||||
b.Navigation("CharacterRecordEntries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
|
||||
{
|
||||
b.Navigation("BanHits");
|
||||
|
|
@ -1992,6 +2091,8 @@ namespace Content.Server.Database.Migrations.Sqlite
|
|||
{
|
||||
b.Navigation("Antags");
|
||||
|
||||
b.Navigation("CDProfile");
|
||||
|
||||
b.Navigation("Jobs");
|
||||
|
||||
b.Navigation("Loadouts");
|
||||
|
|
|
|||
|
|
@ -57,6 +57,20 @@ namespace Content.Server.Database
|
|||
.HasIndex(p => new {p.Slot, PrefsId = p.PreferenceId})
|
||||
.IsUnique();
|
||||
|
||||
// Begin CD - CD Character Data
|
||||
modelBuilder.Entity<CDModel.CDProfile>()
|
||||
.HasOne(p => p.Profile)
|
||||
.WithOne(p => p.CDProfile)
|
||||
.HasForeignKey<CDModel.CDProfile>(p => p.ProfileId)
|
||||
.IsRequired();
|
||||
|
||||
modelBuilder.Entity<CDModel.CharacterRecordEntry>()
|
||||
.HasOne(e => e.CDProfile)
|
||||
.WithMany(e => e.CharacterRecordEntries)
|
||||
.HasForeignKey(e => e.CDProfileId)
|
||||
.IsRequired();
|
||||
// End CD - CD Character Data
|
||||
|
||||
modelBuilder.Entity<Antag>()
|
||||
.HasIndex(p => new {HumanoidProfileId = p.ProfileId, p.AntagName})
|
||||
.IsUnique();
|
||||
|
|
@ -423,6 +437,8 @@ namespace Content.Server.Database
|
|||
|
||||
public int PreferenceId { get; set; }
|
||||
public Preference Preference { get; set; } = null!;
|
||||
|
||||
public CDModel.CDProfile? CDProfile { get; set; } // CD - Character Records
|
||||
}
|
||||
|
||||
public class Job
|
||||
|
|
|
|||
|
|
@ -82,6 +82,12 @@ namespace Content.Server.Database
|
|||
.Property(log => log.Markings)
|
||||
.HasConversion(jsonByteArrayConverter);
|
||||
|
||||
// Begin CD - Character Records
|
||||
modelBuilder.Entity<CDModel.CDProfile>()
|
||||
.Property(log => log.CharacterRecords)
|
||||
.HasConversion(jsonByteArrayConverter);
|
||||
// End CD - Character Records
|
||||
|
||||
// EF core can make this automatically unique on sqlite but not psql.
|
||||
modelBuilder.Entity<IPIntelCache>()
|
||||
.HasIndex(p => p.Address)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ using Robust.Shared.Enums;
|
|||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using Content.Server._CD.Records; // CD - Character Records
|
||||
using Content.Shared._CD.Records; // CD - Character Records
|
||||
|
||||
namespace Content.Server.Database
|
||||
{
|
||||
|
|
@ -48,6 +50,11 @@ namespace Content.Server.Database
|
|||
.Include(p => p.Profiles).ThenInclude(h => h.Jobs)
|
||||
.Include(p => p.Profiles).ThenInclude(h => h.Antags)
|
||||
.Include(p => p.Profiles).ThenInclude(h => h.Traits)
|
||||
// Begin CD - Character Records
|
||||
.Include(p => p.Profiles)
|
||||
.ThenInclude(h => h.CDProfile)
|
||||
.ThenInclude(cd => cd != null ? cd.CharacterRecordEntries : null)
|
||||
// End CD - Character Records
|
||||
.Include(p => p.Profiles)
|
||||
.ThenInclude(h => h.Loadouts)
|
||||
.ThenInclude(l => l.Groups)
|
||||
|
|
@ -95,6 +102,8 @@ namespace Content.Server.Database
|
|||
}
|
||||
|
||||
var oldProfile = db.DbContext.Profile
|
||||
.Include(p => p.CDProfile) // CD - Character Records
|
||||
.ThenInclude(cd => cd != null ? cd.CharacterRecordEntries : null)
|
||||
.Include(p => p.Preference)
|
||||
.Where(p => p.Preference.UserId == userId.UserId)
|
||||
.Include(p => p.Jobs)
|
||||
|
|
@ -216,6 +225,11 @@ namespace Content.Server.Database
|
|||
}
|
||||
}
|
||||
|
||||
// Begin CD - Chracter Records
|
||||
var cdRecords = profile.CDProfile?.CharacterRecords != null
|
||||
? RecordsSerialization.Deserialize(profile.CDProfile.CharacterRecords, profile.CDProfile.CharacterRecordEntries)
|
||||
: PlayerProvidedCharacterRecords.DefaultRecords();
|
||||
// End CD - Character Records
|
||||
var loadouts = new Dictionary<string, RoleLoadout>();
|
||||
|
||||
foreach (var role in profile.Loadouts)
|
||||
|
|
@ -262,7 +276,9 @@ namespace Content.Server.Database
|
|||
(PreferenceUnavailableMode) profile.PreferenceUnavailable,
|
||||
antags.ToHashSet(),
|
||||
traits.ToHashSet(),
|
||||
loadouts
|
||||
loadouts,
|
||||
profile.CDProfile?.Height ?? 1.0f, // CD - Character Records
|
||||
cdRecords // CD - Character Records
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -313,6 +329,18 @@ namespace Content.Server.Database
|
|||
.Select(t => new Trait {TraitName = t})
|
||||
);
|
||||
|
||||
// Begin CD - Character Records
|
||||
profile.CDProfile ??= new CDModel.CDProfile();
|
||||
profile.CDProfile.Height = humanoid.Height;
|
||||
// There are JsonIgnore annotations to ensure that entries are not stored as JSON.
|
||||
profile.CDProfile.CharacterRecords = JsonSerializer.SerializeToDocument(humanoid.CDCharacterRecords ?? PlayerProvidedCharacterRecords.DefaultRecords());
|
||||
if (humanoid.CDCharacterRecords != null)
|
||||
{
|
||||
profile.CDProfile.CharacterRecordEntries.Clear();
|
||||
profile.CDProfile.CharacterRecordEntries.AddRange(RecordsSerialization.GetEntries(humanoid.CDCharacterRecords));
|
||||
}
|
||||
// End CD - Character Records
|
||||
|
||||
profile.Loadouts.Clear();
|
||||
|
||||
foreach (var (role, loadouts) in humanoid.Loadouts)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
namespace Content.Server._CD.Records;
|
||||
|
||||
/// <summary>
|
||||
/// Stores the key to the entities character records.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
[Access(typeof(CharacterRecordsSystem))]
|
||||
public sealed partial class CharacterRecordKeyStorageComponent : Component
|
||||
{
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
public CharacterRecordKey Key;
|
||||
|
||||
public CharacterRecordKeyStorageComponent(CharacterRecordKey key)
|
||||
{
|
||||
Key = key;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
using Content.Shared._CD.Records;
|
||||
|
||||
namespace Content.Server._CD.Records;
|
||||
|
||||
/// <summary>
|
||||
/// The component on the station that stores records after the round starts.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
[Access(typeof(CharacterRecordsSystem))]
|
||||
public sealed partial class CharacterRecordsComponent : Component
|
||||
{
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
public Dictionary<uint, FullCharacterRecords> Records = new();
|
||||
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
private uint _nextKey = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a key has never been used previously
|
||||
/// </summary>
|
||||
public uint CreateNewKey()
|
||||
{
|
||||
return _nextKey++;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record CharacterRecordKey
|
||||
{
|
||||
public EntityUid Station { get; init; }
|
||||
public uint Index { get; init; }
|
||||
}
|
||||
|
|
@ -0,0 +1,194 @@
|
|||
using Content.Server.Forensics;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.StationRecords.Systems;
|
||||
using Content.Server.StationRecords;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.PDA;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.StationRecords;
|
||||
using Content.Shared._CD.Records;
|
||||
using Content.Shared.Forensics.Components;
|
||||
using Content.Shared.GameTicking;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server._CD.Records;
|
||||
|
||||
public sealed class CharacterRecordsSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
[Dependency] private readonly StationRecordsSystem _records = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnPlayerSpawn, after: [typeof(StationRecordsSystem)]);
|
||||
}
|
||||
|
||||
private void OnPlayerSpawn(PlayerSpawnCompleteEvent args)
|
||||
{
|
||||
if (!HasComp<StationRecordsComponent>(args.Station))
|
||||
{
|
||||
Log.Error("Tried to add CharacterRecords on a station without StationRecords");
|
||||
return;
|
||||
}
|
||||
if (!HasComp<CharacterRecordsComponent>(args.Station))
|
||||
AddComp<CharacterRecordsComponent>(args.Station);
|
||||
|
||||
if (string.IsNullOrEmpty(args.JobId))
|
||||
{
|
||||
Log.Error($"Null JobId in CharacterRecordsSystem::OnPlayerSpawn for character {args.Profile.Name} played by {args.Player.Name}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (HasComp<SkipLoadingCharacterRecordsComponent>(args.Mob))
|
||||
return;
|
||||
|
||||
var profile = args.Profile;
|
||||
if (profile.CDCharacterRecords == null)
|
||||
{
|
||||
Log.Error($"Null records in CharacterRecordsSystem::OnPlayerSpawn for character {args.Profile.Name} played by {args.Player.Name}.");
|
||||
return;
|
||||
}
|
||||
|
||||
var player = args.Mob;
|
||||
|
||||
if (!_prototype.TryIndex(args.JobId, out JobPrototype? jobPrototype))
|
||||
{
|
||||
throw new ArgumentException($"Invalid job prototype ID: {args.JobId}");
|
||||
}
|
||||
|
||||
TryComp<FingerprintComponent>(player, out var fingerprintComponent);
|
||||
TryComp<DnaComponent>(player, out var dnaComponent);
|
||||
|
||||
var jobTitle = jobPrototype.LocalizedName;
|
||||
var stationRecordsKey = FindStationRecordsKey(player);
|
||||
|
||||
// Grab the title from the station records if they exist to support our job title system
|
||||
if (stationRecordsKey != null && _records.TryGetRecord<GeneralStationRecord>(stationRecordsKey.Value, out var stationRecords))
|
||||
{
|
||||
jobTitle = stationRecords.JobTitle;
|
||||
}
|
||||
|
||||
var records = new FullCharacterRecords(
|
||||
pRecords: new PlayerProvidedCharacterRecords(profile.CDCharacterRecords),
|
||||
stationRecordsKey: stationRecordsKey?.Id,
|
||||
name: profile.Name,
|
||||
age: profile.Age,
|
||||
species: profile.Species,
|
||||
jobTitle: jobTitle,
|
||||
jobIcon: jobPrototype.Icon,
|
||||
gender: profile.Gender,
|
||||
sex: profile.Sex,
|
||||
fingerprint: fingerprintComponent?.Fingerprint,
|
||||
dna: dnaComponent?.DNA,
|
||||
owner: player);
|
||||
AddRecord(args.Station, args.Mob, records);
|
||||
}
|
||||
|
||||
private StationRecordKey? FindStationRecordsKey(EntityUid uid)
|
||||
{
|
||||
if (!_inventory.TryGetSlotEntity(uid, "id", out var idUid))
|
||||
return null;
|
||||
|
||||
var keyStorageEntity = idUid;
|
||||
if (TryComp<PdaComponent>(idUid, out var pda) && pda.ContainedId is {} id)
|
||||
{
|
||||
keyStorageEntity = id;
|
||||
}
|
||||
|
||||
if (!TryComp<StationRecordKeyStorageComponent>(keyStorageEntity, out var storage))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return storage.Key;
|
||||
}
|
||||
|
||||
private void AddRecord(EntityUid station, EntityUid player, FullCharacterRecords records, CharacterRecordsComponent? recordsDb = null)
|
||||
{
|
||||
if (!Resolve(station, ref recordsDb))
|
||||
return;
|
||||
|
||||
var key = recordsDb.CreateNewKey();
|
||||
recordsDb.Records.Add(key, records);
|
||||
var playerKey = new CharacterRecordKey { Station = station, Index = key };
|
||||
AddComp(player, new CharacterRecordKeyStorageComponent(playerKey));
|
||||
|
||||
RaiseLocalEvent(station, new CharacterRecordsModifiedEvent());
|
||||
}
|
||||
|
||||
public void DelEntry(
|
||||
EntityUid station,
|
||||
EntityUid player,
|
||||
CharacterRecordType ty,
|
||||
int idx,
|
||||
CharacterRecordsComponent? recordsDb = null,
|
||||
CharacterRecordKeyStorageComponent? key = null)
|
||||
{
|
||||
if (!Resolve(station, ref recordsDb) || !Resolve(player, ref key))
|
||||
return;
|
||||
|
||||
if (!recordsDb.Records.TryGetValue(key.Key.Index, out var value))
|
||||
return;
|
||||
|
||||
var cr = value.PRecords;
|
||||
|
||||
switch (ty)
|
||||
{
|
||||
case CharacterRecordType.Employment:
|
||||
cr.EmploymentEntries.RemoveAt(idx);
|
||||
break;
|
||||
case CharacterRecordType.Medical:
|
||||
cr.MedicalEntries.RemoveAt(idx);
|
||||
break;
|
||||
case CharacterRecordType.Security:
|
||||
cr.SecurityEntries.RemoveAt(idx);
|
||||
break;
|
||||
}
|
||||
|
||||
RaiseLocalEvent(station, new CharacterRecordsModifiedEvent());
|
||||
}
|
||||
|
||||
public void ResetRecord(
|
||||
EntityUid station,
|
||||
EntityUid player,
|
||||
CharacterRecordsComponent? recordsDb = null,
|
||||
CharacterRecordKeyStorageComponent? key = null)
|
||||
{
|
||||
if (!Resolve(station, ref recordsDb) || !Resolve(player, ref key))
|
||||
return;
|
||||
|
||||
if (!recordsDb.Records.TryGetValue(key.Key.Index, out var value))
|
||||
return;
|
||||
|
||||
var records = PlayerProvidedCharacterRecords.DefaultRecords();
|
||||
if (TryComp(player, out MetaDataComponent? meta))
|
||||
value.Name = meta.EntityName;
|
||||
value.PRecords = records;
|
||||
RaiseLocalEvent(station, new CharacterRecordsModifiedEvent());
|
||||
}
|
||||
|
||||
public void DeleteAllRecords(EntityUid player, CharacterRecordKeyStorageComponent? key = null)
|
||||
{
|
||||
if (!Resolve(player, ref key))
|
||||
return;
|
||||
|
||||
var station = key.Key.Station;
|
||||
CharacterRecordsComponent? records = null;
|
||||
if (!Resolve(station, ref records))
|
||||
return;
|
||||
|
||||
records.Records.Remove(key.Key.Index);
|
||||
}
|
||||
|
||||
public IDictionary<uint, FullCharacterRecords> QueryRecords(EntityUid station, CharacterRecordsComponent? recordsDb = null)
|
||||
{
|
||||
return !Resolve(station, ref recordsDb)
|
||||
? new Dictionary<uint, FullCharacterRecords>()
|
||||
: recordsDb.Records;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CharacterRecordsModifiedEvent : EntityEventArgs;
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
using Content.Server.Administration;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared._CD.Records;
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Server._CD.Records.Commands;
|
||||
|
||||
[AdminCommand(AdminFlags.Ban)]
|
||||
public sealed class DelRecordEntryCommand : IConsoleCommand
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
public string Command => "delrecordentry";
|
||||
|
||||
public string Description =>
|
||||
"Resets the records of the given entity to the default values. This is not saved to the database and only lasts until the round is over";
|
||||
|
||||
public string Help => $"{Command} <entity> <recordType> <index>";
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length < 3)
|
||||
{
|
||||
shell.WriteLine($"Not enough arguments.\n{Help}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!NetEntity.TryParse(args[0], out var uidNet) || !_entManager.TryGetEntity(uidNet, out var uid))
|
||||
{
|
||||
shell.WriteLine($"Invalid entity id.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Enum.TryParse<CharacterRecordType>(args[1], out var ty))
|
||||
{
|
||||
shell.WriteLine($"Invalid entry type.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!int.TryParse(args[2], out var idx))
|
||||
{
|
||||
shell.WriteLine($"Invalid index.");
|
||||
return;
|
||||
}
|
||||
|
||||
var characterRecordsSystem = _entManager.System<CharacterRecordsSystem>();
|
||||
var stationSystem = _entManager.System<StationSystem>();
|
||||
|
||||
foreach (var s in stationSystem.GetStations())
|
||||
{
|
||||
characterRecordsSystem.DelEntry(s, uid.Value, ty, idx);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
using Content.Server.Administration;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Server._CD.Records.Commands;
|
||||
|
||||
[AdminCommand(AdminFlags.Ban)]
|
||||
public sealed class PurgeCharacterRecordsCommand : IConsoleCommand
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
public string Command => "purgecharacterrecords";
|
||||
|
||||
public string Description =>
|
||||
"Resets the records of the given entity to the default values. This is not saved to the database and only lasts until the round is over";
|
||||
|
||||
public string Help => $"{Command} <entity>";
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length < 1)
|
||||
{
|
||||
shell.WriteLine($"Not enough arguments.\n{Help}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!NetEntity.TryParse(args[0], out var uidNet) || !_entManager.TryGetEntity(uidNet, out var uid))
|
||||
{
|
||||
shell.WriteLine($"Invalid entity id.");
|
||||
return;
|
||||
}
|
||||
|
||||
var characterRecordsSystem = _entManager.System<CharacterRecordsSystem>();
|
||||
var stationSystem = _entManager.System<StationSystem>();
|
||||
|
||||
foreach (var s in stationSystem.GetStations())
|
||||
{
|
||||
characterRecordsSystem.ResetRecord(s, uid.Value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
using Content.Shared._CD.Records;
|
||||
using Content.Shared.StationRecords;
|
||||
|
||||
namespace Content.Server._CD.Records.Consoles;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class CharacterRecordConsoleComponent : Component
|
||||
{
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
public uint? SelectedIndex { get; set; }
|
||||
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
public StationRecordsFilter? Filter;
|
||||
|
||||
[DataField(required: true), ViewVariables(VVAccess.ReadOnly)]
|
||||
public RecordConsoleType ConsoleType;
|
||||
}
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
using Content.Server.Station.Systems;
|
||||
using Content.Server.StationRecords.Systems;
|
||||
using Content.Server.StationRecords;
|
||||
using Content.Shared.CriminalRecords;
|
||||
using Content.Shared.Security;
|
||||
using Content.Shared.StationRecords;
|
||||
using Content.Shared._CD.Records;
|
||||
using Robust.Server.GameObjects;
|
||||
|
||||
namespace Content.Server._CD.Records.Consoles;
|
||||
|
||||
public sealed class CharacterRecordConsoleSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly CharacterRecordsSystem _characterRecords = default!;
|
||||
[Dependency] private readonly IEntityManager _entity = default!;
|
||||
[Dependency] private readonly StationRecordsSystem _records = default!;
|
||||
[Dependency] private readonly StationSystem _station = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _ui = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<CharacterRecordConsoleComponent, CharacterRecordsModifiedEvent>((uid, component, _) =>
|
||||
UpdateUi(uid, component));
|
||||
|
||||
Subs.BuiEvents<CharacterRecordConsoleComponent>(CharacterRecordConsoleKey.Key,
|
||||
subr =>
|
||||
{
|
||||
subr.Event<BoundUIOpenedEvent>((uid, component, _) => UpdateUi(uid, component));
|
||||
subr.Event<CharacterRecordConsoleSelectMsg>(OnKeySelect);
|
||||
subr.Event<CharacterRecordsConsoleFilterMsg>(OnFilterApplied);
|
||||
});
|
||||
}
|
||||
|
||||
private void OnFilterApplied(Entity<CharacterRecordConsoleComponent> ent, ref CharacterRecordsConsoleFilterMsg msg)
|
||||
{
|
||||
ent.Comp.Filter = msg.Filter;
|
||||
UpdateUi(ent);
|
||||
}
|
||||
|
||||
private void OnKeySelect(Entity<CharacterRecordConsoleComponent> ent, ref CharacterRecordConsoleSelectMsg msg)
|
||||
{
|
||||
ent.Comp.SelectedIndex = msg.CharacterRecordKey;
|
||||
UpdateUi(ent);
|
||||
}
|
||||
|
||||
private void UpdateUi(EntityUid entity, CharacterRecordConsoleComponent? console = null)
|
||||
{
|
||||
if (!Resolve(entity, ref console))
|
||||
return;
|
||||
|
||||
var station = _station.GetOwningStation(entity);
|
||||
if (!HasComp<StationRecordsComponent>(station) ||
|
||||
!HasComp<CharacterRecordsComponent>(station))
|
||||
{
|
||||
SendState(entity, new CharacterRecordConsoleState { ConsoleType = console.ConsoleType });
|
||||
return;
|
||||
}
|
||||
|
||||
var characterRecords = _characterRecords.QueryRecords(station.Value);
|
||||
// Get the name and station records key display from the list of records
|
||||
var names = new Dictionary<uint, CharacterRecordConsoleState.CharacterInfo>();
|
||||
foreach (var (i, r) in characterRecords)
|
||||
{
|
||||
var netEnt = _entity.GetNetEntity(r.Owner!.Value);
|
||||
// Admins get additional info to make it easier to run commands
|
||||
var nameJob = console.ConsoleType != RecordConsoleType.Admin
|
||||
? $"{r.Name} ({r.JobTitle})"
|
||||
: $"{r.Name} ({netEnt}, {r.JobTitle}";
|
||||
|
||||
// Apply any filter the user has set
|
||||
if (console.Filter != null)
|
||||
{
|
||||
if (IsSkippedRecord(console.Filter, r, nameJob))
|
||||
continue;
|
||||
}
|
||||
|
||||
if (names.ContainsKey(i))
|
||||
{
|
||||
Log.Error(
|
||||
$"We somehow have duplicate character record keys, NetEntity: {i}, Entity: {entity}, Character Name: {r.Name}");
|
||||
}
|
||||
|
||||
names[i] = new CharacterRecordConsoleState.CharacterInfo
|
||||
{ CharacterDisplayName = nameJob, StationRecordKey = r.StationRecordsKey };
|
||||
}
|
||||
|
||||
var record =
|
||||
console.SelectedIndex == null || !characterRecords.TryGetValue(console.SelectedIndex!.Value, out var value)
|
||||
? null
|
||||
: value;
|
||||
(SecurityStatus, string?)? securityStatus = null;
|
||||
|
||||
// If we need the character's security status, gather it from the criminal records
|
||||
if ((console.ConsoleType == RecordConsoleType.Admin ||
|
||||
console.ConsoleType == RecordConsoleType.Security)
|
||||
&& record?.StationRecordsKey != null)
|
||||
{
|
||||
var key = new StationRecordKey(record.StationRecordsKey.Value, station.Value);
|
||||
if (_records.TryGetRecord<CriminalRecord>(key, out var entry))
|
||||
securityStatus = (entry.Status, entry.Reason);
|
||||
}
|
||||
|
||||
SendState(entity,
|
||||
new CharacterRecordConsoleState
|
||||
{
|
||||
ConsoleType = console.ConsoleType,
|
||||
CharacterList = names,
|
||||
SelectedIndex = console.SelectedIndex,
|
||||
SelectedRecord = record,
|
||||
Filter = console.Filter,
|
||||
SelectedSecurityStatus = securityStatus,
|
||||
});
|
||||
}
|
||||
|
||||
private void SendState(EntityUid entity, CharacterRecordConsoleState state)
|
||||
{
|
||||
_ui.SetUiState(entity, CharacterRecordConsoleKey.Key, state);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Almost exactly the same as <see cref="StationRecordsSystem.IsSkipped"/>
|
||||
/// </summary>
|
||||
private static bool IsSkippedRecord(StationRecordsFilter filter,
|
||||
FullCharacterRecords record,
|
||||
string nameJob)
|
||||
{
|
||||
var isFilter = filter.Value.Length > 0;
|
||||
|
||||
if (!isFilter)
|
||||
return false;
|
||||
|
||||
var filterLowerCaseValue = filter.Value.ToLower();
|
||||
|
||||
return filter.Type switch
|
||||
{
|
||||
StationRecordFilterType.Name =>
|
||||
!nameJob.Contains(filterLowerCaseValue, StringComparison.CurrentCultureIgnoreCase),
|
||||
StationRecordFilterType.Prints => record.Fingerprint != null
|
||||
&& IsFilterWithSomeCodeValue(record.Fingerprint, filterLowerCaseValue),
|
||||
StationRecordFilterType.DNA => record.DNA != null
|
||||
&& IsFilterWithSomeCodeValue(record.DNA, filterLowerCaseValue),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(filter), "Invalid Character Record filter type"),
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsFilterWithSomeCodeValue(string value, string filter)
|
||||
{
|
||||
return !value.StartsWith(filter, StringComparison.CurrentCultureIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
using Content.Server.Database;
|
||||
using Content.Shared._CD.Records;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Content.Server._CD.Records;
|
||||
|
||||
public static class RecordsSerialization
|
||||
{
|
||||
private static int DeserializeInt(JsonElement e, string key, int def)
|
||||
{
|
||||
if (e.TryGetProperty(key, out var prop) && prop.TryGetInt32(out var v))
|
||||
{
|
||||
return v;
|
||||
}
|
||||
|
||||
return def;
|
||||
}
|
||||
|
||||
private static bool DeserializeBool(JsonElement e, string key, bool def)
|
||||
{
|
||||
if (!e.TryGetProperty(key, out var v))
|
||||
return def;
|
||||
|
||||
return v.ValueKind switch
|
||||
{
|
||||
JsonValueKind.True => true,
|
||||
JsonValueKind.False => false,
|
||||
_ => def,
|
||||
};
|
||||
}
|
||||
|
||||
[return: NotNullIfNotNull(nameof(def))]
|
||||
private static string? DeserializeString(JsonElement e, string key, string? def)
|
||||
{
|
||||
if (!e.TryGetProperty(key, out var v))
|
||||
return def;
|
||||
|
||||
if (v.ValueKind == JsonValueKind.String)
|
||||
return v.GetString() ?? def;
|
||||
|
||||
return def;
|
||||
}
|
||||
|
||||
private static List<PlayerProvidedCharacterRecords.RecordEntry> DeserializeEntries(List<CDModel.CharacterRecordEntry> entries, CDModel.DbRecordEntryType ty)
|
||||
{
|
||||
return entries.Where(e => e.Type == ty)
|
||||
.OrderBy(e => e.Id) // attempt at fixing the record order changing bug.
|
||||
.Select(e => new PlayerProvidedCharacterRecords.RecordEntry(e.Title, e.Involved, e.Description))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// We need to manually deserialize CharacterRecords because the easy JSON deserializer does not
|
||||
/// do exactly what we want. More specifically, we need to more robustly handle missing and extra fields
|
||||
/// <br />
|
||||
/// <br />
|
||||
/// Missing fields are filled in with their default value, extra fields are simply ignored
|
||||
/// </summary>
|
||||
public static PlayerProvidedCharacterRecords Deserialize(JsonDocument json, List<CDModel.CharacterRecordEntry> entries)
|
||||
{
|
||||
var e = json.RootElement;
|
||||
var def = PlayerProvidedCharacterRecords.DefaultRecords();
|
||||
return new PlayerProvidedCharacterRecords(
|
||||
height: DeserializeInt(e, nameof(def.Height), def.Height),
|
||||
weight: DeserializeInt(e, nameof(def.Weight), def.Weight),
|
||||
emergencyContactName: DeserializeString(e, nameof(def.EmergencyContactName), def.EmergencyContactName),
|
||||
hasWorkAuthorization: DeserializeBool(e, nameof(def.HasWorkAuthorization), def.HasWorkAuthorization),
|
||||
identifyingFeatures: DeserializeString(e, nameof(def.IdentifyingFeatures), def.IdentifyingFeatures),
|
||||
allergies: DeserializeString(e, nameof(def.Allergies), def.Allergies),
|
||||
drugAllergies: DeserializeString(e, nameof(def.DrugAllergies), def.DrugAllergies),
|
||||
postmortemInstructions: DeserializeString(e, nameof(def.PostmortemInstructions), def.PostmortemInstructions),
|
||||
medicalEntries: DeserializeEntries(entries, CDModel.DbRecordEntryType.Medical),
|
||||
securityEntries: DeserializeEntries(entries, CDModel.DbRecordEntryType.Security),
|
||||
employmentEntries: DeserializeEntries(entries, CDModel.DbRecordEntryType.Employment));
|
||||
}
|
||||
|
||||
private static CDModel.CharacterRecordEntry ConvertEntry(PlayerProvidedCharacterRecords.RecordEntry entry, CDModel.DbRecordEntryType type)
|
||||
{
|
||||
entry.EnsureValid();
|
||||
return new CDModel.CharacterRecordEntry()
|
||||
{ Title = entry.Title, Involved = entry.Involved, Description = entry.Description, Type = type };
|
||||
}
|
||||
|
||||
public static List<CDModel.CharacterRecordEntry> GetEntries(PlayerProvidedCharacterRecords records)
|
||||
{
|
||||
return records.MedicalEntries.Select(medical => ConvertEntry(medical, CDModel.DbRecordEntryType.Medical))
|
||||
.Concat(records.SecurityEntries.Select(security => ConvertEntry(security, CDModel.DbRecordEntryType.Security)))
|
||||
.Concat(records.EmploymentEntries.Select(employment => ConvertEntry(employment, CDModel.DbRecordEntryType.Employment)))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
namespace Content.Server._CD.Records;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class SkipLoadingCharacterRecordsComponent : Component;
|
||||
|
|
@ -85,6 +85,14 @@ public sealed partial class HumanoidAppearanceComponent : Component
|
|||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
public Color? CachedFacialHairColor;
|
||||
|
||||
// CD - Character Records
|
||||
/// <summary>
|
||||
/// The height of this humanoid.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public float Height = 1f;
|
||||
// CD - Character Records
|
||||
|
||||
/// <summary>
|
||||
/// Which layers of this humanoid that should be hidden on equipping a corresponding item..
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -120,6 +120,52 @@ public sealed partial class SpeciesPrototype : IPrototype
|
|||
/// </summary>
|
||||
[DataField]
|
||||
public int MaxAge = 120;
|
||||
|
||||
// Begin DV - CD Character Records shouldn't nuke species heights
|
||||
/// <summary>
|
||||
/// The base height scale for this species
|
||||
/// </summary>
|
||||
[DataField("baseScale")]
|
||||
public System.Numerics.Vector2 BaseScale = new(1f, 1f);
|
||||
// End DV - CD Character Records shouldn't nuke species heights
|
||||
|
||||
// Begin CD - Character Records
|
||||
/// <summary>
|
||||
/// The minimum height for this species
|
||||
/// </summary>
|
||||
[DataField("minHeight")]
|
||||
public float MinHeight = 0.85f; // DeltaV - less trolling with the heights
|
||||
|
||||
/// <summary>
|
||||
/// The maximum height for this species
|
||||
/// </summary>
|
||||
[DataField("maxHeight")]
|
||||
public float MaxHeight = 1.2f; // DeltaV - less trolling with the heights
|
||||
|
||||
/// <summary>
|
||||
/// The default height for this species
|
||||
/// </summary>
|
||||
[DataField("defaultHeight")]
|
||||
public float DefaultHeight = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// The default width for this species
|
||||
/// </summary>
|
||||
[DataField("defaultWidth")]
|
||||
public float DefaultWidth = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to scale horizontally or not
|
||||
/// </summary>
|
||||
[DataField("scaleWidth")]
|
||||
public bool ScaleWidth = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to scale vertically or not
|
||||
/// </summary>
|
||||
[DataField("scaleHeight")]
|
||||
public bool ScaleHeight = true;
|
||||
// End CD - Character Records
|
||||
}
|
||||
|
||||
public enum SpeciesNaming : byte
|
||||
|
|
|
|||
|
|
@ -154,6 +154,8 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem
|
|||
targetHumanoid.CustomBaseLayers = new(sourceHumanoid.CustomBaseLayers);
|
||||
targetHumanoid.MarkingSet = new(sourceHumanoid.MarkingSet);
|
||||
|
||||
targetHumanoid.Height = sourceHumanoid.Height; // CD - Character Records
|
||||
|
||||
targetHumanoid.Gender = sourceHumanoid.Gender;
|
||||
if (TryComp<GrammarComponent>(target, out var grammar))
|
||||
grammar.Gender = sourceHumanoid.Gender;
|
||||
|
|
@ -417,6 +419,7 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem
|
|||
}
|
||||
|
||||
humanoid.Age = profile.Age;
|
||||
humanoid.Height = profile.Height; // CD - Character Records
|
||||
|
||||
humanoid.LastProfileLoaded = profile; // DeltaV - let paradox anomaly be cloned
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ using Robust.Shared.Prototypes;
|
|||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
using Content.Shared._CD.Records; // CD - Character Records
|
||||
|
||||
namespace Content.Shared.Preferences
|
||||
{
|
||||
|
|
@ -135,6 +136,14 @@ namespace Content.Shared.Preferences
|
|||
public PreferenceUnavailableMode PreferenceUnavailable { get; private set; } =
|
||||
PreferenceUnavailableMode.SpawnAsOverflow;
|
||||
|
||||
// Begin CD - Character records
|
||||
[DataField("cosmaticDriftCharacterHeight")]
|
||||
public float Height = 1f;
|
||||
|
||||
[DataField("cosmaticDriftCharacterRecords")]
|
||||
public PlayerProvidedCharacterRecords? CDCharacterRecords;
|
||||
// End CD - Character records
|
||||
|
||||
public HumanoidCharacterProfile(
|
||||
string name,
|
||||
string flavortext,
|
||||
|
|
@ -148,7 +157,12 @@ namespace Content.Shared.Preferences
|
|||
PreferenceUnavailableMode preferenceUnavailable,
|
||||
HashSet<ProtoId<AntagPrototype>> antagPreferences,
|
||||
HashSet<ProtoId<TraitPrototype>> traitPreferences,
|
||||
Dictionary<string, RoleLoadout> loadouts)
|
||||
Dictionary<string, RoleLoadout> loadouts,
|
||||
// Begin CD - Character Records
|
||||
float height,
|
||||
PlayerProvidedCharacterRecords? cdCharacterRecords
|
||||
// End CD - Character Records
|
||||
)
|
||||
{
|
||||
Name = name;
|
||||
FlavorText = flavortext;
|
||||
|
|
@ -163,6 +177,10 @@ namespace Content.Shared.Preferences
|
|||
_antagPreferences = antagPreferences;
|
||||
_traitPreferences = traitPreferences;
|
||||
_loadouts = loadouts;
|
||||
// Begin CD - Character Records
|
||||
Height = height;
|
||||
CDCharacterRecords = cdCharacterRecords;
|
||||
// End CD - Character Records
|
||||
|
||||
var hasHighPrority = false;
|
||||
foreach (var (key, value) in _jobPriorities)
|
||||
|
|
@ -193,7 +211,9 @@ namespace Content.Shared.Preferences
|
|||
other.PreferenceUnavailable,
|
||||
new HashSet<ProtoId<AntagPrototype>>(other.AntagPreferences),
|
||||
new HashSet<ProtoId<TraitPrototype>>(other.TraitPreferences),
|
||||
new Dictionary<string, RoleLoadout>(other.Loadouts))
|
||||
new Dictionary<string, RoleLoadout>(other.Loadouts),
|
||||
other.Height, // CD - Character Records
|
||||
other.CDCharacterRecords) // CD - Character Records
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -241,10 +261,12 @@ namespace Content.Shared.Preferences
|
|||
|
||||
var sex = Sex.Unsexed;
|
||||
var age = 18;
|
||||
var height = 1f; // CD - Character Records
|
||||
if (prototypeManager.TryIndex<SpeciesPrototype>(species, out var speciesPrototype))
|
||||
{
|
||||
sex = random.Pick(speciesPrototype.Sexes);
|
||||
age = random.Next(speciesPrototype.MinAge, speciesPrototype.OldAge); // people don't look and keep making 119 year old characters with zero rp, cap it at middle aged
|
||||
height = MathF.Round(random.NextFloat(speciesPrototype.MinHeight, speciesPrototype.MaxHeight), 2); // CD - Character Records
|
||||
}
|
||||
|
||||
var gender = Gender.Epicene;
|
||||
|
|
@ -269,6 +291,7 @@ namespace Content.Shared.Preferences
|
|||
Gender = gender,
|
||||
Species = species,
|
||||
Appearance = HumanoidCharacterAppearance.Random(species, sex),
|
||||
Height = height,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -313,6 +336,18 @@ namespace Content.Shared.Preferences
|
|||
return new(this) { SpawnPriority = spawnPriority };
|
||||
}
|
||||
|
||||
// Begin CD - Character Records
|
||||
public HumanoidCharacterProfile WithHeight(float height)
|
||||
{
|
||||
return new(this) { Height = height };
|
||||
}
|
||||
|
||||
public HumanoidCharacterProfile WithCDCharacterRecords(PlayerProvidedCharacterRecords records)
|
||||
{
|
||||
return new HumanoidCharacterProfile(this) { CDCharacterRecords = records };
|
||||
}
|
||||
// End CD - Character Records
|
||||
|
||||
public HumanoidCharacterProfile WithJobPriorities(IEnumerable<KeyValuePair<ProtoId<JobPrototype>, JobPriority>> jobPriorities)
|
||||
{
|
||||
var dictionary = new Dictionary<ProtoId<JobPrototype>, JobPriority>(jobPriorities);
|
||||
|
|
@ -479,6 +514,9 @@ namespace Content.Shared.Preferences
|
|||
if (!_traitPreferences.SequenceEqual(other._traitPreferences)) return false;
|
||||
if (!Loadouts.SequenceEqual(other.Loadouts)) return false;
|
||||
if (FlavorText != other.FlavorText) return false;
|
||||
if (Height != other.Height) return false; // CD
|
||||
if (CDCharacterRecords != null && other.CDCharacterRecords != null && // CD
|
||||
!CDCharacterRecords.MemberwiseEquals(other.CDCharacterRecords)) return false; // CD
|
||||
return Appearance.MemberwiseEquals(other.Appearance);
|
||||
}
|
||||
|
||||
|
|
@ -558,6 +596,12 @@ namespace Content.Shared.Preferences
|
|||
flavortext = FormattedMessage.RemoveMarkupOrThrow(FlavorText);
|
||||
}
|
||||
|
||||
// Begin CD - Character Records
|
||||
var height = Height;
|
||||
if (speciesPrototype != null)
|
||||
height = Math.Clamp(MathF.Round(Height, 2), speciesPrototype.MinHeight, speciesPrototype.MaxHeight);
|
||||
// End CD - Character Records
|
||||
|
||||
var appearance = HumanoidCharacterAppearance.EnsureValid(Appearance, Species, Sex);
|
||||
|
||||
var prefsUnavailableMode = PreferenceUnavailable switch
|
||||
|
|
@ -627,6 +671,18 @@ namespace Content.Shared.Preferences
|
|||
_traitPreferences.Clear();
|
||||
_traitPreferences.UnionWith(GetValidTraits(traits, prototypeManager));
|
||||
|
||||
// Begin CD - Character Records
|
||||
Height = height;
|
||||
if (CDCharacterRecords == null)
|
||||
{
|
||||
CDCharacterRecords = PlayerProvidedCharacterRecords.DefaultRecords();
|
||||
}
|
||||
else
|
||||
{
|
||||
CDCharacterRecords!.EnsureValid();
|
||||
}
|
||||
// End CD - Character Records
|
||||
|
||||
// Checks prototypes exist for all loadouts and dump / set to default if not.
|
||||
var toRemove = new ValueList<string>();
|
||||
|
||||
|
|
@ -722,6 +778,7 @@ namespace Content.Shared.Preferences
|
|||
hashCode.Add(Appearance);
|
||||
hashCode.Add((int)SpawnPriority);
|
||||
hashCode.Add((int)PreferenceUnavailable);
|
||||
hashCode.Add(Height); // CD - Character Records
|
||||
return hashCode.ToHashCode();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
using Content.Shared.Humanoid;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared._CD.Records;
|
||||
|
||||
/// <summary>
|
||||
/// Contains the full records information, not just stuff that is in the database.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class FullCharacterRecords(
|
||||
PlayerProvidedCharacterRecords pRecords,
|
||||
uint? stationRecordsKey,
|
||||
string name,
|
||||
int age,
|
||||
string jobTitle,
|
||||
string jobIcon,
|
||||
string species,
|
||||
Gender gender,
|
||||
Sex sex,
|
||||
string? fingerprint,
|
||||
string? dna,
|
||||
EntityUid? owner = null)
|
||||
{
|
||||
[ViewVariables]
|
||||
public PlayerProvidedCharacterRecords PRecords = pRecords;
|
||||
|
||||
/// <summary>
|
||||
/// Key for the equivalent entry in the station records
|
||||
///
|
||||
/// Sadly, this has to be a uint because StationRecordsKey is not serializable
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public uint? StationRecordsKey = stationRecordsKey;
|
||||
|
||||
/// <summary>
|
||||
/// Name tied to this record.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public string Name = name;
|
||||
|
||||
/// <summary>
|
||||
/// Age of the person that this record represents.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public int Age = age;
|
||||
|
||||
/// <summary>
|
||||
/// Job title tied to this record.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public string JobTitle = jobTitle;
|
||||
|
||||
/// <summary>
|
||||
/// Job icon tied to this record.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public string JobIcon = jobIcon;
|
||||
|
||||
/// <summary>
|
||||
/// Species tied to this record.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public string Species = species;
|
||||
|
||||
/// <summary>
|
||||
/// Gender identity tied to this record.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public Gender Gender = gender;
|
||||
|
||||
/// <summary>
|
||||
/// Sex identity tied to this record.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public Sex Sex = sex;
|
||||
|
||||
[ViewVariables]
|
||||
public string? Fingerprint = fingerprint;
|
||||
|
||||
/// <summary>
|
||||
/// DNA of the person.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public string? DNA = dna;
|
||||
|
||||
/// <summary>
|
||||
/// The entity that owns this record. Should always nonnull inside CharacterRecordsComponent. This field should not be accessed client side.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[NonSerialized]
|
||||
public EntityUid? Owner = owner;
|
||||
}
|
||||
|
|
@ -0,0 +1,268 @@
|
|||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared._CD.Records;
|
||||
|
||||
/// <summary>
|
||||
/// Contains Cosmatic Drift records that can be changed in the character editor. This is stored on the character's profile.
|
||||
/// </summary>
|
||||
[DataDefinition]
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class PlayerProvidedCharacterRecords
|
||||
{
|
||||
public const int TextMedLen = 64;
|
||||
public const int TextVeryLargeLen = 4096;
|
||||
|
||||
/* Basic info */
|
||||
|
||||
// Additional data is fetched from the Profile
|
||||
|
||||
// All
|
||||
[DataField]
|
||||
public int Height { get; private set; }
|
||||
public const int MaxHeight = 800;
|
||||
|
||||
[DataField]
|
||||
public int Weight { get; private set; }
|
||||
public const int MaxWeight = 300;
|
||||
|
||||
[DataField]
|
||||
public string EmergencyContactName { get; private set; }
|
||||
|
||||
// Employment
|
||||
[DataField]
|
||||
public bool HasWorkAuthorization { get; private set; }
|
||||
|
||||
// Security
|
||||
[DataField]
|
||||
public string IdentifyingFeatures { get; private set; }
|
||||
|
||||
// Medical
|
||||
[DataField]
|
||||
public string Allergies { get; private set; }
|
||||
[DataField]
|
||||
public string DrugAllergies { get; private set; }
|
||||
[DataField]
|
||||
public string PostmortemInstructions { get; private set; }
|
||||
// history, prescriptions, etc. would be a record below
|
||||
|
||||
// "incidents"
|
||||
[DataField, JsonIgnore]
|
||||
public List<RecordEntry> MedicalEntries { get; private set; }
|
||||
[DataField, JsonIgnore]
|
||||
public List<RecordEntry> SecurityEntries { get; private set; }
|
||||
[DataField, JsonIgnore]
|
||||
public List<RecordEntry> EmploymentEntries { get; private set; }
|
||||
|
||||
[DataDefinition]
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class RecordEntry
|
||||
{
|
||||
[DataField]
|
||||
public string Title { get; private set; }
|
||||
// players involved, can be left blank (or with a generic "CentCom" etc.) for backstory related issues
|
||||
[DataField]
|
||||
public string Involved { get; private set; }
|
||||
// Longer description of events.
|
||||
[DataField]
|
||||
public string Description { get; private set; }
|
||||
|
||||
public RecordEntry(string title, string involved, string desc)
|
||||
{
|
||||
Title = title;
|
||||
Involved = involved;
|
||||
Description = desc;
|
||||
}
|
||||
|
||||
public RecordEntry(RecordEntry other)
|
||||
: this(other.Title, other.Involved, other.Description)
|
||||
{
|
||||
}
|
||||
|
||||
public bool MemberwiseEquals(RecordEntry other)
|
||||
{
|
||||
return Title == other.Title && Involved == other.Involved && Description == other.Description;
|
||||
}
|
||||
|
||||
public void EnsureValid()
|
||||
{
|
||||
Title = ClampString(Title, TextMedLen);
|
||||
Involved = ClampString(Involved, TextMedLen);
|
||||
Description = ClampString(Description, TextVeryLargeLen);
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerProvidedCharacterRecords(
|
||||
bool hasWorkAuthorization,
|
||||
int height, int weight,
|
||||
string emergencyContactName,
|
||||
string identifyingFeatures,
|
||||
string allergies, string drugAllergies,
|
||||
string postmortemInstructions,
|
||||
List<RecordEntry> medicalEntries, List<RecordEntry> securityEntries, List<RecordEntry> employmentEntries)
|
||||
{
|
||||
HasWorkAuthorization = hasWorkAuthorization;
|
||||
Height = height;
|
||||
Weight = weight;
|
||||
EmergencyContactName = emergencyContactName;
|
||||
IdentifyingFeatures = identifyingFeatures;
|
||||
Allergies = allergies;
|
||||
DrugAllergies = drugAllergies;
|
||||
PostmortemInstructions = postmortemInstructions;
|
||||
MedicalEntries = medicalEntries;
|
||||
SecurityEntries = securityEntries;
|
||||
EmploymentEntries = employmentEntries;
|
||||
}
|
||||
|
||||
public PlayerProvidedCharacterRecords(PlayerProvidedCharacterRecords other)
|
||||
{
|
||||
Height = other.Height;
|
||||
Weight = other.Weight;
|
||||
EmergencyContactName = other.EmergencyContactName;
|
||||
HasWorkAuthorization = other.HasWorkAuthorization;
|
||||
IdentifyingFeatures = other.IdentifyingFeatures;
|
||||
Allergies = other.Allergies;
|
||||
DrugAllergies = other.DrugAllergies;
|
||||
PostmortemInstructions = other.PostmortemInstructions;
|
||||
MedicalEntries = other.MedicalEntries.Select(x => new RecordEntry(x)).ToList();
|
||||
SecurityEntries = other.SecurityEntries.Select(x => new RecordEntry(x)).ToList();
|
||||
EmploymentEntries = other.EmploymentEntries.Select(x => new RecordEntry(x)).ToList();
|
||||
}
|
||||
|
||||
public static PlayerProvidedCharacterRecords DefaultRecords()
|
||||
{
|
||||
return new PlayerProvidedCharacterRecords(
|
||||
hasWorkAuthorization: true,
|
||||
height: 170, weight: 70,
|
||||
emergencyContactName: "",
|
||||
identifyingFeatures: "",
|
||||
allergies: "None",
|
||||
drugAllergies: "None",
|
||||
postmortemInstructions: "Return home",
|
||||
medicalEntries: new List<RecordEntry>(),
|
||||
securityEntries: new List<RecordEntry>(),
|
||||
employmentEntries: new List<RecordEntry>()
|
||||
);
|
||||
}
|
||||
|
||||
public bool MemberwiseEquals(PlayerProvidedCharacterRecords other)
|
||||
{
|
||||
// This is ugly but is only used for integration tests.
|
||||
var test = Height == other.Height
|
||||
&& Weight == other.Weight
|
||||
&& EmergencyContactName == other.EmergencyContactName
|
||||
&& HasWorkAuthorization == other.HasWorkAuthorization
|
||||
&& IdentifyingFeatures == other.IdentifyingFeatures
|
||||
&& Allergies == other.Allergies
|
||||
&& DrugAllergies == other.DrugAllergies
|
||||
&& PostmortemInstructions == other.PostmortemInstructions;
|
||||
if (!test)
|
||||
return false;
|
||||
if (MedicalEntries.Count != other.MedicalEntries.Count)
|
||||
return false;
|
||||
if (SecurityEntries.Count != other.SecurityEntries.Count)
|
||||
return false;
|
||||
if (EmploymentEntries.Count != other.EmploymentEntries.Count)
|
||||
return false;
|
||||
if (MedicalEntries.Where((t, i) => !t.MemberwiseEquals(other.MedicalEntries[i])).Any())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (SecurityEntries.Where((t, i) => !t.MemberwiseEquals(other.SecurityEntries[i])).Any())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (EmploymentEntries.Where((t, i) => !t.MemberwiseEquals(other.EmploymentEntries[i])).Any())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string ClampString(string str, int maxLen)
|
||||
{
|
||||
if (str.Length > maxLen)
|
||||
{
|
||||
return str[..maxLen];
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
private static void EnsureValidEntries(List<RecordEntry> entries)
|
||||
{
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
entry.EnsureValid();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamp invalid entries to valid values
|
||||
/// </summary>
|
||||
public void EnsureValid()
|
||||
{
|
||||
Height = Math.Clamp(Height, 0, MaxHeight);
|
||||
Weight = Math.Clamp(Weight, 0, MaxWeight);
|
||||
EmergencyContactName =
|
||||
ClampString(EmergencyContactName, TextMedLen);
|
||||
IdentifyingFeatures = ClampString(IdentifyingFeatures, TextMedLen);
|
||||
Allergies = ClampString(Allergies, TextMedLen);
|
||||
DrugAllergies = ClampString(DrugAllergies, TextMedLen);
|
||||
PostmortemInstructions = ClampString(PostmortemInstructions, TextMedLen);
|
||||
|
||||
EnsureValidEntries(EmploymentEntries);
|
||||
EnsureValidEntries(MedicalEntries);
|
||||
EnsureValidEntries(SecurityEntries);
|
||||
}
|
||||
public PlayerProvidedCharacterRecords WithHeight(int height)
|
||||
{
|
||||
return new(this) { Height = height };
|
||||
}
|
||||
public PlayerProvidedCharacterRecords WithWeight(int weight)
|
||||
{
|
||||
return new(this) { Weight = weight };
|
||||
}
|
||||
public PlayerProvidedCharacterRecords WithWorkAuth(bool auth)
|
||||
{
|
||||
return new(this) { HasWorkAuthorization = auth };
|
||||
}
|
||||
public PlayerProvidedCharacterRecords WithContactName(string name)
|
||||
{
|
||||
return new(this) { EmergencyContactName = name};
|
||||
}
|
||||
public PlayerProvidedCharacterRecords WithIdentifyingFeatures(string feat)
|
||||
{
|
||||
return new(this) { IdentifyingFeatures = feat};
|
||||
}
|
||||
public PlayerProvidedCharacterRecords WithAllergies(string s)
|
||||
{
|
||||
return new(this) { Allergies = s };
|
||||
}
|
||||
public PlayerProvidedCharacterRecords WithDrugAllergies(string s)
|
||||
{
|
||||
return new(this) { DrugAllergies = s };
|
||||
}
|
||||
public PlayerProvidedCharacterRecords WithPostmortemInstructions(string s)
|
||||
{
|
||||
return new(this) { PostmortemInstructions = s};
|
||||
}
|
||||
public PlayerProvidedCharacterRecords WithEmploymentEntries(List<RecordEntry> entries)
|
||||
{
|
||||
return new(this) { EmploymentEntries = entries};
|
||||
}
|
||||
public PlayerProvidedCharacterRecords WithMedicalEntries(List<RecordEntry> entries)
|
||||
{
|
||||
return new(this) { MedicalEntries = entries};
|
||||
}
|
||||
public PlayerProvidedCharacterRecords WithSecurityEntries(List<RecordEntry> entries)
|
||||
{
|
||||
return new(this) { SecurityEntries = entries};
|
||||
}
|
||||
}
|
||||
|
||||
public enum CharacterRecordType : byte
|
||||
{
|
||||
Employment, Medical, Security
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
using Content.Shared.Security;
|
||||
using Content.Shared.StationRecords;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared._CD.Records;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum CharacterRecordConsoleKey : byte
|
||||
{
|
||||
Key
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum RecordConsoleType : byte
|
||||
{
|
||||
Security,
|
||||
Medical,
|
||||
Employment,
|
||||
/// <summary>
|
||||
/// Admin console has the functionality of all other types and has some additional admin related functionality
|
||||
/// </summary>
|
||||
Admin
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class CharacterRecordConsoleState : BoundUserInterfaceState
|
||||
{
|
||||
[Serializable, NetSerializable]
|
||||
public struct CharacterInfo
|
||||
{
|
||||
public string CharacterDisplayName;
|
||||
public uint? StationRecordKey;
|
||||
}
|
||||
|
||||
public RecordConsoleType ConsoleType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Character selected in the console
|
||||
/// </summary>
|
||||
public uint? SelectedIndex { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// List of names+station record keys to display in the listing
|
||||
/// </summary>
|
||||
public Dictionary<uint, CharacterInfo>? CharacterList { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The contents of the selected record
|
||||
/// </summary>
|
||||
public FullCharacterRecords? SelectedRecord { get; set; } = null;
|
||||
|
||||
public StationRecordsFilter? Filter { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Security status of the selected record
|
||||
/// </summary>
|
||||
public (SecurityStatus, string?)? SelectedSecurityStatus = null;
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class CharacterRecordsConsoleFilterMsg : BoundUserInterfaceMessage
|
||||
{
|
||||
public readonly StationRecordsFilter? Filter;
|
||||
|
||||
public CharacterRecordsConsoleFilterMsg(StationRecordsFilter? filter)
|
||||
{
|
||||
Filter = filter;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class CharacterRecordConsoleSelectMsg : BoundUserInterfaceMessage
|
||||
{
|
||||
public readonly uint? CharacterRecordKey;
|
||||
|
||||
public CharacterRecordConsoleSelectMsg(uint? characterRecordKey)
|
||||
{
|
||||
CharacterRecordKey = characterRecordKey;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
admin-player-actions-window-cd-record-purge = Modify Character Records
|
||||
cd-actions-admin-modify-records = Modify Character Records
|
||||
cd-actions-admin-modify-reset = Reset Records
|
||||
cd-actions-admin-modify-del-entry = Delete Entry
|
||||
|
||||
cd-eventpreferencepanel-title = Event Preference View
|
||||
cd-eventpreferencepanel-player = Player: {$player}
|
||||
cd-eventpreferencepanel-character = Character: {$characterName}
|
||||
|
||||
cmd-eventpreferences-help = Usage: eventpreferences <name>
|
||||
cmd-eventpreferences-desc = Displays a player's current character's event preferences (i.e. antags)
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
guide-entry-cd-records = Character Records
|
||||
guide-entry-rules-cd = Cosmatic Drift Rules
|
||||
|
||||
guide-entry-cd = Cosmatic Drift Content
|
||||
guide-entry-cd-newplayer = Welcome to Cosmatic Drift!
|
||||
|
||||
guide-entry-rules-ic = In Character Policy
|
||||
guide-entry-rules-sop-core = Standard Operating Procedure
|
||||
guide-entry-rules-sop-restricted = List of Restricted Items
|
||||
guide-entry-rules-sop-command = Command
|
||||
guide-entry-rules-sop-sec = Security
|
||||
guide-entry-rules-sop-engi = Engineering
|
||||
guide-entry-rules-sop-med = Medical
|
||||
guide-entry-rules-sop-sci = Science
|
||||
guide-entry-rules-sop-cargo = Cargo
|
||||
guide-entry-rules-sop-service = Service
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
humanoid-profile-editor-height-label = Height:
|
||||
humanoid-profile-editor-reset-height-button = Reset
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
ent-CriminalRecordsComputerCircuitboard = security records computer board
|
||||
.desc = A computer printed circuit board for a security records computer.
|
||||
|
||||
ent-StationRecordsComputerCircuitboard = employment records computer board
|
||||
.desc = A computer printed circuit board for a employment records computer.
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
# Records editor
|
||||
humanoid-profile-editor-cd-records-tab = Records
|
||||
|
||||
# General
|
||||
humanoid-profile-editor-cd-records-height = Height (cm):
|
||||
humanoid-profile-editor-cd-records-weight = Weight (kg):
|
||||
humanoid-profile-editor-cd-records-contact-name = Emergency Contact Names(s):
|
||||
|
||||
# Employment
|
||||
humanoid-profile-editor-cd-records-employment = Employment
|
||||
humanoid-profile-editor-cd-records-work-authorization = Work Authorization:
|
||||
|
||||
# Security
|
||||
humanoid-profile-editor-cd-records-identifying-features = Identifying Features:
|
||||
|
||||
# Medical
|
||||
humanoid-profile-editor-cd-records-allergies = Allergies:
|
||||
humanoid-profile-editor-cd-records-drug-allergies = Drug Allergies:
|
||||
humanoid-profile-editor-cd-records-postmortem = Postmortem Instructions:
|
||||
|
||||
# Entries
|
||||
humanoid-profile-editor-cd-records-add-entry = Add Entry
|
||||
humanoid-profile-editor-cd-records-edit-entry = Edit Entry
|
||||
humanoid-profile-editor-cd-records-view-entry = View Entry
|
||||
humanoid-profile-editor-cd-records-remove-entry = Remove Entry
|
||||
humanoid-profile-editor-cd-records-up = Up
|
||||
humanoid-profile-editor-cd-records-down = Down
|
||||
|
||||
cd-records-entry-edit-popup-title = View/Edit Entry
|
||||
cd-records-entry-edit-popup-save = Save
|
||||
cd-records-entry-default-title = Untitled Entry
|
||||
|
||||
cd-records-entry-edit-popup-title-placeholder = Entry Title
|
||||
cd-records-entry-edit-popup-involved-placeholder = Author(s)
|
||||
cd-records-entry-edit-popup-description-placeholder = Description
|
||||
|
||||
cd-records-entry-edit-popup-title-required = Title is required
|
||||
cd-records-entry-edit-popup-involved-required = Author(s) is required
|
||||
cd-records-entry-edit-popup-description-required = Description is required
|
||||
cd-records-entry-edit-popup-description-too-long = Description is too long! ({$current}/{$max} characters)
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
cd-character-records-viewer-title-employ = Employment Records
|
||||
cd-character-records-viewer-title-sec = Security Records
|
||||
cd-character-records-viewer-title-med = Medical Records
|
||||
|
||||
cd-record-viewer-empty-state = Cannot fetch records.
|
||||
cd-record-viewer-no-record-selected = Please select record.
|
||||
|
||||
cd-character-records-viewer-record-age = Age:
|
||||
cd-character-records-viewer-record-job = Job:
|
||||
cd-character-records-viewer-record-gender = Gender:
|
||||
cd-character-records-viewer-record-species = Species:
|
||||
|
||||
cd-character-records-viewer-record-sec-fingerprint = Fingerprint:
|
||||
cd-character-records-viewer-record-sec-dna = DNA:
|
||||
|
||||
cd-character-records-viewer-unknown = ERROR UNKNOWN
|
||||
|
||||
cd-character-records-viewer-record-med-sex = Sex:
|
||||
|
||||
cd-character-records-viewer-view-entry = View
|
||||
cd-records-entry-view-popup-title = View Entry
|
||||
|
||||
cd-character-records-entry-view-title = Title:
|
||||
cd-character-records-entry-view-involved = Author(s):
|
||||
|
||||
cd-character-records-viewer-setwanted-placeholder = Reason
|
||||
|
|
@ -52,6 +52,10 @@
|
|||
type: CargoOrderConsoleBoundUserInterface
|
||||
enum.CrewMonitoringUIKey.Key:
|
||||
type: CrewMonitoringBoundUserInterface
|
||||
# Begin CD - Character Records
|
||||
enum.CharacterRecordConsoleKey.Key:
|
||||
type: CharacterRecordConsoleBoundUserInterface
|
||||
# End CD - Character Records
|
||||
enum.GeneralStationRecordConsoleKey.Key:
|
||||
# who the fuck named this bruh
|
||||
type: GeneralStationRecordConsoleBoundUserInterface
|
||||
|
|
@ -69,6 +73,10 @@
|
|||
toggleAction: ActionAGhostShowCrewMonitoring
|
||||
enum.GeneralStationRecordConsoleKey.Key:
|
||||
toggleAction: ActionAGhostShowStationRecords
|
||||
# Begin CD - Character Records
|
||||
enum.CharacterRecordConsoleKey.Key:
|
||||
toggleAction: ActionAGhostShowCharacterRecords
|
||||
# End CD - Character Records
|
||||
- type: SolarControlConsole # look ma i AM the computer!
|
||||
- type: CommunicationsConsole
|
||||
title: comms-console-announcement-title-centcom
|
||||
|
|
@ -83,6 +91,11 @@
|
|||
- type: CrewMonitoringConsole
|
||||
- type: GeneralStationRecordConsole
|
||||
canDeleteEntries: true
|
||||
# Begin CD - Character Records
|
||||
- type: CharacterRecordConsole
|
||||
consoleType: Admin
|
||||
- type: CriminalRecordsConsole
|
||||
# End CD - Character Records
|
||||
- type: DeviceNetwork
|
||||
deviceNetId: Wireless
|
||||
receiveFrequencyId: CrewMonitor
|
||||
|
|
@ -174,3 +187,18 @@
|
|||
keywords: [ "AI", "console", "interface" ]
|
||||
priority: -7
|
||||
event: !type:ToggleIntrinsicUIEvent { key: enum.GeneralStationRecordConsoleKey.Key }
|
||||
|
||||
# Begin CD - Character Records
|
||||
- type: entity
|
||||
id: ActionAGhostShowCharacterRecords
|
||||
name: Character Records Interface
|
||||
description: View all of the character records
|
||||
categories: [ HideSpawnMenu ]
|
||||
components:
|
||||
- type: InstantAction
|
||||
icon: { sprite: Structures/Machines/parts.rsi, state: box_0 }
|
||||
iconOn: Structures/Machines/parts.rsi/box_2.png
|
||||
keywords: [ "AI", "console", "interface" ]
|
||||
priority: -10
|
||||
event: !type:ToggleIntrinsicUIEvent { key: enum.CharacterRecordConsoleKey.Key }
|
||||
# Edn CD - Character Records
|
||||
|
|
|
|||
|
|
@ -360,6 +360,16 @@
|
|||
color: "#1f8c28"
|
||||
- type: Computer
|
||||
board: MedicalRecordsComputerCircuitboard
|
||||
# Begin CD - Character Records
|
||||
- type: CharacterRecordConsole
|
||||
consoleType: Medical
|
||||
- type: UserInterface
|
||||
interfaces:
|
||||
enum.CharacterRecordConsoleKey.Key:
|
||||
type: CharacterRecordConsoleBoundUserInterface
|
||||
- type: ActivatableUI
|
||||
key: enum.CharacterRecordConsoleKey.Key
|
||||
# End CD - Character Records
|
||||
|
||||
- type: entity
|
||||
parent: BaseComputerAiAccess
|
||||
|
|
@ -370,12 +380,18 @@
|
|||
- type: CriminalRecordsConsole
|
||||
- type: UserInterface
|
||||
interfaces:
|
||||
enum.CriminalRecordsConsoleKey.Key:
|
||||
type: CriminalRecordsConsoleBoundUserInterface
|
||||
# Begin CD - Character Records
|
||||
enum.CharacterRecordConsoleKey.Key:
|
||||
type: CharacterRecordConsoleBoundUserInterface
|
||||
# End CD - Character Records
|
||||
enum.WiresUiKey.Key:
|
||||
type: WiresBoundUserInterface
|
||||
# Begin CD - Character Records
|
||||
- type: CharacterRecordConsole
|
||||
consoleType: Security
|
||||
- type: ActivatableUI
|
||||
key: enum.CriminalRecordsConsoleKey.Key
|
||||
key: enum.CharacterRecordConsoleKey.Key
|
||||
# End CD - Character Records
|
||||
- type: Sprite
|
||||
layers:
|
||||
- map: ["computerLayerBody"]
|
||||
|
|
@ -409,12 +425,18 @@
|
|||
- type: GeneralStationRecordConsole
|
||||
- type: UserInterface
|
||||
interfaces:
|
||||
enum.GeneralStationRecordConsoleKey.Key:
|
||||
type: GeneralStationRecordConsoleBoundUserInterface
|
||||
# Begin CD - Character Records
|
||||
enum.CharacterRecordConsoleKey.Key:
|
||||
type: CharacterRecordConsoleBoundUserInterface
|
||||
# End CD - Character Records
|
||||
enum.WiresUiKey.Key:
|
||||
type: WiresBoundUserInterface
|
||||
# Begin CD - Character Records
|
||||
- type: CharacterRecordConsole
|
||||
consoleType: Employment
|
||||
- type: ActivatableUI
|
||||
key: enum.GeneralStationRecordConsoleKey.Key
|
||||
key: enum.CharacterRecordConsoleKey.Key
|
||||
# End CD - Character Records
|
||||
- type: PointLight
|
||||
radius: 1.5
|
||||
energy: 1.6
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@
|
|||
text: "/ServerInfo/Guidebook/NewPlayer/CharacterCreation.xml"
|
||||
children:
|
||||
- YourFirstCharacter
|
||||
- Species
|
||||
- Species
|
||||
- Records # DeltaV - CD records page goes here
|
||||
|
||||
- type: guideEntry
|
||||
id: YourFirstCharacter
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
femaleFirstNames: NamesOniFemale
|
||||
lastNames: NamesOniLocation
|
||||
naming: LastNoFirst
|
||||
baseScale: "1.2, 1.2"
|
||||
|
||||
- type: markingPoints
|
||||
id: MobOniMarkingLimits
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
markingLimits: MobFelinidMarkingLimits
|
||||
dollPrototype: MobFelinidDummy
|
||||
skinColoration: Hues # DeltaV
|
||||
baseScale: "0.8, 0.8"
|
||||
|
||||
- type: markingPoints
|
||||
id: MobFelinidMarkingLimits
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
- type: guideEntry
|
||||
id: Records
|
||||
name: guide-entry-cd-records
|
||||
text: "/ServerInfo/Guidebook/_CD/Records.xml"
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
markingLimits: MobHarpyMarkingLimits
|
||||
dollPrototype: MobHarpyDummy
|
||||
skinColoration: HumanToned
|
||||
baseScale: "0.9, 0.9"
|
||||
|
||||
- type: speciesBaseSprites
|
||||
id: MobHarpySprites
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
femaleFirstNames: NamesRodentiaFemale
|
||||
lastNames: NamesRodentiaLast
|
||||
naming: LastFirst
|
||||
baseScale: "0.8, 0.8"
|
||||
|
||||
- type: speciesBaseSprites
|
||||
id: MobRodentiaSprites
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
- Male
|
||||
- Female
|
||||
- Unsexed
|
||||
baseScale: "1, 1.05" # DeltaV - make sure Thaven are still scaled w/ CD records
|
||||
|
||||
- type: speciesBaseSprites
|
||||
id: MobThavenSprites
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
<Document>
|
||||
|
||||
## Character Records
|
||||
|
||||
You can create records for you characters in the records tab of the character
|
||||
creator. Just simply fill in the values and save. Entries should typically be
|
||||
written from the perspective of a CentCom official.
|
||||
|
||||
## Employment Entries
|
||||
|
||||
Employment Entries should include your characters education, previous jobs, and
|
||||
anything that you think NanoTrasen would log about your characters past that is
|
||||
not security related.
|
||||
|
||||
## Medical Entries
|
||||
|
||||
Medical Entries should include anything a doctor should know before treating
|
||||
your character, major surgeries, prescriptions, and any psychological evaluations.
|
||||
|
||||
## Security Entries
|
||||
|
||||
Security Entries should include any major arrests/incidents your character has
|
||||
had in their past or while working with NanoTrasen. Do not log minor incidents.
|
||||
|
||||
## Accessing Records
|
||||
You can access the records of your crewmates in game from their respective consoles.
|
||||
|
||||
<Box>
|
||||
<GuideEntityEmbed Entity="ComputerMedicalRecords" Caption="Medical"/>
|
||||
<GuideEntityEmbed Entity="ComputerCriminalRecords" Caption="Security"/>
|
||||
<GuideEntityEmbed Entity="ComputerStationRecords" Caption="Station"/>
|
||||
</Box>
|
||||
|
||||
It is currently not possible to create new records in game. This is something
|
||||
that will be added in the future. If there is something that occurs in the round
|
||||
that you think is relevant for a record entry, it is on you to write it at the
|
||||
moment. Keep in mind that records should typically be written from the
|
||||
perspective of some CentCom official.
|
||||
|
||||
</Document>
|
||||
Loading…
Reference in New Issue