Обновление консоли станционного учета (#2800)

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
ThereDrD 2025-08-16 06:13:21 +03:00 committed by GitHub
parent fae071e3f5
commit b38dc9bcce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 856 additions and 11 deletions

View file

@ -1,3 +1,4 @@
using Content.Shared._Sunrise.StationRecords;
using Content.Shared.StationRecords;
using Robust.Client.UserInterface;
@ -22,6 +23,11 @@ public sealed class GeneralStationRecordConsoleBoundUserInterface : BoundUserInt
_window.OnFiltersChanged += (type, filterValue) =>
SendMessage(new SetStationRecordFilter(type, filterValue));
_window.OnDeleted += id => SendMessage(new DeleteStationRecord(id));
// Sunrise added start
_window.OnSaved += (record, id) => SendMessage(new SaveStationRecord(record, id));
_window.OnPrinted += id => SendMessage(new PrintStationRecord(id));
// Sunrise added end
}
protected override void UpdateState(BoundUserInterfaceState state)

View file

@ -17,9 +17,9 @@
<ItemList Name="RecordListing" />
</ScrollContainer>
</BoxContainer>
<BoxContainer Orientation="Vertical" Margin="5">
<BoxContainer Orientation="Vertical" Margin="5" HorizontalExpand="True" VerticalExpand="True">
<Label Name="RecordContainerStatus" Visible="False" Text="{Loc 'general-station-record-console-select-record-info'}"/>
<Control Name="RecordContainer" Visible="False"/>
<BoxContainer Name="RecordContainer" Visible="False" HorizontalExpand="True" VerticalExpand="True"/>
</BoxContainer>
</BoxContainer>
</BoxContainer>

View file

@ -1,7 +1,12 @@
using Content.Client._Sunrise.StationRecords;
using Content.Client.Lobby;
using Content.Client.Roles;
using Content.Shared.StationRecords;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
namespace Content.Client.StationRecords;
@ -17,10 +22,30 @@ public sealed partial class GeneralStationRecordConsoleWindow : DefaultWindow
private StationRecordFilterType _currentFilterType;
// Sunrise added start
public Action<GeneralStationRecord, uint>? OnSaved;
public Action<uint>? OnPrinted;
private readonly IEntityManager _entity;
private readonly IPrototypeManager _prototype;
private readonly ILocalizationManager _loc;
private readonly JobSystem _job;
private readonly LobbyUIController _controller;
// Sunrise added end
public GeneralStationRecordConsoleWindow()
{
RobustXamlLoader.Load(this);
// Sunrise added start
_entity = IoCManager.Resolve<IEntityManager>();
_prototype = IoCManager.Resolve<IPrototypeManager>();
_loc = IoCManager.Resolve<ILocalizationManager>();
var interfaceManager = IoCManager.Resolve<IUserInterfaceManager>();
_job = _entity.System<JobSystem>();
_controller = interfaceManager.GetUIController<LobbyUIController>();
// Sunrise added end
_currentFilterType = StationRecordFilterType.Name;
foreach (var item in Enum.GetValues<StationRecordFilterType>())
@ -111,7 +136,8 @@ public sealed partial class GeneralStationRecordConsoleWindow : DefaultWindow
RecordContainerStatus.Text = state.SelectedKey == null
? Loc.GetString("general-station-record-console-no-record-found")
: Loc.GetString("general-station-record-console-select-record-info");
PopulateRecordContainer(state.Record, state.CanDeleteEntries, state.SelectedKey);
// Sunrise edit
PopulateRecordContainer(state.Record, state.CanDeleteEntries, state.CanRedactSensitiveData, state.HasAccess, state.SelectedKey);
}
else
{
@ -136,11 +162,17 @@ public sealed partial class GeneralStationRecordConsoleWindow : DefaultWindow
RecordListing.SortItemsByText();
}
private void PopulateRecordContainer(GeneralStationRecord record, bool enableDelete, uint? id)
// Sunrise edit
private void PopulateRecordContainer(GeneralStationRecord record, bool enableDelete, bool canRedactSensitiveData, bool hasAccess, uint? id)
{
RecordContainer.RemoveAllChildren();
var newRecord = new GeneralRecord(record, enableDelete, id);
// Sunrise edit start
var newRecord =
new SunriseGeneralRecord(record, enableDelete, canRedactSensitiveData, hasAccess, id, in _entity, in _prototype, in _loc, in _job, in _controller);
// Sunrise edit end
newRecord.OnDeletePressed = OnDeleted;
newRecord.OnPrintPressed = OnPrinted;
newRecord.OnSaveButtonPressed = OnSaved;
RecordContainer.AddChild(newRecord);
}

View file

@ -0,0 +1,69 @@
<BoxContainer xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True"
MinSize="500 600">
<!-- Заголовок с именем, работой и отделом-->
<controls:StripeBack VerticalAlignment="Top" HorizontalExpand="True" VerticalExpand="True">
<BoxContainer Orientation="Vertical" VerticalAlignment="Top">
<Label Name="NameHeading" Access="Public" StyleClasses="LabelHeading" HorizontalAlignment="Center" VerticalAlignment="Bottom"/>
<BoxContainer Orientation="Horizontal" Align="Center">
<Label Name="SubText" VerticalAlignment="Center" HorizontalAlignment="Center" StyleClasses="LabelSubText" Access="Public"/>
</BoxContainer>
</BoxContainer>
</controls:StripeBack>
<!-- Спрайт персонажа посередине окна-->
<SpriteView OverrideDirection="South" Scale="8 8" Name="SpriteView" Access="Public" MinSize="256 256" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="5 25 5 25"/>
<controls:StripeBack VerticalAlignment="Bottom" HorizontalExpand="True" VerticalExpand="True">
<!-- Нижняя часть-->
<BoxContainer Orientation="Horizontal" VerticalAlignment="Center" Margin="5 5 5 5">
<!-- Белая полоска слева -->
<PanelContainer
VerticalExpand="True"
MinWidth="2"
MaxWidth="2"
Margin="9 5 9 0">
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#CFCFCF"/>
</PanelContainer.PanelOverride>
</PanelContainer>
<!-- Поля для изменения данных -->
<GridContainer Name="ControlGrid" Columns="2" HorizontalAlignment="Stretch" HorizontalExpand="True">
<Label Name="NameLabel" Text="{Loc 'station-records-ui-name'}"/> <LineEdit Name="Name" HorizontalExpand="True"/>
<Label Name="AgeLabel" Text="{Loc 'station-records-ui-age'}"/> <LineEdit Name="Age"/>
<Label Name="GenderLabel" Text="{Loc 'station-records-ui-gender'}"/> <OptionButton Name="Gender"/>
<Label Name="SpeciesLabel" Text="{Loc 'station-records-ui-species'}"/> <OptionButton Name="Species"/>
<Label Name="JobLabel" Text="{Loc 'station-records-ui-job'}"/> <OptionButton Name="Job"/>
<Label Name="FingerprintLabel" Text="{Loc 'station-records-ui-fingerprint'}"/> <LineEdit Name="Fingerprint" Editable="False"/>
<Label Name="DnaLabel" Text="{Loc 'station-records-ui-dna'}"/> <LineEdit Name="Dna" Editable="False"/>
</GridContainer>
</BoxContainer>
</controls:StripeBack>
<!-- Кнопки сохранить, распечатать и удалить(не видна без емага) -->
<GridContainer Columns="3" HorizontalAlignment="Stretch" HorizontalExpand="True">
<Button Visible="False" Name="SaveButton" Text="{Loc 'station-records-ui-save'}" Disabled="True" StyleClasses="ButtonColorGreen"/>
<Button Visible="False" Name="PrintButton" Text="{Loc 'station-records-ui-print'}"/>
<Button Visible="False" Name="DeleteButton" Text="{Loc 'general-station-record-console-delete'}" StyleClasses="ButtonColorRed"/>
</GridContainer>
<!-- Характеристика -->
<controls:StripeBack VerticalAlignment="Bottom" HorizontalExpand="True" VerticalExpand="True">
<BoxContainer Orientation="Horizontal" VerticalAlignment="Center" Margin="5 5 5 5">
<!-- Опять белая полоска слева -->
<PanelContainer
VerticalExpand="True"
MinWidth="2"
MaxWidth="2"
Margin="9 5 9 0">
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#CFCFCF"/>
</PanelContainer.PanelOverride>
</PanelContainer>
<GridContainer Columns="1" HorizontalAlignment="Stretch" HorizontalExpand="True">
<Label Name="PersonalityLabel" Text="{Loc 'station-records-ui-personality'}"/>
<TextEdit Name="Personality" HorizontalExpand="True" VerticalExpand="True" MinSize="300 100"/>
</GridContainer>
</BoxContainer>
</controls:StripeBack>
</BoxContainer>

View file

@ -0,0 +1,282 @@
using System.Linq;
using Content.Client.Lobby;
using Content.Client.Roles;
using Content.Shared._Sunrise.Helpers;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Content.Shared.StationRecords;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client._Sunrise.StationRecords;
[GenerateTypedNameReferences]
public sealed partial class SunriseGeneralRecord : BoxContainer
{
private readonly IEntityManager _entity;
private readonly IPrototypeManager _prototype;
private readonly ILocalizationManager _loc;
private readonly JobSystem _job;
private readonly LobbyUIController _controller;
public Action<GeneralStationRecord, uint>? OnSaveButtonPressed;
public Action<uint>? OnPrintPressed;
public Action<uint>? OnDeletePressed;
private EntityUid _previewDummy;
private readonly HumanoidCharacterProfile? _profile;
private readonly List<SpeciesPrototype> _allSpecies;
private readonly List<JobPrototype> _allJobs;
private readonly Gender[] _allGender;
private readonly bool _hasAccess;
// Копия этого хранится в серверной системе
private const int MaxAgeLength = 6;
public SunriseGeneralRecord(GeneralStationRecord record,
bool canDelete,
bool canRedactSensitiveData,
bool hasAccess,
uint? id,
in IEntityManager entity,
in IPrototypeManager prototype,
in ILocalizationManager loc,
in JobSystem job,
in LobbyUIController controller)
{
RobustXamlLoader.Load(this);
_entity = entity;
_prototype = prototype;
_loc = loc;
_job = job;
_controller = controller;
_profile = record.HumanoidProfile;
_allSpecies = _prototype.EnumeratePrototypes<SpeciesPrototype>().ToList();
_allJobs = _prototype.EnumeratePrototypes<JobPrototype>().ToList();
_allGender = Enum.GetValues<Gender>();
// Сортировка рас и работ по имени
_allSpecies.Sort((a, b)
=> string.Compare(_loc.GetString(a.Name), _loc.GetString(b.Name), StringComparison.Ordinal));
_allJobs.Sort((a, b)
=> string.Compare(a.LocalizedName, b.LocalizedName, StringComparison.Ordinal));
Fingerprint.Editable = canRedactSensitiveData && hasAccess;
Dna.Editable = canRedactSensitiveData && hasAccess;
if (canDelete && id != null )
{
DeleteButton.Visible = true;
DeleteButton.OnPressed += _ => OnDeletePressed?.Invoke(id.Value);
}
if (id != null )
{
SaveButton.Visible = true;
SaveButton.OnPressed += _ =>
{
var updatedRecord = BuildUpdatedRecord(record);
OnSaveButtonPressed?.Invoke(updatedRecord, id.Value);
};
}
if (id != null )
{
PrintButton.Visible = true;
PrintButton.OnPressed += _ => OnPrintPressed?.Invoke(id.Value);
}
_hasAccess = hasAccess;
UpdateEditableInfo(record);
UpdateHeading(record);
ReloadPreview(record.JobPrototype);
MakeDropDownSelectable();
CheckAccess();
CheckChanges();
}
protected override void ExitedTree()
{
base.ExitedTree();
_entity.DeleteEntity(_previewDummy);
}
/// <summary>
/// Обновляет информацию, которая может быть изменена игроком.
/// </summary>
private void UpdateEditableInfo(GeneralStationRecord record)
{
Name.Text = record.Name;
Age.Text = record.Age.ToString();
for (var i = 0; i < _allGender.Length; i++)
{
var item = _allGender[i];
var name = _loc.GetString("station-records-gender", ("gender", item.ToString()));
Gender.AddItem(name, i);
if (item == record.Gender)
Gender.SelectId(i);
}
for (var i = 0; i < _allSpecies.Count; i++)
{
var item = _allSpecies[i];
if (item.StationRecordsHidden && item.ID != record.Species)
continue;
var name = _loc.GetString(item.Name);
Species.AddItem(name, i);
if (item.ID == record.Species)
Species.SelectId(i);
}
for (var i = 0; i < _allJobs.Count; i++)
{
var item = _allJobs[i];
if (item.OverrideConsoleVisibility == false && item.ID != record.JobPrototype)
continue;
var name = item.LocalizedName;
Job.AddItem(name, i);
if (item.ID == record.JobPrototype)
Job.SelectId(i);
}
Fingerprint.Text = record.Fingerprint ?? _loc.GetString("generic-not-available-shorthand");
Dna.Text = record.DNA ?? _loc.GetString("generic-not-available-shorthand");
Personality.Placeholder = new Rope.Leaf(_loc.GetString("station-records-ui-personality-placeholder"));
if (!string.IsNullOrEmpty(record.Personality))
Personality.TextRope = new Rope.Leaf(record.Personality);
}
/// <summary>
/// Обновляет заголовок сверху персонажа.
/// </summary>
private void UpdateHeading(GeneralStationRecord record)
{
SubText.Visible = false;
NameHeading.Text = record.Name;
if (!_prototype.TryIndex<JobPrototype>(record.JobPrototype, out var job))
return;
SubText.Text = $"{job.LocalizedName}";
SubText.Visible = true;
if (!_job.TryGetDepartment(job.ID, out var department))
return;
SubText.Text = $"{_loc.GetString(department.Name)}, {job.LocalizedName}";
}
/// <summary>
/// Обновляет превью игрока.
/// </summary>
private void ReloadPreview(ProtoId<JobPrototype> jobProtoId)
{
if (!_prototype.TryIndex(jobProtoId, out var job))
return;
_entity.DeleteEntity(_previewDummy);
_previewDummy = EntityUid.Invalid;
if (job.JobEntity != null)
_previewDummy = _entity.Spawn(job.JobEntity, MapCoordinates.Nullspace);
if (_profile != null && _prototype.HasIndex(_profile.Species))
_previewDummy = _controller.LoadProfileEntity(_profile, job, true);
SpriteView.SetEntity(_previewDummy);
}
/// <summary>
/// Создает новую структуру, помещающую в себя из полей в интерфейсе для отправки на сервере для сохранения.
/// Перед этим проводит валидацию строк и проверяет, что данные не обосраны.
/// </summary>
private GeneralStationRecord BuildUpdatedRecord(GeneralStationRecord original)
{
var textAge = Age.Text.SanitizeInput(MaxAgeLength);
var updated = original with
{
Name = Name.Text,
Age = int.TryParse(textAge, out var ageVal) ? ageVal : original.Age,
Gender = _allGender[Gender.SelectedId],
Species = _allSpecies[Species.SelectedId].ID,
JobPrototype = _allJobs[Job.SelectedId].ID,
Fingerprint = Fingerprint.Text,
DNA = Dna.Text,
Personality = Rope.Collapse(Personality.TextRope),
};
return GeneralStationRecord.SanitizeRecord(updated, in _prototype);
}
private void MakeDropDownSelectable()
{
// Почему это не встроено в сами кнопки
foreach (var child in ControlGrid.Children)
{
if (child is not OptionButton optionButton)
continue;
optionButton.OnItemSelected += Select;
}
}
private void Select(OptionButton.ItemSelectedEventArgs args)
{
args.Button.SelectId(args.Id);
MakeSaveAvailable();
}
private void CheckAccess()
{
Name.Editable = _hasAccess;
Age.Editable = _hasAccess;
Personality.Editable = _hasAccess;
Gender.Disabled = !_hasAccess;
Species.Disabled = !_hasAccess;
Job.Disabled = !_hasAccess;
}
private void CheckChanges()
{
foreach (var child in ControlGrid.Children)
{
if (child is not LineEdit lineEdit)
continue;
lineEdit.OnTextChanged += _ => MakeSaveAvailable();
}
Personality.OnTextChanged += _ => MakeSaveAvailable();
}
private void MakeSaveAvailable()
{
SaveButton.Disabled = !_hasAccess;
}
}

View file

@ -1,5 +1,8 @@
using Content.Server.StationRecords.Systems;
using Content.Shared.Radio;
using Content.Shared.StationRecords;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
namespace Content.Server.StationRecords.Components;
@ -24,4 +27,38 @@ public sealed partial class GeneralStationRecordConsoleComponent : Component
/// </summary>
[DataField]
public bool CanDeleteEntries;
// Sunrise added start - возможность редактировать отпечатки через емаг
[DataField]
public bool CanRedactSensitiveData;
[DataField]
public bool HasAccess;
[DataField]
public bool Silent;
[DataField]
public bool SkipAccessCheck;
[DataField]
public SoundSpecifier SuccessfulSound = new SoundPathSpecifier("/Audio/Effects/Cargo/ping.ogg");
[DataField]
public SoundSpecifier FailedSound = new SoundPathSpecifier("/Audio/Effects/Cargo/buzz_sigh.ogg");
public TimeSpan NextPrintTime = TimeSpan.Zero;
[DataField]
public TimeSpan PrintCooldown = TimeSpan.FromSeconds(5);
[DataField]
public EntProtoId Paper = "Paper";
[DataField]
public SoundSpecifier SoundPrint = new SoundPathSpecifier("/Audio/Machines/short_print_and_rip.ogg");
[DataField]
public List<ProtoId<RadioChannelPrototype>> AnnouncementChannels = ["Command", "Security"];
// Sunrise added end
}

View file

@ -6,7 +6,7 @@ using Robust.Server.GameObjects;
namespace Content.Server.StationRecords.Systems;
public sealed class GeneralStationRecordConsoleSystem : EntitySystem
public sealed partial class GeneralStationRecordConsoleSystem : EntitySystem
{
[Dependency] private readonly UserInterfaceSystem _ui = default!;
[Dependency] private readonly StationSystem _station = default!;
@ -20,11 +20,14 @@ public sealed class GeneralStationRecordConsoleSystem : EntitySystem
Subs.BuiEvents<GeneralStationRecordConsoleComponent>(GeneralStationRecordConsoleKey.Key, subs =>
{
subs.Event<BoundUIOpenedEvent>(UpdateUserInterface);
subs.Event<BoundUIOpenedEvent>(OnOpened);
subs.Event<SelectStationRecord>(OnKeySelected);
subs.Event<SetStationRecordFilter>(OnFiltersChanged);
subs.Event<DeleteStationRecord>(OnRecordDelete);
});
// Sunrise added
InitializeSunrise();
}
private void OnRecordDelete(Entity<GeneralStationRecordConsoleComponent> ent, ref DeleteStationRecord args)
@ -34,6 +37,23 @@ public sealed class GeneralStationRecordConsoleSystem : EntitySystem
var owning = _station.GetOwningStation(ent.Owner);
// Sunrise added start
if (owning == null)
return;
// Дополнительная серверная проверка на случай педиков с читами
if (!HasAccess(ent, args.Actor))
return;
if (!_stationRecords.TryGetRecord<GeneralStationRecord>(new StationRecordKey(args.Id, owning.Value), out var record))
return;
var message = Loc.GetString("station-record-deleted", ("name", record.Name));
var popup = Loc.GetString("station-record-deleted-successfully");
DoFeedback(ent, message, popup);
// Sunrise added end
if (owning != null)
_stationRecords.RemoveRecord(new StationRecordKey(args.Id, owning.Value));
UpdateUserInterface(ent); // Apparently an event does not get raised for this.
@ -93,7 +113,8 @@ public sealed class GeneralStationRecordConsoleSystem : EntitySystem
var key = new StationRecordKey(id, owningStation.Value);
_stationRecords.TryGetRecord<GeneralStationRecord>(key, out var record, stationRecords);
GeneralStationRecordConsoleState newState = new(id, record, listing, console.Filter, ent.Comp.CanDeleteEntries);
// Sunrise edit
GeneralStationRecordConsoleState newState = new(id, record, listing, console.Filter, ent.Comp.CanDeleteEntries, ent.Comp.CanRedactSensitiveData, ent.Comp.HasAccess);
_ui.SetUiState(uid, GeneralStationRecordConsoleKey.Key, newState);
}
}

View file

@ -179,6 +179,7 @@ public sealed class StationRecordsSystem : SharedStationRecordsSystem
Fingerprint = mobFingerprint,
DNA = dna,
Silicon = silicon, // Sunrise-Edit
HumanoidProfile = profile, // Sunrise edit
};
var key = AddRecordEntry(station, record);

View file

@ -0,0 +1,234 @@
using Content.Server.Hands.Systems;
using Content.Server.Popups;
using Content.Server.Radio.EntitySystems;
using Content.Server.Roles.Jobs;
using Content.Server.StationRecords.Components;
using Content.Shared._Sunrise.StationRecords;
using Content.Shared.Access.Systems;
using Content.Shared.Emag.Systems;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Paper;
using Content.Shared.Roles;
using Content.Shared.StationRecords;
using Robust.Server.Audio;
using Robust.Shared.Audio;
using Robust.Shared.Enums;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Server.StationRecords.Systems;
public sealed partial class GeneralStationRecordConsoleSystem
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly RadioSystem _radio = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly AccessReaderSystem _access = default!;
[Dependency] private readonly HandsSystem _hands = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly JobSystem _job = default!;
[Dependency] private readonly PaperSystem _paper = default!;
private void InitializeSunrise()
{
SubscribeLocalEvent<GeneralStationRecordConsoleComponent, GotEmaggedEvent>(OnEmagged);
Subs.BuiEvents<GeneralStationRecordConsoleComponent>(GeneralStationRecordConsoleKey.Key, subs =>
{
subs.Event<SaveStationRecord>(OnSave);
subs.Event<PrintStationRecord>(Print);
});
}
private void OnSave(Entity<GeneralStationRecordConsoleComponent> ent, ref SaveStationRecord args)
{
var owning = _station.GetOwningStation(ent.Owner);
if (owning == null)
{
_audio.PlayPvs(ent.Comp.FailedSound, ent);
return;
}
// Дополнительная серверная проверка на случай педиков с читами
if (!HasAccess(ent, args.Actor))
{
_audio.PlayPvs(ent.Comp.FailedSound, ent);
return;
}
// Удаляем старую запись
if (!_stationRecords.RemoveRecord(new StationRecordKey(args.Id, owning.Value)))
{
_audio.PlayPvs(ent.Comp.FailedSound, ent);
return;
}
// Добавляем новую
var record = GeneralStationRecord.SanitizeRecord(args.Record, in _prototype);
var id = _stationRecords.AddRecordEntry(owning.Value, record);
ent.Comp.ActiveKey = id.Id;
var message = Loc.GetString("station-record-updated", ("name", args.Record.Name));
var popup = Loc.GetString("station-record-updated-successfully");
DoFeedback(ent, message, popup);
UpdateUserInterface(ent);
}
private void OnEmagged(Entity<GeneralStationRecordConsoleComponent> ent, ref GotEmaggedEvent args)
{
if (ent.Comp.CanRedactSensitiveData
&& ent.Comp.CanDeleteEntries
&& ent.Comp.Silent
&& ent.Comp.SkipAccessCheck)
return;
if (args.Handled)
return;
ent.Comp.CanDeleteEntries = true;
ent.Comp.CanRedactSensitiveData = true;
ent.Comp.Silent = true;
ent.Comp.SkipAccessCheck = true;
UpdateUserInterface(ent);
args.Handled = true;
}
private void Print(Entity<GeneralStationRecordConsoleComponent> ent, ref PrintStationRecord args)
{
var user = args.Actor;
if (_timing.CurTime < ent.Comp.NextPrintTime)
{
_popup.PopupEntity(Loc.GetString("forensic-scanner-printer-not-ready"), ent, user);
_audio.PlayPvs(ent.Comp.FailedSound, ent);
return;
}
var owning = _station.GetOwningStation(ent.Owner);
if (owning == null)
{
_audio.PlayPvs(ent.Comp.FailedSound, ent);
return;
}
if (!_stationRecords.TryGetRecord<GeneralStationRecord>(new StationRecordKey(args.Id, owning.Value), out var record))
{
_audio.PlayPvs(ent.Comp.FailedSound, ent);
return;
}
// Spawn a piece of paper.
var printed = Spawn(ent.Comp.Paper, Transform(ent).Coordinates);
_hands.PickupOrDrop(args.Actor, printed, checkActionBlocker: false);
if (!TryComp<PaperComponent>(printed, out var paperComp))
{
_audio.PlayPvs(ent.Comp.FailedSound, ent);
return;
}
var documentName = Loc.GetString("printed-station-records-document-name", ("name", record.Name));
_metaData.SetEntityName(printed, documentName);
var text = Loc.GetString(
"printed-station-records-content",
("name", record.Name),
("job", GetJobName(record.JobPrototype)),
("department", GetDepartmentName(record.JobPrototype)),
("age", record.Age),
("gender", GetGenderName(record.Gender)),
("species", GetSpeciesName(record.Species)),
("dna", record.DNA ?? Loc.GetString("printed-station-records-unrecognized")),
("fingerprint", record.Fingerprint ?? Loc.GetString("printed-station-records-unrecognized")),
("personality", GetPersonality(record.Personality))
);
_paper.SetContent((printed, paperComp), text);
_audio.PlayPvs(ent.Comp.SoundPrint, ent,
AudioParams.Default
.WithVariation(0.25f)
.WithVolume(4f)
.WithRolloffFactor(2.8f)
.WithMaxDistance(4.5f));
ent.Comp.NextPrintTime = _timing.CurTime + ent.Comp.PrintCooldown;
}
private void DoFeedback(Entity<GeneralStationRecordConsoleComponent> ent, string message, string popup)
{
_popup.PopupEntity(popup, ent);
if (ent.Comp.Silent)
return;
foreach (var channel in ent.Comp.AnnouncementChannels)
{
_radio.SendRadioMessage(ent, message, channel, ent);
}
_audio.PlayPvs(ent.Comp.SuccessfulSound, ent);
}
private void OnOpened(Entity<GeneralStationRecordConsoleComponent> ent, ref BoundUIOpenedEvent msg)
{
ent.Comp.HasAccess = HasAccess(ent, msg.Actor);
UpdateUserInterface(ent);
}
/// <summary>
/// Проверяет наличие у персонажа доступа к консоли.
/// </summary>
private bool HasAccess(Entity<GeneralStationRecordConsoleComponent> ent, EntityUid actor)
{
var allowed = _access.IsAllowed(actor, ent);
return allowed || ent.Comp.SkipAccessCheck;
}
#region Helpers
private string GetJobName(ProtoId<JobPrototype> job)
{
if (!_prototype.TryIndex(job, out var jobPrototype))
return Loc.GetString("printed-station-records-unrecognized");
return jobPrototype.LocalizedName;
}
private string GetDepartmentName(ProtoId<JobPrototype> job)
{
if (!_job.TryGetDepartment(job, out var department))
return Loc.GetString("printed-station-records-unrecognized");
return Loc.GetString(department.Name);
}
private string GetGenderName(Gender gender)
{
return Loc.GetString("station-records-gender", ("gender", gender.ToString()));
}
private string GetSpeciesName(ProtoId<SpeciesPrototype> species)
{
if (!_prototype.TryIndex(species, out var speciesPrototype))
return Loc.GetString("printed-station-records-unrecognized");
return Loc.GetString(speciesPrototype.Name);
}
private string GetPersonality(string personality)
{
if (string.IsNullOrEmpty(personality))
return Loc.GetString("printed-station-records-unrecognized");
return personality;
}
#endregion
}

View file

@ -199,6 +199,9 @@ public sealed partial class SpeciesPrototype : IPrototype
/// </summary>
[DataField]
public int StandardDensity = 120;
[DataField]
public bool StationRecordsHidden;
//Sunrise end

View file

@ -38,21 +38,28 @@ public sealed class GeneralStationRecordConsoleState : BoundUserInterfaceState
public readonly Dictionary<uint, string>? RecordListing;
public readonly StationRecordsFilter? Filter;
public readonly bool CanDeleteEntries;
public readonly bool CanRedactSensitiveData; // Sunrise added
public readonly bool HasAccess; // Sunrise added
public GeneralStationRecordConsoleState(uint? key,
GeneralStationRecord? record,
Dictionary<uint, string>? recordListing,
StationRecordsFilter? newFilter,
bool canDeleteEntries)
bool canDeleteEntries,
bool canRedactSensitiveData, // Sunrise added
bool hasAccess) // Sunrise added
{
SelectedKey = key;
Record = record;
RecordListing = recordListing;
Filter = newFilter;
CanDeleteEntries = canDeleteEntries;
CanRedactSensitiveData = canRedactSensitiveData; // Sunrise added
HasAccess = hasAccess; // Sunrise added
}
public GeneralStationRecordConsoleState() : this(null, null, null, null, false)
// Sunrise edit
public GeneralStationRecordConsoleState() : this(null, null, null, null, false, false, false)
{
}

View file

@ -1,4 +1,9 @@
using Content.Shared._Sunrise.Helpers;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Robust.Shared.Enums;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.StationRecords;
@ -69,6 +74,43 @@ public sealed record GeneralStationRecord
[DataField]
public string? DNA;
// Sunrise added start
[DataField]
public bool Silicon; // Sunrise-Edit
public bool Silicon;
[DataField]
public HumanoidCharacterProfile? HumanoidProfile;
[DataField]
public string Personality = string.Empty;
[NonSerialized] private const int MaxNameLength = 64;
[NonSerialized] private const int MaxAge = 10000;
[NonSerialized] private const int MaxFingerprintLength = 32;
[NonSerialized] private const int MaxDnaLength = 16;
[NonSerialized] private const int MaxPersonalityLength = 1024;
[NonSerialized] private static readonly ProtoId<JobPrototype> FallbackJobPrototype = "Passenger";
[NonSerialized] private static readonly ProtoId<SpeciesPrototype> FallbackSpeciesPrototype = "Human";
/// <summary>
/// Санитизирует данные, требуется для механики изменения и сохранения данных в консоли станционного учета.
/// </summary>
public static GeneralStationRecord SanitizeRecord(GeneralStationRecord original, in IPrototypeManager prototype)
{
var updated = original with
{
Name = original.Name.SanitizeInput(MaxNameLength),
Age = original.Age <= MaxAge ? original.Age : MaxAge,
Species = prototype.TryIndex<SpeciesPrototype>(original.Species, out var species) ? species.ID : FallbackSpeciesPrototype,
JobPrototype = prototype.TryIndex<JobPrototype>(original.JobPrototype, out var job) ? job.ID : FallbackJobPrototype,
Fingerprint = original.Fingerprint.SanitizeInput(MaxFingerprintLength),
DNA = original.DNA.SanitizeInput(MaxDnaLength),
Personality = original.Personality.SanitizeInput(MaxPersonalityLength),
};
return updated;
}
// Sunrise added end
}

View file

@ -0,0 +1,39 @@
using System.Text;
using System.Text.RegularExpressions;
namespace Content.Shared._Sunrise.Helpers;
public static class StringExtensions
{
private static readonly Regex AllowedCharsRegex = new Regex(
@"[^a-zA-Zа-яА-ЯёЁ0-9\s.,!?;:\-_\(\)\[\]{}""'/\\@#%\^&\*\+=<>]",
RegexOptions.Compiled | RegexOptions.CultureInvariant
);
/// <summary>
/// Санитизация пользовательского ввода:
/// - Убирает лишние пробелы по краям
/// - Обрезает до maxLength (если задан)
/// - Убирает недопустимые символы
/// - Нормализует Unicode
/// </summary>
/// <remarks>
/// Рекомендуется для UI, содержащих LineEdit или подобные возможности передать текст на сервер.
/// </remarks>
public static string SanitizeInput(this string? input, int? maxLength = null)
{
if (string.IsNullOrEmpty(input))
return string.Empty;
input = input.Trim();
if (maxLength.HasValue && input.Length > maxLength.Value)
input = input.Substring(0, maxLength.Value);
input = input.Normalize(NormalizationForm.FormC);
input = AllowedCharsRegex.Replace(input, string.Empty);
return input;
}
}

View file

@ -0,0 +1,17 @@
using Content.Shared.StationRecords;
using Robust.Shared.Serialization;
namespace Content.Shared._Sunrise.StationRecords;
[Serializable, NetSerializable]
public sealed class SaveStationRecord(GeneralStationRecord record, uint id) : BoundUserInterfaceMessage
{
public readonly uint Id = id;
public readonly GeneralStationRecord Record = record;
}
[Serializable, NetSerializable]
public sealed class PrintStationRecord(uint id) : BoundUserInterfaceMessage
{
public readonly uint Id = id;
}

View file

@ -0,0 +1,27 @@
printed-station-records-content =
[head=1]NanoTransen[/head]
[bold]Распечатка из базы данных экипажа станции[/bold]
▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
[head=2]Основная информация[/head]
[bullet] [color=#594D4A][bold]Имя:[/bold][/color] [italic]{$name}[/italic]
[bullet] [color=#594D4A][bold]Должность:[/bold][/color] [italic]{$job}[/italic]
[bullet] [color=#594D4A][bold]Отдел:[/bold][/color] [italic]{$department}[/italic]
[bullet] [color=#594D4A][bold]Возраст:[/bold][/color] [italic]{$age}[/italic]
[bullet] [color=#594D4A][bold]Пол:[/bold][/color] [italic]{$gender}[/italic]
[bullet] [color=#594D4A][bold]Раса:[/bold][/color] [italic]{$species}[/italic]
[head=2]Биометрические данные[/head]
[bullet] [color=#5BA4CF][bold]ДНК:[/bold][/color] [mono]{$dna}[/mono]
[bullet] [color=#5BA4CF][bold]Отпечаток пальцев:[/bold][/color] [mono]{$fingerprint}[/mono]
[head=2]Характеристика[/head]
[italic]{$personality}[/italic]
printed-station-records-document-name = Распечатка на {$name}
printed-station-records-unrecognized = Неустановлено

View file

@ -0,0 +1,2 @@
station-record-updated = Запись в базе данных для «{$name}» была обновлена!
station-record-deleted = [bold]Запись в базе данных для «{$name}» была УДАЛЕНА![/bold]

View file

@ -0,0 +1,20 @@
station-records-gender =
{ $gender ->
[Male] Мужской
[Female] Женский
[Epicene] Двуполый
*[Neuter] Бесполый
}
station-records-ui-name = Имя:
station-records-ui-age = Возраст:
station-records-ui-job = Должность:
station-records-ui-species = Раса:
station-records-ui-gender = Пол:
station-records-ui-fingerprint = Отпечатки:
station-records-ui-dna = ДНК:
station-records-ui-personality = Характеристика:
station-records-ui-personality-placeholder = Здесь будет текст...
station-records-ui-save = Сохранить
station-records-ui-print = Распечатать

View file

@ -408,6 +408,10 @@
name: station records computer
description: This can be used to check station records.
components:
# Sunrise added start
- type: AccessReader
access: [["Security"], ["HeadOfPersonnel"]]
# Sunrise added end
- type: GeneralStationRecordConsole
- type: UserInterface
interfaces:

View file

@ -12,6 +12,7 @@
dollPrototype: MobSkeletonPersonDummy
skinColoration: TintedHues
buttScanTexture: /Textures/_Sunrise/ButtsScans/human.png # Sunrise-Edit
stationRecordsHidden: true
- type: bodyType
id: TerminatorNormal

View file

@ -11,6 +11,7 @@
defaultSkinTone: "#BFC2C7"
defaultHeight: 1.05
defaultWidth: 1.05
stationRecordsHidden: true
- type: bodyType
id: MobAbductorSprites