Merge branch 'KitsuneV4' of https://github.com/AeraAuling/Delta-v-stuff into KitsuneV4

This commit is contained in:
Aera Aulin 2025-03-13 01:14:41 -07:00
commit a1480c1ffd
105 changed files with 9887 additions and 989 deletions

View File

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

View File

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

View File

@ -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'}" />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
namespace Content.Server._CD.Records;
[RegisterComponent]
public sealed partial class SkipLoadingCharacterRecordsComponent : Component;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -161,4 +161,13 @@ public sealed class DCCVars
/// </summary>
public static readonly CVarDef<bool> EnableBacktoBack =
CVarDef.Create("game.disable_preset_test", false, CVar.SERVERONLY);
/* Laying down combat */
/// <summary>
/// Modifier to apply to all melee attacks when laying down.
/// Don't increase this above 1...
/// </summary>
public static readonly CVarDef<float> LayingDownMeleeMod =
CVarDef.Create("game.laying_down_melee_mod", 0.25f, CVar.REPLICATED);
}

View File

@ -0,0 +1,51 @@
using Content.Shared.DoAfter;
using Content.Shared.Whitelist;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared._DV.Construction;
/// <summary>
/// Component for an upgrade kit that upgrades allowed machines then deletes itself.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(UpgradeKitSystem))]
public sealed partial class UpgradeKitComponent : Component
{
/// <summary>
/// A whitelist that entities must match to be upgraded.
/// </summary>
[DataField(required: true)]
public EntityWhitelist Whitelist = new();
/// <summary>
/// A blacklist that entities cannot match to be upgraded.
/// </summary>
[DataField(required: true)]
public EntityWhitelist Blacklist = new();
/// <summary>
/// Components added to the machine after it's upgraded.
/// Some of these must blacklist it from upgrades to prevent stacking.
/// </summary>
[DataField(required: true)]
public ComponentRegistry Components = new();
/// <summary>
/// How long the doafter is
/// </summary>
[DataField]
public TimeSpan Delay = TimeSpan.FromSeconds(4);
/// <summary>
/// Sound played when upgrading an entity.
/// </summary>
[DataField]
public SoundSpecifier? UpgradeSound = new SoundPathSpecifier("/Audio/Items/rped.ogg");
public EntityUid? SoundStream;
}
[Serializable, NetSerializable]
public sealed partial class UpgradeKitDoAfterEvent : SimpleDoAfterEvent;

View File

@ -0,0 +1,89 @@
using Content.Shared.DoAfter;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Content.Shared.Whitelist;
using Content.Shared.Wires;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Network;
namespace Content.Shared._DV.Construction;
/// <summary>
/// Handles upgrading machines using upgrade kits.
/// </summary>
public sealed class UpgradeKitSystem : EntitySystem
{
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedWiresSystem _wires = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<UpgradeKitComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<UpgradeKitComponent, UpgradeKitDoAfterEvent>(OnDoAfter);
}
private void OnAfterInteract(Entity<UpgradeKitComponent> ent, ref AfterInteractEvent args)
{
if (args.Handled || !args.CanReach || args.Target is not {} target)
return;
args.Handled = true;
var user = args.User;
if (!CanUpgrade(ent, target, user))
return;
if (!_wires.IsPanelOpen(target))
{
_popup.PopupClient(Loc.GetString("construction-step-condition-wire-panel-open"), target, user);
return;
}
ent.Comp.SoundStream = _audio.PlayPredicted(ent.Comp.UpgradeSound, ent, user)?.Entity;
Dirty(ent);
var ev = new UpgradeKitDoAfterEvent();
_doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, ent.Comp.Delay, ev, ent, target, ent));
}
private void OnDoAfter(Entity<UpgradeKitComponent> ent, ref UpgradeKitDoAfterEvent args)
{
ent.Comp.SoundStream = _audio.Stop(ent.Comp.SoundStream);
if (args.Cancelled)
return;
if (args.Handled || args.Args.Target is not {} target)
return;
args.Handled = true;
var user = args.Args.User;
if (!CanUpgrade(ent, target, user))
return;
// do the upgrading now
EntityManager.AddComponents(target, ent.Comp.Components);
if (_net.IsServer)
QueueDel(ent);
}
/// <summary>
/// Check the upgrade kit's whitelist and blacklist, showing a popup if it is invalid.
/// </summary>
public bool CanUpgrade(Entity<UpgradeKitComponent> ent, EntityUid target, EntityUid user)
{
if (_whitelist.IsWhitelistFail(ent.Comp.Whitelist, target) ||
_whitelist.IsBlacklistPass(ent.Comp.Blacklist, target))
{
_popup.PopupClient(Loc.GetString("upgrade-kit-invalid-target"), target, user);
return false;
}
return true;
}
}

View File

@ -0,0 +1,17 @@
using Robust.Shared.GameStates;
namespace Content.Shared._DV.Construction;
/// <summary>
/// Component added to machines to prevent stacking upgrades and show what upgrade they have.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(UpgradedMachineSystem))]
[AutoGenerateComponentState]
public sealed partial class UpgradedMachineComponent : Component
{
/// <summary>
/// The string to show when examined.
/// </summary>
[DataField(required: true), AutoNetworkedField]
public LocId Upgrade;
}

View File

@ -0,0 +1,21 @@
using Content.Shared.Examine;
namespace Content.Shared._DV.Construction;
public sealed class UpgradedMachineSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<UpgradedMachineComponent, ExaminedEvent>(OnExamined);
}
private void OnExamined(Entity<UpgradedMachineComponent> ent, ref ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
args.PushMarkup(Loc.GetString(ent.Comp.Upgrade));
}
}

View File

@ -0,0 +1,78 @@
using System.Linq;
using Content.Shared.EntityEffects;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Robust.Shared.Prototypes;
using Robust.Shared.Localization;
using Content.Shared.Body.Part;
using Content.Shared.Body.Systems;
namespace Content.Shared._DV.EntityEffects.EffectConditions;
/// <summary>
/// Reagent effect condition that depends on if the entity has a given component(s), potentially on a body part
/// </summary>
public sealed partial class HasComponent : EntityEffectCondition
{
/// <summary>
/// The set of components that this condition cares about
/// </summary>
[DataField(required: true)]
public ComponentRegistry Components;
/// <summary>
/// Whether or not the given components should be present
/// </summary>
[DataField]
public bool ShouldHave = true;
/// <summary>
/// Whether the check is an existential or universal check
/// </summary>
[DataField]
public bool ConsiderAll;
/// <summary>
/// The explanation displayed in the guidebook for this condition
/// </summary>
[DataField(required: true)]
public LocId Explanation;
/// <summary>
/// The body part of the entity to test for the components
/// </summary>
[DataField]
public BodyPartType? BodyPart;
/// <summary>
/// The side of the entity's body to test for the components
/// </summary>
[DataField]
public BodyPartSymmetry? BodyPartSymmetry;
public override bool Condition(EntityEffectBaseArgs args)
{
var _body = args.EntityManager.System<SharedBodySystem>();
var entity =
BodyPart is {} bodyPart
? _body.GetBodyChildrenOfType(args.TargetEntity, bodyPart, symmetry: BodyPartSymmetry).Select(it => it.Id).FirstOrDefault()
: args.TargetEntity;
if (!entity.IsValid())
{
return !ShouldHave;
}
var tested =
ConsiderAll
? Components.Values.All(c => args.EntityManager.HasComponent(entity, c.Component.GetType()))
: Components.Values.Any(c => args.EntityManager.HasComponent(entity, c.Component.GetType()));
return tested ^ !ShouldHave;
}
public override string GuidebookExplanation(IPrototypeManager prototype)
{
return Loc.GetString(Explanation);
}
}

View File

@ -0,0 +1,20 @@
using Robust.Shared.GameStates;
namespace Content.Shared._DV.Lathe;
/// <summary>
/// Any non-null fields get copied onto LatheComponent at MapInit.
/// Gets removed from the entity after its work is done.
/// </summary>
/// <remarks>
/// Only exists because ComponentRegistry / AddComponent bulldozes existing fields unlike prototype composition.
/// </remarks>
[RegisterComponent, NetworkedComponent, Access(typeof(LatheUpgradeSystem))]
public sealed partial class LatheUpgradeComponent : Component
{
[DataField]
public float? TimeMultiplier;
[DataField]
public float? MaterialUseMultiplier;
}

View File

@ -0,0 +1,31 @@
using Content.Shared.Lathe;
namespace Content.Shared._DV.Lathe;
/// <summary>
/// Applies <see cref="LatheUpgradeComponent"/> modifiers when added to a lathe and removes it.
/// </summary>
public sealed class LatheUpgradeSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<LatheUpgradeComponent, MapInitEvent>(OnMapInit);
}
private void OnMapInit(Entity<LatheUpgradeComponent> ent, ref MapInitEvent args)
{
RemCompDeferred<LatheUpgradeComponent>(ent);
if (!TryComp<LatheComponent>(ent, out var lathe))
return;
if (ent.Comp.MaterialUseMultiplier is {} matMul)
lathe.MaterialUseMultiplier = matMul;
if (ent.Comp.TimeMultiplier is {} timeMul)
lathe.TimeMultiplier = timeMul;
Dirty(ent, lathe);
}
}

View File

@ -0,0 +1,47 @@
using Content.Shared._DV.CCVars;
using Content.Shared._White.Standing;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Standing;
using Content.Shared.Weapons.Melee.Events;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
namespace Content.Shared._DV.Standing;
/// <summary>
/// Prevents shooting and makes melee weaker while you are laying down (R)
/// </summary>
public sealed class LayingDownCombatSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly StandingStateSystem _standing = default!;
private DamageModifierSet _meleeMod = new();
public override void Initialize()
{
base.Initialize();
// subscribe to LayingDownComponent instead of StandingState so it only applies to mobs that can lie down on keypress
SubscribeLocalEvent<LayingDownComponent, GetMeleeDamageEvent>(OnGetMeleeDamage);
Subs.CVar(_cfg, DCCVars.LayingDownMeleeMod, mod =>
{
_meleeMod.Coefficients.Clear();
foreach (var proto in _proto.EnumeratePrototypes<DamageTypePrototype>())
{
_meleeMod.Coefficients.Add(proto.ID, mod);
}
}, true);
}
private void OnGetMeleeDamage(Entity<LayingDownComponent> ent, ref GetMeleeDamageEvent args)
{
if (!_standing.IsDown(ent))
return;
args.Modifiers.Add(_meleeMod);
}
}

View File

@ -110,7 +110,7 @@ public sealed class DroppableBorgModuleSystem : EntitySystem
if (hand?.HeldEntity is { } item)
QueueDel(item);
else if (!TerminatingOrDeleted(chassis) && Transform(chassis).MapID != MapId.Nullspace) // don't care if its empty if the server is shutting down
Log.Error($"Borg {ToPrettyString(chassis)} terminated with empty hand {i} in {ToPrettyString(ent)}");
Log.Warning($"Borg {ToPrettyString(chassis)} terminated with empty hand {i} in {ToPrettyString(ent)}");
_hands.RemoveHand(chassis, handId, hands);
}
return;

View File

@ -1,105 +1,4 @@
Entries:
- author: MilonPL
changes:
- message: Fixed the sudden TPS drops, please report any lag issues in the Discord!
type: Fix
id: 632
time: '2024-10-31T07:18:08.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2075
- author: reesque
changes:
- message: pie not dropping tin on thrown
type: Fix
id: 633
time: '2024-11-01T13:29:38.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2082
- author: Radezolid
changes:
- message: Fixed the Mail Carrier PDA not coming with the MailMetrics program!
type: Fix
id: 634
time: '2024-11-02T02:21:28.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2086
- author: Radezolid
changes:
- message: Added an Agent ID to LPO for them to fake signatures, plus a backpack
to store all those illegal documents!.
type: Add
id: 635
time: '2024-11-02T14:21:28.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2009
- author: Lyndomen
changes:
- message: Nuclear Operative Agent now requires 4 hours of Chemist playtime.
type: Tweak
id: 636
time: '2024-11-03T06:08:01.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2098
- author: Radezolid
changes:
- message: Now the prosecutor, clerk and chief justice PDAs comes with a SecWatch
program installed.
type: Tweak
id: 637
time: '2024-11-03T07:22:26.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2085
- author: Eternally-Confused
changes:
- message: Canes can now be selected in loadouts.
type: Add
id: 638
time: '2024-11-03T09:26:52.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2095
- author: deltanedas
changes:
- message: Prosecutors can now add their overcoat to loadouts.
type: Add
id: 639
time: '2024-11-03T09:40:39.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/1923
- author: Radezolid
changes:
- message: You can use emotes while sleeping to enhance narcoleptic RP.
type: Tweak
id: 640
time: '2024-11-03T15:33:52.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2084
- author: deltanedas
changes:
- message: You can no longer eat brains, they aren't allowed to remember who killed
them anyway!
type: Tweak
id: 641
time: '2024-11-03T17:27:55.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2102
- author: Radezolid
changes:
- message: Medibots now smile!
type: Tweak
id: 642
time: '2024-11-03T17:40:49.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2087
- author: beck-thompson
changes:
- message: Syrinx implant now works correctly
type: Fix
id: 643
time: '2024-11-03T18:02:03.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2055
- author: Monotheonist
changes:
- message: Changed Delta-V cartridges to tapes.
type: Tweak
id: 644
time: '2024-11-04T02:03:38.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2108
- author: Radezolid
changes:
- message: Added prescription security glasses and prescription corpsman glasses!
type: Add
id: 645
time: '2024-11-04T07:39:15.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2083
- author: clinux
changes:
- message: Removed the ability to upgrade reinforced windows to into shuttle windows
@ -3939,3 +3838,119 @@
id: 1131
time: '2025-03-10T07:31:34.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/3178
- author: deltanedas
changes:
- message: Added lathe upgrade kits to the Industrial Engineering tech, they can
be used on any kind of lathe including techfabs.
type: Add
- message: Added the cryo-stabilizing lathe upgrade kit, it makes lathes faster
in exchange for freezing the air.
type: Add
id: 1132
time: '2025-03-10T19:26:33.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/3169
- author: deltanedas
changes:
- message: Added a new T3 industrial tech, Matter-Energy Conversion, which lets
you make self-recharging RCDs.
type: Add
- message: The borg RCD module now has to be reloaded with compressed matter or
replaced with a different RCD.
type: Tweak
id: 1133
time: '2025-03-10T20:19:17.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/3168
- author: Field Command
changes:
- message: Pebble is now as compliant as it can be with current mapping standards
type: Fix
id: 1134
time: '2025-03-10T23:55:51.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/3186
- author: sowelipililimute
changes:
- message: Departamental lathes can be emagged for access to weapons
type: Add
id: 1135
time: '2025-03-11T04:42:17.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/3146
- author: BlitzTheSquishy
changes:
- message: Feroxi now get tinned meat in their survival boxes instead of PSB, except
you mimes, you get to keep your freshly baked bread
type: Tweak
id: 1136
time: '2025-03-11T04:50:36.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/3191
- author: Radezolid
changes:
- message: The overlord board price is back to 10 TC with a possible discount to
7 TC.
type: Tweak
id: 1137
time: '2025-03-11T04:54:23.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/3167
- author: deltanedas
changes:
- message: Fixed not being able to install the escalation cyborg module.
type: Fix
id: 1138
time: '2025-03-11T05:22:28.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/3193
- author: Radezolid
changes:
- message: A salvage expeditions computer board spawns inside the LO's locker now.
type: Tweak
- message: Removed the salvage expeditions computer board from research and lathes.
type: Remove
id: 1139
time: '2025-03-11T15:23:27.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/3166
- author: dffdff2423, Aquif, MilonPL, sowelipililimute, Lyndomen
changes:
- message: You can now create medical, security, and employment records in the character
editor. They can be accessed in game from their respective consoles. Ported
from Cosmatic Drift with love.
type: Add
- message: The station records console is now the employment records console. Please
use the security/criminal records console to match DNA/Fingerprints. Ported
from Cosmatic Drift with appreciation.
type: Tweak
id: 1140
time: '2025-03-11T15:34:45.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/2236
- author: KZhProgram
changes:
- message: MOTH CAN NOW EAT PILLS!! YIPPEEEEE
type: Add
id: 1141
time: '2025-03-11T21:06:33.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/3199
- author: deltanedas
changes:
- message: Fixed harpies being immune to anomaly infections.
type: Fix
id: 1142
time: '2025-03-11T22:06:04.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/3201
- author: KZhProgram
changes:
- message: Fixed feroxi not being able to eat prepackaged sustenance bars!
type: Fix
id: 1143
time: '2025-03-12T00:15:11.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/3202
- author: deltanedas
changes:
- message: Melee attacks made when laying down are much weaker.
type: Tweak
id: 1144
time: '2025-03-12T00:26:20.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/3173
- author: Lyndomen
changes:
- message: Survival has gotten 20% less spicy
type: Tweak
id: 1145
time: '2025-03-12T15:11:15.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/3210

View File

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

View File

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

View File

@ -0,0 +1,2 @@
humanoid-profile-editor-height-label = Height:
humanoid-profile-editor-reset-height-button = Reset

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
gas-pipe-sensor-control-oxygen = Oxygen control
gas-pipe-sensor-control-nitrogen = Nitrogen control
gas-pipe-sensor-control-overpressure = Overpressure control

View File

@ -0,0 +1,4 @@
upgrade-kit-invalid-target = You can't upgrade that!
lathe-upgrade-kit-hyper-convection = [color=cyan]Hyper-convection[/color]: [color=green]Saves resources[/color] in exchange for [color=red]being slower and producing heat[/color].
lathe-upgrade-kit-cryo-stabilizing = [color=cyan]Cryo-stabilizing[/color]: [color=green]Works faster[/color] in exchange for [color=red]freezing the area[/color].

View File

@ -12,4 +12,4 @@ ui-options-function-nano-chat-navigate-up-unread = Navigate up to next unread
ui-options-function-nano-chat-navigate-down-unread = Navigate down to next unread
## DeltaV staff chats
ui-options-function-open-staff-chats-deltav = Open admin help and curator chat
ui-options-function-open-a-help-curator-chat = Open admin help and curator chat

View File

@ -1,5 +1,6 @@
# Industrial
research-technology-aerial-extraction = Aerial Extraction
research-technology-matter-energy-conversion = Matter-Energy Conversion
# Civilian
research-technology-syringe-gun = Syringe Gun

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,7 @@
- ClothMade
- Fruit # DeltaV - Moth can eat fruits.
- Paper
- Pill # DeltaV - Moth can eat pills.
- type: SolutionContainerManager
solutions:
stomach:

View File

@ -1017,9 +1017,9 @@
productEntity: OverlordCircuitBoard # DeltaV: replaced antimov with overlord
discountCategory: usualDiscounts
discountDownTo:
Telecrystal: 16 # DeltaV: was 10
Telecrystal: 7 # DeltaV: was 10
cost:
Telecrystal: 20 # DeltaV: was 14
Telecrystal: 10 # DeltaV: was 14
categories:
- UplinkDisruption
conditions:

View File

@ -313,6 +313,7 @@
- AllAccessBorg
- type: AccessReader
access: [["Command"], ["Research"]]
breakOnAccessBreaker: false # DeltaV - prevent emag metachecking by unlocking without an id
- type: SlavedBorg # DeltaV: NT borgs are enslaved to the AI by default
law: ObeyAI
- type: ShowJobIcons

View File

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

View File

@ -11,10 +11,6 @@
MatterBin: 3
Manipulator: 1
Glass: 1
- type: ReverseEngineering # DeltaV
difficulty: 2
recipes:
- AutolatheHyperConvectionMachineCircuitboard
- type: entity
parent: BaseMachineCircuitboard
@ -48,10 +44,6 @@
GlassBeaker:
amount: 2
defaultPrototype: Beaker
- type: ReverseEngineering # DeltaV
difficulty: 2
recipes:
- ProtolatheHyperConvectionMachineCircuitboard
- type: entity
parent: BaseMachineCircuitboard

View File

@ -357,10 +357,25 @@
layers:
- state: engineering
- state: icon-rcd
- type: ItemBorgModule
moduleId: RCD # Frontier
# Begin DeltaV Removals - Changed to hands
#- type: ItemBorgModule
# moduleId: RCD # Frontier
# items:
# - RCDRecharging
# End DeltaV Removals
# Begin DeltaV Additions
- type: DroppableBorgModule
moduleId: RCD
items:
- RCDRecharging
- id: RCD
whitelist:
components:
- RCD
- id: RCDAmmo
whitelist:
components:
- RCDAmmo
# End DeltaV Additions
- type: BorgModuleIcon
icon: { sprite: Interface/Actions/actions_borg.rsi, state: rcd-module }

View File

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

View File

@ -458,7 +458,7 @@
- type: entity
id: MedicalTechFab
parent: BaseLatheLube
parent: BaseTechFabDepartamental # DeltaV - make sure it inherits the emag recipes
name: medical techfab
description: Prints equipment for use by the medbay.
components:

View File

@ -393,7 +393,7 @@
components:
- type: NextEvent # DeltaV: Queue Event for precognition
- type: RampingStationEventScheduler
averageChaos: 6 # DeltaV
averageChaos: 5 # DeltaV
averageEndTime: 180 # DeltaV
scheduledGameRules: !type:NestedSelector
tableId: BasicGameRulesTable

View File

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

View File

@ -42,6 +42,7 @@
MedicalDoctor: [ 2, 3 ]
MedicalIntern: [ 2, 2 ]
Paramedic: [ 1, 1 ]
Surgeon: [ 1, 1 ]
Psychologist: [ 1, 1 ]
MedicalBorg: [ 1, 1 ]
#science

View File

@ -75,6 +75,7 @@
- type: Tag
tags:
- Fruit # DeltaV - Allow anyone to eat it, hopefully
- ReptilianFood # DeltaV - Allows feroxi (shark people) to eat it
- type: entity
name: soy sustenance bar

View File

@ -11,6 +11,7 @@
femaleFirstNames: NamesOniFemale
lastNames: NamesOniLocation
naming: LastNoFirst
baseScale: "1.2, 1.2"
- type: markingPoints
id: MobOniMarkingLimits

View File

@ -7,6 +7,7 @@
markingLimits: MobFelinidMarkingLimits
dollPrototype: MobFelinidDummy
skinColoration: Hues # DeltaV
baseScale: "0.8, 0.8"
- type: markingPoints
id: MobFelinidMarkingLimits

View File

@ -26,8 +26,6 @@
- OreBagOfHolding
- type: latheRecipePack
parent:
- LogisticsBoards # DeltaV
id: CargoBoards
recipes:
- OreProcessorIndustrialMachineCircuitboard

View File

@ -43,6 +43,8 @@
- ClothingBackpackDuffelHolding
- type: latheRecipePack
parent:
- PowerCellsDeltaV # DeltaV
id: PowerCells
recipes:
- PowerCellMicroreactor
@ -84,9 +86,11 @@
id: ScienceBoards
recipes:
- TurboItemRechargerCircuitboard
- AutolatheHyperConvectionMachineCircuitboard
- ProtolatheHyperConvectionMachineCircuitboard
- CircuitImprinterHyperConvectionMachineCircuitboard
# Begin DeltaV Removals - replaced by techfabs/upgrade kits
#- AutolatheHyperConvectionMachineCircuitboard
#- ProtolatheHyperConvectionMachineCircuitboard
#- CircuitImprinterHyperConvectionMachineCircuitboard
# End DeltaV Removals
- TechDiskComputerCircuitboard
- FlatpackerMachineCircuitboard
- SheetifierMachineCircuitboard

View File

@ -205,11 +205,13 @@
result: ProtolatheMachineCircuitboard
- type: latheRecipe
abstract: true # DeltaV
parent: BaseGoldCircuitboardRecipe
id: AutolatheHyperConvectionMachineCircuitboard
result: AutolatheHyperConvectionMachineCircuitboard
- type: latheRecipe
abstract: true # DeltaV
parent: BaseGoldCircuitboardRecipe
id: ProtolatheHyperConvectionMachineCircuitboard
result: ProtolatheHyperConvectionMachineCircuitboard
@ -220,6 +222,7 @@
result: CircuitImprinterMachineCircuitboard
- type: latheRecipe
abstract: true # DeltaV
parent: BaseGoldCircuitboardRecipe
id: CircuitImprinterHyperConvectionMachineCircuitboard
result: CircuitImprinterHyperConvectionMachineCircuitboard

View File

@ -13,7 +13,6 @@
- MiningDrill
- MineralScannerEmpty
- OreProcessorIndustrialMachineCircuitboard
- SalvageExpeditionsComputerCircuitboard # DeltaV
- ClothingMaskWeldingGas
- type: technology
@ -66,9 +65,15 @@
tier: 1
cost: 10000
recipeUnlocks:
- AutolatheHyperConvectionMachineCircuitboard
- ProtolatheHyperConvectionMachineCircuitboard
- CircuitImprinterHyperConvectionMachineCircuitboard
# Begin DeltaV Removals - replaced by upgrade kits + departmental techfabs
#- AutolatheHyperConvectionMachineCircuitboard
#- ProtolatheHyperConvectionMachineCircuitboard
#- CircuitImprinterHyperConvectionMachineCircuitboard
# End DeltaV Removals
# Begin DeltaV Additions
- LatheUpgradeKitHyper
- LatheUpgradeKitCryo
# End DeltaV Additions
- SheetifierMachineCircuitboard
- type: technology

View File

@ -0,0 +1,4 @@
- type: guideEntry
id: Records
name: guide-entry-cd-records
text: "/ServerInfo/Guidebook/_CD/Records.xml"

View File

@ -12,6 +12,7 @@
- id: MiningShuttleConsoleCircuitboard
- id: StockTradingCartridge
- id: LogisticsTechFabCircuitboard
- id: SalvageExpeditionsComputerCircuitboard
- id: LunchboxCommandFilledRandom
prob: 0.3

View File

@ -145,6 +145,7 @@
- CanPilot
- FootstepSound
- DoorBumpOpener
- AnomalyHost
- type: FootPrints # DeltaV port from EE, blood splatter
leftBarePrint: "footprint-left-bare-lizard" # DeltaV port from EE, blood splatter
rightBarePrint: "footprint-right-bare-lizard" # DeltaV port from EE, blood splatter

View File

@ -72,8 +72,8 @@
- Handcuff
- type: entity
parent: [ BaseBorgModuleSecurity, BaseProviderBorgModule ]
id: BorgModuleSecurityEscalate
parent: BorgModuleSecurityDeescalate
name: escalation cyborg module
description: Peace was never an option.
components:

View File

@ -0,0 +1,51 @@
- type: entity
abstract: true
parent: BaseItem
id: BaseLatheUpgradeKit
components:
- type: Sprite
sprite: _DV/Objects/Tools/lathe_upgrade_kit.rsi
state: icon
- type: UpgradeKit
whitelist:
components:
- Lathe
blacklist:
components:
- UpgradedMachine
# ALWAYS add UpgradedMachine to components in inheritors
- type: entity
parent: BaseLatheUpgradeKit
id: LatheUpgradeKitHyper
name: hyper-convection upgrade kit
description: An upgrade kit with all the parts needed to upgrade a lathe. This one will save materials at the cost of speed and producing heat.
components:
- type: UpgradeKit
components:
- type: UpgradedMachine
upgrade: lathe-upgrade-kit-hyper-convection
- type: LatheUpgrade
materialUseMultiplier: 0.5
timeMultiplier: 1.5
- type: LatheHeatProducing
- type: ReagentSpeed
solution: lube
modifiers:
SpaceLube: 0.8 # being faster means less heat so lube needs to be nerfed
SpaceGlue: 5 # no change from normal lathe, overheat!!!
- type: entity
parent: BaseLatheUpgradeKit
id: LatheUpgradeKitCryo
name: cryo-stabilizing upgrade kit
description: An upgrade kit with all the parts needed to upgrade a lathe. This one will speed up production at the cost of freezing the area.
components:
- type: UpgradeKit
components:
- type: UpgradedMachine
upgrade: lathe-upgrade-kit-cryo-stabilizing
- type: LatheUpgrade
timeMultiplier: 0.5
- type: LatheHeatProducing
energyPerSecond: -20000 # cool instead of heat, not as extreme

View File

@ -22,6 +22,14 @@
- type: Lathe
idleState: icon
runningState: icon
- type: EmagLatheRecipes
emagStaticPacks:
- SecurityAmmoStatic
emagDynamicPacks:
- SecurityAmmo
- SecurityExplosives
- SecurityEquipment
- SecurityWeapons
- type: entity
parent: BaseTechFabDepartamental
@ -58,6 +66,7 @@
- EngineeringWeapons
- FauxTiles
- Equipment
- UpgradeKits
- type: Machine
board: EngineeringTechFabCircuitboard
- type: StealTarget

View File

@ -4,10 +4,21 @@
id: AtmosToolsDeltaV
recipes:
- FireExtinguisherBluespace
- PowerCellHyper # RE
- RCDAmmo # RE
- RCDRecharging
- type: latheRecipePack
id: PowerCellsDeltaV
recipes:
- PowerCellHyper # RE
- type: latheRecipePack
id: EngineeringBoardsDeltaV
recipes:
- AlertsComputerCircuitboard
- type: latheRecipePack
id: UpgradeKits
recipes:
- LatheUpgradeKitHyper
- LatheUpgradeKitCryo

View File

@ -12,8 +12,3 @@
- WeaponCrusher
- WeaponCrusherDagger
- WeaponCrusherGlaive
- type: latheRecipePack
id: LogisticsBoards
recipes:
- SalvageExpeditionsComputerCircuitboard

View File

@ -1,8 +1,3 @@
- type: latheRecipe
parent: BaseSilverCircuitboardRecipe
id: SalvageExpeditionsComputerCircuitboard
result: SalvageExpeditionsComputerCircuitboard
- type: latheRecipe
parent: BaseCircuitboardRecipe
id: AlertsComputerCircuitboard

View File

@ -8,3 +8,15 @@
Silver: 250
Plasma: 500
Bluespace: 200
- type: latheRecipe
parent: BaseToolRecipe
id: RCDRecharging
result: RCDRecharging
completetime: 8
materials:
Steel: 1500
Plastic: 500
Uranium: 400
Gold: 300
Bluespace: 150

View File

@ -0,0 +1,20 @@
# a bit more expensive than igniter + hyper-convection X board, its more convenient
- type: latheRecipe
abstract: true
id: BaseUpgradeKit
completetime: 5
materials:
Glass: 800
Steel: 600
Plastic: 400
Gold: 200
- type: latheRecipe
parent: BaseUpgradeKit
id: LatheUpgradeKitHyper
result: LatheUpgradeKitHyper
- type: latheRecipe
parent: BaseUpgradeKit
id: LatheUpgradeKitCryo
result: LatheUpgradeKitCryo

View File

@ -1,3 +1,5 @@
# Tier 1
- type: technology
id: AerialExtraction
name: research-technology-aerial-extraction
@ -10,3 +12,17 @@
recipeUnlocks:
- Fulton
- FultonBeacon
# Tier 3
- type: technology
id: MatterEnergyConversion
name: research-technology-matter-energy-conversion
icon:
sprite: Objects/Tools/rcd.rsi
state: icon
discipline: Industrial
tier: 3
cost: 15000
recipeUnlocks:
- RCDRecharging

View File

@ -7,6 +7,7 @@
markingLimits: MobHarpyMarkingLimits
dollPrototype: MobHarpyDummy
skinColoration: HumanToned
baseScale: "0.9, 0.9"
- type: speciesBaseSprites
id: MobHarpySprites

View File

@ -12,6 +12,7 @@
femaleFirstNames: NamesRodentiaFemale
lastNames: NamesRodentiaLast
naming: LastFirst
baseScale: "0.8, 0.8"
- type: speciesBaseSprites
id: MobRodentiaSprites

Some files were not shown because too many files have changed in this diff Show More