This commit is contained in:
BarryNorfolk 2026-08-14 01:46:10 +00:00 committed by GitHub
commit 8f1d5b2b11
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 1234 additions and 27 deletions

View File

@ -110,7 +110,7 @@ public sealed partial class DialogWindow : FancyWindow
protected override void Opened()
{
base.Opened();
// Grab keyboard focus for the first dialog entry
_promptLines[0].Item2.GrabKeyboardFocus();
}

View File

@ -0,0 +1,60 @@
<BoxContainer
xmlns="https://spacestation14.io"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
HorizontalExpand="True"
Orientation="Vertical"
>
<BoxContainer Orientation="Horizontal" VerticalExpand="True">
<PanelContainer Margin="5" >
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#1B1B1B"/>
</PanelContainer.PanelOverride>
<BoxContainer Orientation="Vertical" SeparationOverride="5">
<BoxContainer Orientation="Horizontal" >
<Label Text="Issuer:" Margin="5"/>
<LineEdit
Name="ObjectiveIssuer"
HorizontalExpand="True"
Margin="5"
/>
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="Title:" Margin="5"/>
<LineEdit
Name="ObjectiveTitle"
HorizontalExpand="True"
Margin="5"
/>
</BoxContainer>
<customControls:HSeparator/>
<Label Text="Description:" Margin="5"/>
<TextEdit
Name="ObjectiveDescription"
HorizontalExpand="True"
VerticalExpand="True"
MinWidth="500"
MinHeight="60"
Margin="5"
/>
</BoxContainer>
</PanelContainer>
<BoxContainer Orientation="Vertical" Align="Center">
<BoxContainer Align="Center">
<PanelContainer>
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#1B1B1B"/>
</PanelContainer.PanelOverride>
<TextureButton Name="IconSelectButton" SetWidth="128" SetHeight="128" />
</PanelContainer>
</BoxContainer>
<BoxContainer Orientation="Horizontal" Align="Center" >
<Button Name="ViewButton" Text="{Loc objective-editor-admin-ui-view}" StyleClasses="OpenRight" />
<Button Name="ResetButton" Text="{Loc objective-editor-admin-ui-reset}" StyleClasses="OpenLeft"/>
</BoxContainer>
</BoxContainer>
</BoxContainer>
</BoxContainer>

View File

@ -0,0 +1,143 @@
using Content.Client._DV.UserInterfaces.Controls;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Console;
using Robust.Shared.Utility;
namespace Content.Client._DV.Objectives.Eui;
[GenerateTypedNameReferences]
public sealed partial class ObjectiveContainer : BoxContainer
{
[Dependency] private readonly EntityManager _entityManager = default!;
[Dependency] private readonly IConsoleHost _console = default!;
private readonly SpriteSystem _sprite;
private readonly SpritePickerWindow _spritePicker = new();
private ObjectiveUI? _objective = null;
public ObjectiveContainer()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_sprite = _entityManager.System<SpriteSystem>();
_spritePicker.OnSpriteSelected += SetIcon;
ObjectiveTitle.OnTextChanged += OnTitleChanged;
ObjectiveIssuer.OnTextChanged += OnIssuerChanged;
ObjectiveDescription.OnTextChanged += OnDescriptionChanged;
ObjectiveDescription.Placeholder = new Rope.Leaf(Loc.GetString("objective-editor-admin-ui-placeholder-description"));
ResetButton.OnPressed += _ => ResetObjective();
ViewButton.OnPressed += _ => OnViewObjective();
IconSelectButton.OnPressed += _ => OnOpenIconSelect();
ClearData();
}
public bool HasChanges()
{
return _objective?.HasChanges ?? false;
}
public void ClearData()
{
_objective = null;
ObjectiveIssuer.Editable = false;
ObjectiveTitle.Editable = false;
ObjectiveDescription.Editable = false;
ResetButton.Disabled = true;
ViewButton.Disabled = true;
IconSelectButton.Disabled = true;
ObjectiveIssuer.Text = "";
ObjectiveTitle.Text = "";
ObjectiveDescription.TextRope = new Rope.Leaf("");
IconSelectButton.TextureNormal = null;
}
public void SetData(ObjectiveUI data)
{
_objective = data;
ObjectiveIssuer.Editable = true;
ObjectiveTitle.Editable = true;
ObjectiveDescription.Editable = true;
ResetButton.Disabled = false;
ViewButton.Disabled = false;
IconSelectButton.Disabled = false;
ObjectiveIssuer.Text = _objective.Issuer;
ObjectiveTitle.Text = _objective.Title;
ObjectiveDescription.TextRope = new Rope.Leaf(_objective.Description);
IconSelectButton.TextureNormal = _sprite.Frame0(data.Icon);
}
public void ResetObjective()
{
if (_objective is null)
return;
_objective.Reset();
IconSelectButton.TextureNormal = _sprite.Frame0(_objective.Icon);
}
private void OnTitleChanged(LineEdit.LineEditEventArgs args)
{
if (_objective is null)
return;
_objective.Title = args.Text;
}
private void OnIssuerChanged(LineEdit.LineEditEventArgs args)
{
if (_objective is null)
return;
_objective.Issuer = args.Text;
}
private void OnDescriptionChanged(TextEdit.TextEditEventArgs args)
{
if (_objective is null)
return;
_objective.Description = Rope.Collapse(args.TextRope).Trim();
}
private void OnOpenIconSelect()
{
if (_objective is null)
return;
_spritePicker.OpenWithSprite(_objective.Icon);
}
private void OnViewObjective()
{
if (_objective is null)
return;
_console.ExecuteCommand($"vv {_objective.Entity}");
}
private void SetIcon(SpriteSpecifier sprite)
{
if (_objective is null)
return;
IconSelectButton.TextureNormal = _sprite.Frame0(sprite);
_objective.Icon = sprite;
}
}

View File

@ -0,0 +1,81 @@
using Content.Client.Eui;
using Content.Shared._DV.Objectives.Eui;
using Content.Shared.Eui;
using Content.Shared.Mind;
using Robust.Shared.Prototypes;
namespace Content.Client._DV.Objectives.Eui;
public sealed class ObjectiveEditorEui : BaseEui
{
[Dependency] private readonly EntityManager _entityManager = default!;
private readonly ObjectiveEditorUi _editorUi;
private Entity<MindComponent> _targetMind;
public ObjectiveEditorEui()
{
IoCManager.InjectDependencies(this);
_editorUi = new ObjectiveEditorUi();
_editorUi.SaveAction += SaveObjectives;
_editorUi.CreateAction += CreateObjective;
}
public override void Opened()
{
base.Opened();
_editorUi.OpenCentered();
}
public override void HandleState(EuiStateBase state)
{
if (state is not ObjectiveEditorEUIState s)
return;
var mind = _entityManager.GetEntity(s.TargetMind);
if (!_entityManager.TryGetComponent<MindComponent>(mind, out var comp))
{
// TODO: Log
return;
}
_targetMind = (mind, comp);
var name = comp.CharacterName ?? Loc.GetString("objective-editor-admin-ui-unknown-mind");
_editorUi.SetEditorTitle(name, comp.RoleType, s.Subtype);
_editorUi.SetObjectives(s.Objectives);
}
public override void HandleMessage(EuiMessageBase msg)
{
base.HandleMessage(msg);
switch (msg)
{
case ObjectiveEditorCreateResponse message:
HandleCreateResponse(message);
break;
}
}
private void HandleCreateResponse(ObjectiveEditorCreateResponse response)
{
if (response.Data == null)
{
// TODO(Barry): Logging
return;
}
_editorUi.AddObjective(response.Data);
}
private void SaveObjectives(bool silentSave)
{
SendMessage(new ObjectiveEditorSaveMessage(_editorUi.GetObjectives(), _entityManager.GetNetEntity(_targetMind), silentSave));
}
private void CreateObjective(EntProtoId? proto)
{
SendMessage(new ObjectiveEditorCreateMessage(proto));
}
}

View File

@ -0,0 +1,43 @@
<controls:FancyWindow
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:obj="clr-namespace:Content.Client._DV.Objectives.Eui"
Title="{Loc objective-editor-admin-ui-title}"
MinSize="800 400"
>
<BoxContainer Orientation="Vertical" >
<BoxContainer Orientation="Horizontal" Align="End">
<CheckBox
Name="SilentSave"
Text="{Loc objective-editor-admin-ui-silent-save}"
ToolTip="{Loc objective-editor-admin-ui-silent-save-tooltip}"
Margin="5 0 5 0"/>
<controls:ConfirmButton
Name="ResetButton"
Text="{Loc objective-editor-admin-ui-reset-all}"
StyleClasses="OpenRight"
ModulateSelfOverride="Red" />
<controls:ConfirmButton
Name="SaveButton"
Text="{Loc objective-editor-admin-ui-save}"
StyleClasses="OpenLeft"/>
</BoxContainer>
<customControls:HSeparator Margin = "5"/>
<BoxContainer Orientation="Horizontal" VerticalExpand="True">
<BoxContainer Orientation="Vertical" MinWidth="200" Margin="5">
<ItemList Name="ObjectiveList" VerticalExpand="True" HorizontalExpand="True" />
<BoxContainer Orientation="Horizontal" >
<Button Name="AddButton" Text="+" StyleClasses="OpenRight" />
<controls:ConfirmButton Name="RemoveButton" Text="-" StyleClasses="OpenLeft" ModulateSelfOverride="Red"/>
</BoxContainer>
</BoxContainer>
<obj:ObjectiveContainer Name="Container"/>
</BoxContainer>
<customControls:HSeparator />
<Label Text="{Loc objective-editor-admin-ui-check-note}" FontColorOverride="Red" Align="Center"/>
</BoxContainer>
</controls:FancyWindow>

View File

@ -0,0 +1,152 @@
using System.Linq;
using Content.Client.UserInterface.Controls;
using Content.Shared._DV.Objectives.Eui;
using Content.Shared.Mind;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
namespace Content.Client._DV.Objectives.Eui;
[GenerateTypedNameReferences]
public sealed partial class ObjectiveEditorUi : FancyWindow
{
private const int MaxTitleLength = 20;
private const string ChangeMarker = "(*)";
private List<ObjectiveUI> _objectives = new();
private ObjectiveUI? _selectedObjective = null;
public Action<bool>? SaveAction = null;
public Action<EntProtoId?>? CreateAction = null;
public ObjectiveEditorUi()
{
RobustXamlLoader.Load(this);
ResetButton.OnPressed += _ => OnResetObjectives();
SaveButton.OnPressed += _ => SaveAction?.Invoke(SilentSave.Pressed);
AddButton.OnPressed += _ => OnAddObjective();
RemoveButton.OnPressed += _ => OnDeleteObjective();
ObjectiveList.OnItemSelected += OnObjectiveSelected;
ObjectiveList.OnItemDeselected += OnObjectiveDeselected;
}
public List<ObjectiveData> GetObjectives()
{
return [.. _objectives.Where(x => !x.Deleted).Select(x => x.Current)];
}
public void SetEditorTitle(
string name,
ProtoId<RoleTypePrototype>? role,
LocId? subtype)
{
var roleText = role.HasValue ? role.Value.Id.ToString() : "N/A";
if (subtype.HasValue)
{
roleText += string.Format(" - {0}", Loc.GetString(subtype));
}
Title = Loc.GetString("objective-editor-admin-ui-title-filled", ("name", name), ("role", roleText));
}
public void SetObjectives(List<ObjectiveData> objectives)
{
_objectives.Clear();
_objectives = [.. objectives.Select(x => new ObjectiveUI(x))];
ObjectiveList.Clear();
ObjectiveList.ClearSelected();
if (objectives.Count == 0)
return; // Nothing to do
foreach (var objective in _objectives)
{
ObjectiveList.AddItem(GetShortTitle(objective.Title), metadata: objective);
}
}
public void AddObjective(ObjectiveData objective)
{
var data = new ObjectiveUI(objective);
_objectives.Add(data);
ObjectiveList.AddItem(GetShortTitle(data.Title), metadata: data);
}
private void OnObjectiveSelected(ItemList.ItemListSelectedEventArgs args)
{
var item = args.ItemList[args.ItemIndex];
_selectedObjective = (ObjectiveUI)item.Metadata!;
SetItemText(item, _selectedObjective);
Container.SetData(_selectedObjective);
}
private void OnObjectiveDeselected(ItemList.ItemListDeselectedEventArgs args)
{
var item = args.ItemList[args.ItemIndex];
SetItemText(item, (ObjectiveUI)item.Metadata!);
_selectedObjective = null;
Container.ClearData();
}
private void SetItemText(ItemList.Item item, ObjectiveUI data)
{
var hasMarker = item.Text!.StartsWith(ChangeMarker);
if (data.HasChanges && !hasMarker)
{
item.Text = string.Format("{0} {1}",
ChangeMarker,
GetShortTitle(data.Title)
);
}
else if (!data.HasChanges && hasMarker)
{
item.Text = GetShortTitle(data.Title);
}
}
private string GetShortTitle(string title)
{
var originalTitle = title;
return title.Length <= MaxTitleLength
? originalTitle
: $"{originalTitle[..(MaxTitleLength - 3)]}...";
}
private void OnResetObjectives()
{
Container.ResetObjective(); // Ensures that the Icon and whatever else is updated
_objectives.ForEach(x => x.Reset());
foreach (var item in ObjectiveList)
{
SetItemText(item, (ObjectiveUI)item.Metadata!);
}
}
private void OnAddObjective()
{
CreateAction?.Invoke(null);
}
private void OnDeleteObjective()
{
var selected = ObjectiveList.GetSelected();
if (!selected.Any())
return; // Nothing selected, do nothing
// We're only removing this from the list of things, never deleting the objective from our
// list in the case where a user needs to reset.
var item = selected.First();
ObjectiveList.Remove(item);
ObjectiveList.ClearSelected();
Container.ClearData();
((ObjectiveUI)item.Metadata!).Deleted = true;
}
}

View File

@ -0,0 +1,67 @@
using Content.Shared._DV.Objectives.Eui;
using Robust.Shared.Utility;
namespace Content.Client._DV.Objectives.Eui;
// TODO(Barry): Think of a better name for this
public sealed class ObjectiveUI(ObjectiveData data)
{
private readonly ObjectiveData _current = data;
private readonly ObjectiveData _original = data.Clone();
public bool HasChanges { get; private set; } = false;
public ObjectiveData Current => _current;
public bool Deleted = false;
public NetEntity Entity
{
get => _current.Entity;
set
{
_current.Entity = value;
HasChanges = true;
}
}
public string Issuer
{
get => _current.Issuer;
set
{
_current.Issuer = value;
HasChanges = true;
}
}
public string Title
{
get => _current.Info.Title;
set
{
_current.Info.Title = value;
HasChanges = true;
}
}
public string Description
{
get => _current.Info.Description;
set
{
_current.Info.Description = value;
HasChanges = true;
}
}
public SpriteSpecifier Icon
{
get => _current.Info.Icon;
set
{
_current.Info.Icon = value;
HasChanges = true;
}
}
public void Reset()
{
_current.CopyFrom(_original);
HasChanges = false;
Deleted = false;
}
}

View File

@ -0,0 +1,42 @@
<controls:FancyWindow
Title="{Loc 'sprite-selector-ui-window-title'}"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls">
<BoxContainer MinWidth="300" MinHeight="200" Orientation="Vertical">
<PanelContainer>
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#1B1B1B" />
</PanelContainer.PanelOverride>
<TextureRect
Name="PreviewTexture"
VerticalAlignment="Top"
HorizontalAlignment="Center"
Stretch="KeepAspectCentered"
MinWidth="128"
MinHeight="128"
Margin="0 8 0 0"/>
</PanelContainer>
<TabContainer Name="TextureTypes">
<BoxContainer Name="RSISelection" Orientation="Vertical">
<LineEdit Name="RSIPath" />
<OptionButton Name="RSIStates" />
</BoxContainer>
<BoxContainer Name="TextureSelection" Orientation="Vertical">
<LineEdit Name="TexturePath" />
</BoxContainer>
</TabContainer>
<Label Name="ErrorText" Visible="False" Align="Center" FontColorOverride="Red"/>
<BoxContainer Orientation="Horizontal" HorizontalAlignment="Center" Margin="5">
<Button Name="OkButton" Text="{Loc 'sprite-selector-ui-ok'}"/>
<Button Name="CancelButton" Text="{Loc 'sprite-selector-ui-cancel'}" ModulateSelfOverride="Red" />
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@ -0,0 +1,174 @@
using Content.Client.UserInterface.Controls;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Serialization.TypeSerializers.Implementations;
using Robust.Shared.Utility;
namespace Content.Client._DV.UserInterfaces.Controls;
[GenerateTypedNameReferences]
public sealed partial class SpritePickerWindow : FancyWindow
{
private readonly SpriteSystem _sprite;
private readonly IResourceCache _cache;
public Action<SpriteSpecifier>? OnSpriteSelected;
private SpriteSpecifier _selectedSprite = SpriteSpecifier.Invalid;
public SpritePickerWindow()
{
RobustXamlLoader.Load(this);
var entMan = IoCManager.Resolve<EntityManager>();
_sprite = entMan.System<SpriteSystem>();
_cache = IoCManager.Resolve<IResourceCache>();
TextureTypes.SetTabTitle(0, Loc.GetString("sprite-selector-ui-rsi-tab"));
TextureTypes.SetTabTitle(1, Loc.GetString("sprite-selector-ui-texture-tab"));
RSIPath.OnTextChanged += OnRSIPathChanged;
RSIPath.PlaceHolder = Loc.GetString("sprite-selector-ui-rsi-placeholder");
RSIStates.OnItemSelected += OnRSIStateSelected;
TexturePath.OnTextChanged += OnTexturePathChanged;
TexturePath.PlaceHolder = Loc.GetString("sprite-selector-ui-texture-placeholder");
OkButton.OnPressed += _ => OnConfirm();
CancelButton.OnPressed += _ => Close();
}
public void OpenWithSprite(SpriteSpecifier sprite)
{
ErrorText.Visible = false;
// Select the right tab
if (sprite is SpriteSpecifier.Rsi rsi)
{
RSIPath.Text = rsi.RsiPath.CanonPath;
SetAvailableStates(rsi.RsiPath, initialState: rsi.RsiState);
TexturePath.Text = "";
}
else if (sprite is SpriteSpecifier.Texture texture)
{
TexturePath.Text = texture.TexturePath.CanonPath;
RSIPath.Text = "";
}
else
{
// Clear everything as this was invalid
TexturePath.Text = "";
RSIPath.Text = "";
}
OpenCentered();
}
private void OnRSIPathChanged(LineEdit.LineEditEventArgs args)
{
if (!args.Text.EndsWith(".rsi"))
return; // User might still be typing this out, don't bother validating just yet
SetAvailableStates(new ResPath(args.Text));
}
private void OnRSIStateSelected(OptionButton.ItemSelectedEventArgs args)
{
RSIStates.SelectId(args.Id);
var state = (RSI.State)RSIStates.SelectedMetadata!;
_selectedSprite = new SpriteSpecifier.Rsi(new ResPath(RSIPath.Text), state.StateId.Name!);
UpdatePreview();
}
private void SetAvailableStates(ResPath resPath, string? initialState = null)
{
// Textures/Sprites MUST be relative to the Texture root or else the cache won't find them.
if (!resPath.TryRelativeTo(SpriteSpecifierSerializer.TextureRoot, out _))
resPath = SpriteSpecifierSerializer.TextureRoot / resPath;
if (!_cache.TryGetResource<RSIResource>(resPath, out var rsi))
{
ErrorText.Visible = true;
ErrorText.Text = Loc.GetString("sprite-selector-ui-rsi-not-found");
return;
}
ErrorText.Visible = false;
RSIStates.Clear();
var initialSelection = 0;
var i = 0;
foreach (var state in rsi.RSI)
{
var name = state.StateId.Name!;
RSIStates.AddItem(name, i);
RSIStates.SetItemMetadata(i, state);
if (initialState != null && name == initialState)
initialSelection = i;
i++;
}
if (RSIStates.ChildCount == 0)
return; // No states to show
// Setup the icon to show something when the user swaps to it
string rsiState;
if (initialState == null)
{
// Select the first state available so it shows something to the user
var firstState = (RSI.State)RSIStates.SelectedMetadata!;
rsiState = firstState.StateId.Name!;
}
else
{
RSIStates.Select(initialSelection);
rsiState = initialState;
}
_selectedSprite = new SpriteSpecifier.Rsi(new ResPath(RSIPath.Text), rsiState);
UpdatePreview();
}
private void UpdatePreview()
{
PreviewTexture.Texture = _sprite.Frame0(_selectedSprite);
}
private void OnTexturePathChanged(LineEdit.LineEditEventArgs args)
{
if (!args.Text.EndsWith(".png"))
return; // User might still be typing this out, don't bother validating just yet
var resPath = new ResPath(args.Text);
if (!resPath.TryRelativeTo(SpriteSpecifierSerializer.TextureRoot, out _))
resPath = SpriteSpecifierSerializer.TextureRoot / resPath;
if (!_cache.TryGetResource<TextureResource>(resPath, out var _))
{
ErrorText.Visible = true;
ErrorText.Text = Loc.GetString("sprite-selector-ui-texture-not-found");
return;
}
ErrorText.Visible = false;
_selectedSprite = new SpriteSpecifier.Texture(resPath);
UpdatePreview();
}
private void OnConfirm()
{
OnSpriteSelected?.Invoke(_selectedSprite);
Close();
}
}

View File

@ -36,8 +36,6 @@ using Robust.Shared.Toolshed;
using Robust.Shared.Utility;
using System.Linq;
using static Content.Shared.Configurable.ConfigurationComponent;
using Content.Shared._Impstation.Thaven.Components; // DeltaV
using Content.Server._Impstation.Thaven; // DeltaV
namespace Content.Server.Administration.Systems
{
@ -68,7 +66,6 @@ namespace Content.Server.Administration.Systems
[Dependency] private readonly AdminFrozenSystem _freeze = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly SiliconLawSystem _siliconLawSystem = default!;
[Dependency] private readonly ThavenMoodsSystem _moods = default!; // DeltaV
private readonly Dictionary<ICommonSession, List<EditSolutionsEui>> _openSolutionUis = new();
@ -403,26 +400,7 @@ namespace Content.Server.Administration.Systems
});
}
// Begin DeltaV Additions - thaven moods
if (TryComp<ThavenMoodsComponent>(args.Target, out var moods))
{
args.Verbs.Add(new Verb()
{
Text = Loc.GetString("thaven-moods-ui-verb"),
Category = VerbCategory.Admin,
Act = () =>
{
var ui = new ThavenMoodsEui(_moods, EntityManager, _adminManager);
if (!_playerManager.TryGetSessionByEntity(args.User, out var session))
return;
_euiManager.OpenEui(ui, session);
ui.UpdateMoods(moods, args.Target);
},
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Interface/Actions/actions_borg.rsi"), "state-laws"),
});
}
// End DeltaV Additions
AddDVAdminVerbs(args); // DeltaV - Extra admin verbs
// open camera
args.Verbs.Add(new Verb()

View File

@ -215,7 +215,7 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
}
}
var successRate = totalObjectives > 0 ? (float) completedObjectives / totalObjectives : 0f;
var successRate = totalObjectives > 0 ? (float)completedObjectives / totalObjectives : 0f;
// Begin DeltaV Additions - custom objective response.
if (TryComp<CustomObjectiveSummaryComponent>(mindId, out var customComp) &&
customComp.ObjectiveSummary.Length <= _maxLengthSummaryLength)
@ -352,6 +352,19 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
.Select(p => p.ID)
.Order();
}
/// <summary>
/// Sets the issuer for a given Objective
/// </summary>
/// <param name="objective">The objective to set the issuer for.</param>
/// <param name="issuer">The new issuer of the objective.</param>
public void SetIssuer(EntityUid objective, LocId issuer)
{
if (!TryComp<ObjectiveComponent>(objective, out var comp))
return;
comp.Issuer = issuer;
}
}
/// <summary>

View File

@ -0,0 +1,55 @@
using Content.Server._DV.Objectives.Eui;
using Content.Server._Impstation.Thaven;
using Content.Shared._Impstation.Thaven.Components;
using Content.Server.Objectives;
using Content.Shared.Verbs;
using Robust.Shared.Utility;
namespace Content.Server.Administration.Systems;
public sealed partial class AdminVerbSystem : EntitySystem
{
[Dependency] private readonly ThavenMoodsSystem _moods = default!;
private void AddDVAdminVerbs(GetVerbsEvent<Verb> args)
{
if (TryComp<ThavenMoodsComponent>(args.Target, out var moods))
{
args.Verbs.Add(new Verb()
{
Text = Loc.GetString("thaven-moods-ui-verb"),
Category = VerbCategory.Admin,
Act = () =>
{
var ui = new ThavenMoodsEui(_moods, EntityManager, _adminManager);
if (!_playerManager.TryGetSessionByEntity(args.User, out var session))
return;
_euiManager.OpenEui(ui, session);
ui.UpdateMoods(moods, args.Target);
},
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Interface/Actions/actions_borg.rsi"), "state-laws"),
});
}
if (_mindSystem.TryGetMind(args.Target, out var mindId, out var mindComp) &&
mindComp.UserId != null)
{
args.Verbs.Add(new Verb()
{
Text = Loc.GetString("Edit Objectives"),
Category = VerbCategory.Admin,
Act = () =>
{
if (!_playerManager.TryGetSessionByEntity(args.User, out var session))
return;
var ui = new ObjectiveEditorEui();
_euiManager.OpenEui(ui, session);
ui.UpdateObjectivesFor((mindId, mindComp));
},
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Interface/Actions/actions_borg.rsi"), "state-laws"),
});
}
}
}

View File

@ -0,0 +1,260 @@
using Content.Server.Administration.Managers;
using Content.Server.EUI;
using Content.Server.Mind;
using Content.Server.Objectives;
using Content.Shared.Objectives;
using Content.Shared._DV.Objectives.Eui;
using Content.Shared.Administration;
using Content.Shared.Eui;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using System.Linq;
using Content.Server.Popups;
namespace Content.Server._DV.Objectives.Eui;
public sealed class ObjectiveEditorEui : BaseEui
{
[Dependency] private readonly EntityManager _entityManager = default!;
[Dependency] private readonly IAdminManager _adminManager = default!;
private readonly ObjectivesSystem _objectiveSystem = default!;
private readonly MindSystem _mind = default!;
private readonly MetaDataSystem _metadata = default!;
private readonly PopupSystem _popup = default!;
private readonly ISawmill _sawmill = Logger.GetSawmill("objective-editor-eui"); // TODO(Barry): Actually need this or?
private readonly Dictionary<EntityUid, ObjectiveData> _objectives = [];
private readonly EntProtoId _fallbackObjective = "EditorDefaultObjective";
private readonly ObjectiveInfo _fallbackData = new ObjectiveInfo(
"Title",
"Description",
new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Nano/cross.svg.png")),
0f
);
private Entity<MindComponent> _targetMind;
/// <summary>
/// A list of temporary objective entities that have not yet been
/// added to the target's mind.
/// </summary>
private List<EntityUid> _temporaryIds = [];
public ObjectiveEditorEui()
{
IoCManager.InjectDependencies(this);
_objectiveSystem = _entityManager.System<ObjectivesSystem>();
_mind = _entityManager.System<MindSystem>();
_metadata = _entityManager.System<MetaDataSystem>();
_popup = _entityManager.System<PopupSystem>();
}
public override EuiStateBase GetNewState()
{
return new ObjectiveEditorEUIState(
[.. _objectives.Values],
_entityManager.GetNetEntity(_targetMind.Owner),
_targetMind.Comp.Subtype);
}
public override void Closed()
{
base.Closed();
ClearTempObjectives();
}
/// <summary>
/// Grabs objectives for the specified mind and sets the UI state to reflect it.
/// </summary>
/// <param name="mind">The target mind to grab objectives for.</param>
public void UpdateObjectivesFor(Entity<MindComponent> mind)
{
if (!IsAllowed())
return;
_targetMind = mind;
foreach (var objective in mind.Comp.Objectives)
{
_objectives.Add(objective, CreateObjectiveData(objective, mind));
}
ClearTempObjectives();
StateDirty();
}
public override void HandleMessage(EuiMessageBase msg)
{
base.HandleMessage(msg);
if (!IsAllowed())
return;
switch (msg)
{
case ObjectiveEditorSaveMessage message:
HandleSaveMessage(message);
break;
case ObjectiveEditorCreateMessage message:
HandleCreateMessage(message);
break;
}
}
/// <summary>
/// Handle when a user on the client side wishes to save their current objective changes.
/// </summary>
/// <param name="message">The save message, which includes the new objectives.</param>
private void HandleSaveMessage(ObjectiveEditorSaveMessage message)
{
var oldObjectives = _objectives.ShallowClone();
_objectives.Clear(); // Clean up the previous set we knew about
foreach (var incomingObjective in message.Objectives)
{
var objEnt = _entityManager.GetEntity(incomingObjective.Entity);
EntityUid objective;
if (oldObjectives.ContainsKey(objEnt))
{
// We can just update this entity, no need to add it to the mind either
objective = objEnt;
oldObjectives.Remove(objEnt); // Ensure it's not cleaned up
}
else if (_temporaryIds.Contains(objEnt))
{
objective = objEnt;
_temporaryIds.Remove(objEnt); // Ensure it's not cleaned up
_mind.AddObjective(_targetMind.Owner, _targetMind.Comp, objective);
}
else
{
// Gotta make a new entity for it
var newObjective = CreateObjective(incomingObjective.Proto);
if (!newObjective.HasValue)
continue; // TODO(Barry) Log an error
objective = newObjective.Value;
// Ensure we add the new objective we've just created
_mind.AddObjective(_targetMind.Owner, _targetMind.Comp, objective);
}
var metadata = _entityManager.GetComponent<MetaDataComponent>(objective);
// Setup new Title/Description
_metadata.SetEntityName(objective, incomingObjective.Info.Title, metadata: metadata);
_metadata.SetEntityDescription(objective, incomingObjective.Info.Description, metadata: metadata);
// Now the issuer and Icon
_objectiveSystem.SetIssuer(objective, incomingObjective.Issuer);
_objectiveSystem.SetIcon(objective, incomingObjective.Info.Icon);
_objectives.Add(objective, CreateObjectiveData(objective, _targetMind));
}
foreach (var old in oldObjectives.Keys)
{
_mind.TryRemoveObjective(_targetMind, _targetMind.Comp, old);
}
ClearTempObjectives(); // Ensure all temporary objectives are cleared and deleted
StateDirty(); // Ensure client is freshly updated with all the new Objective information
if (!message.Silent && _targetMind.Comp.CurrentEntity is EntityUid uid)
{
_popup.PopupEntity(Loc.GetString("objective-editor-admin-ui-notification"), uid, uid);
}
}
/// <summary>
/// Handles when a user on the client side wants to create a whole new objective.
/// The new objective entity will be stored temporarily until either the window closes,
/// or the user performs a save.
/// </summary>
/// <param name="message">The create message, which may include a ProtoId to use.</param>
private void HandleCreateMessage(ObjectiveEditorCreateMessage message)
{
var newObjective = CreateObjective(message.Proto);
if (!newObjective.HasValue)
return;
// N.b. The objective is not yet added to the mind.
var data = CreateObjectiveData(newObjective.Value, _targetMind);
_temporaryIds.Add(newObjective.Value);
SendMessage(new ObjectiveEditorCreateResponse(data));
}
/// <summary>
/// Creates an ObjectiveData structure for the specified objective entity.
/// </summary>
/// <param name="objective">The objective entity.</param>
/// <param name="mind">The mind the objective is bound to.</param>
/// <param name="data">The output ObjectiveData structure.</param>
private ObjectiveData CreateObjectiveData(
EntityUid objective,
Entity<MindComponent> mind)
{
var info = _objectiveSystem.GetInfo(objective, mind, mind.Comp);
if (!info.HasValue)
info = _fallbackData;
var metadata = _entityManager.GetComponent<MetaDataComponent>(objective);
var issuer = _entityManager.GetComponent<ObjectiveComponent>(objective).LocIssuer;
return new ObjectiveData(
_entityManager.GetNetEntity(objective),
issuer,
metadata.EntityPrototype?.ID,
info.Value);
}
/// <summary>
/// Clears all temporary objectives and queues them for deletion.
/// </summary>
private void ClearTempObjectives()
{
foreach (var ent in _temporaryIds)
{
_entityManager.QueueDeleteEntity(ent);
}
}
/// <summary>
/// Creates a new entity matching the specified Entity Prototype or a fallback, if that fails.
/// </summary>
/// <param name="prototype">The prototype to use, may be null.</param>
/// <returns>The EntityUid of the new objective if successful, otherwise null.</returns>
private EntityUid? CreateObjective(EntProtoId? prototype)
{
EntityUid? objective = null;
if (prototype.HasValue)
objective = _objectiveSystem.TryCreateObjective(_targetMind, _targetMind.Comp, prototype.Value);
if (!objective.HasValue)
objective = _objectiveSystem.TryCreateObjective(_targetMind, _targetMind.Comp, _fallbackObjective);
return objective;
}
/// <summary>
/// Checks whether the current user is allowed to edit the objectives of any mind.
/// </summary>
/// <returns>True if allowed, otherwise False.</returns>
private bool IsAllowed()
{
var adminData = _adminManager.GetAdminData(Player);
if (adminData == null || !adminData.HasFlag(AdminFlags.Moderator))
{
_sawmill.Warning($"Player {Player.UserId} tried to open / use player objective editor UI without permission.");
return false;
}
return true;
}
}

View File

@ -366,6 +366,7 @@ public abstract partial class SharedMindSystem : EntitySystem
mind.Objectives.Add(objective);
}
// Begin DeltaV Additions - Additional RemoveObjective functions
/// <summary>
/// Removes an objective from this mind.
/// </summary>
@ -375,7 +376,16 @@ public abstract partial class SharedMindSystem : EntitySystem
if (index < 0 || index >= mind.Objectives.Count)
return false;
var objective = mind.Objectives[index];
return TryRemoveObjective(mindId, mind, mind.Objectives[index]);
}
/// Removes an objective from this mind.
/// </summary>
/// <returns>Returns true if the removal succeeded.</returns>
public bool TryRemoveObjective(EntityUid mindId, MindComponent mind, EntityUid objective)
{
if (!mind.Objectives.Contains(objective))
return false;
var title = Name(objective);
_adminLogger.Add(LogType.Mind, LogImpact.Low, $"Objective {objective} ({title}) removed from the mind of {MindOwnerLoggingString(mind)}");
@ -393,6 +403,7 @@ public abstract partial class SharedMindSystem : EntitySystem
Del(objective);
return true;
}
// End DeltaV Additions
public bool TryGetObjectiveComp<T>(EntityUid uid, [NotNullWhen(true)] out T? objective) where T : IComponent
{

View File

@ -23,7 +23,7 @@ public sealed partial class ObjectiveComponent : Component
/// Organisation that issued this objective, used for grouping and as a header above common objectives.
/// </summary>
[DataField("issuer", required: true)]
private LocId Issuer { get; set; }
public LocId Issuer { get; set; } // DeltaV - Made public
[ViewVariables(VVAccess.ReadOnly)]
public string LocIssuer => Loc.GetString(Issuer);

View File

@ -0,0 +1,42 @@
using Content.Shared.Objectives;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared._DV.Objectives.Eui;
/// <summary>
/// Bundle of data for objectives to display for editing.
/// </summary>
/// <param name="entity">The original objective entity.</param>
/// <param name="issuer">The issuer for the objective.</param>
/// <param name="proto">An optional prototype used to spawn the entity.</param>
/// <param name="info">All other ObjectiveInfo for the entity.</param>
[Serializable, NetSerializable]
public sealed class ObjectiveData(NetEntity entity, string issuer, EntProtoId? proto, ObjectiveInfo info)
{
public NetEntity Entity = entity;
public string Issuer = issuer;
public EntProtoId? Proto = proto;
public ObjectiveInfo Info = info;
/// <summary>
/// Provides a clone of the data.
/// </summary>
/// <returns>A full copy of the original data.</returns>
public ObjectiveData Clone()
{
return (ObjectiveData)MemberwiseClone();
}
/// <summary>
/// Copies data from another ObjectiveData.
/// </summary>
/// <param name="other">The other data to copy from.</param>
public void CopyFrom(ObjectiveData other)
{
Entity = other.Entity;
Issuer = other.Issuer;
Proto = other.Proto;
Info = other.Info;
}
}

View File

@ -0,0 +1,40 @@
using Content.Shared.Eui;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared._DV.Objectives.Eui;
[Serializable, NetSerializable]
public sealed class ObjectiveEditorEUIState(
List<ObjectiveData> objectives,
NetEntity targetMind,
LocId? subtype) : EuiStateBase
{
public List<ObjectiveData> Objectives { get; } = objectives;
public NetEntity TargetMind { get; } = targetMind;
public LocId? Subtype = subtype;
}
[Serializable, NetSerializable]
public sealed class ObjectiveEditorSaveMessage(
List<ObjectiveData> objectives, NetEntity target, bool silent) : EuiMessageBase
{
public List<ObjectiveData> Objectives = objectives;
public NetEntity Target = target;
public bool Silent = silent;
}
[Serializable, NetSerializable]
public sealed class ObjectiveEditorCreateMessage(EntProtoId? proto = null) : EuiMessageBase
{
public EntProtoId? Proto = proto;
}
[Serializable, NetSerializable]
public sealed class ObjectiveEditorCreateResponse(ObjectiveData? data) : EuiMessageBase
{
public ObjectiveData? Data = data;
}

View File

@ -0,0 +1,24 @@
objective-editor-admin-ui-title = Objectives Editor
objective-editor-admin-ui-title-filled = Objectives Editor - {$name} | {$role}
objective-editor-admin-ui-unknown-mind = Unknown
objective-editor-admin-ui-new-objective = New
objective-editor-admin-ui-save = Save
objective-editor-admin-ui-delete = Delete
objective-editor-admin-ui-delete-all = Delete All
objective-editor-admin-ui-view = View
objective-editor-admin-ui-reset = Reset
objective-editor-admin-ui-reset-all = Reset All
objective-editor-admin-ui-placeholder-issuer = New Issuer
objective-editor-admin-ui-placeholder-title = New Title
objective-editor-admin-ui-placeholder-description = New Description
objective-editor-admin-ui-check-note = The editor does NOT update the checks for an objective
objective-editor-admin-ui-silent-save = Silent Save
objective-editor-admin-ui-silent-save-tooltip = Whether to show a popup to the user when their objectives change
objective-editor-admin-ui-notification = You feel new ideas enter your mind...

View File

@ -0,0 +1,11 @@
sprite-selector-ui-window-title = Sprite Selector
sprite-selector-ui-ok = Ok
sprite-selector-ui-cancel = Cancel
sprite-selector-ui-rsi-tab = RSI
sprite-selector-ui-rsi-placeholder = /Textures/Tips/tippy.rsi
sprite-selector-ui-rsi-not-found = Unable to find the specified RSI
sprite-selector-ui-texture-tab = Texture
sprite-selector-ui-texture-placeholder = /Textures/Tips/tippy.rsi/down.png
sprite-selector-ui-texture-not-found = Unable to find the specified texture

View File

@ -0,0 +1,11 @@
- type: entity
parent: BaseObjective
id: EditorDefaultObjective
name: Default Title
description: Default Description
components:
- type: Objective
issuer: Default Issuer
difficulty: 0
icon: Interface/Nano/cross.svg.png
- type: FreeObjective