Lobby refactor + species loadouts support (#27576)

* Vox stuff

* Species loadouts and lobby refactor

The control flow for lobby is all over the shop so I pulled it all up from the individual controls so now they handle the bare minimum required and LobbyUIController handles the rest.

* a

* Bulk changes

* a

* weh

* Character import / export

* finalise

* woops this stuff too

* Also datafield exporting

* comments

* Review
This commit is contained in:
metalgearsloth
2024-05-12 09:18:21 +10:00
committed by GitHub
parent 999d044139
commit 332f54a3ae
49 changed files with 1867 additions and 1563 deletions

View File

@@ -10,10 +10,10 @@ using Content.Client.Info;
using Content.Client.Input; using Content.Client.Input;
using Content.Client.IoC; using Content.Client.IoC;
using Content.Client.Launcher; using Content.Client.Launcher;
using Content.Client.Lobby;
using Content.Client.MainMenu; using Content.Client.MainMenu;
using Content.Client.Parallax.Managers; using Content.Client.Parallax.Managers;
using Content.Client.Players.PlayTimeTracking; using Content.Client.Players.PlayTimeTracking;
using Content.Client.Preferences;
using Content.Client.Radiation.Overlays; using Content.Client.Radiation.Overlays;
using Content.Client.Replay; using Content.Client.Replay;
using Content.Client.Screenshot; using Content.Client.Screenshot;

View File

@@ -2,23 +2,20 @@ using Content.Client.Administration.Managers;
using Content.Client.Changelog; using Content.Client.Changelog;
using Content.Client.Chat.Managers; using Content.Client.Chat.Managers;
using Content.Client.Clickable; using Content.Client.Clickable;
using Content.Client.Options;
using Content.Client.Eui; using Content.Client.Eui;
using Content.Client.GhostKick; using Content.Client.GhostKick;
using Content.Client.Info; using Content.Client.Info;
using Content.Client.Launcher; using Content.Client.Launcher;
using Content.Client.Parallax.Managers; using Content.Client.Parallax.Managers;
using Content.Client.Players.PlayTimeTracking; using Content.Client.Players.PlayTimeTracking;
using Content.Client.Preferences;
using Content.Client.Screenshot; using Content.Client.Screenshot;
using Content.Client.Fullscreen; using Content.Client.Fullscreen;
using Content.Client.Stylesheets; using Content.Client.Stylesheets;
using Content.Client.Viewport; using Content.Client.Viewport;
using Content.Client.Voting; using Content.Client.Voting;
using Content.Shared.Administration;
using Content.Shared.Administration.Logs; using Content.Shared.Administration.Logs;
using Content.Shared.Module;
using Content.Client.Guidebook; using Content.Client.Guidebook;
using Content.Client.Lobby;
using Content.Client.Replay; using Content.Client.Replay;
using Content.Shared.Administration.Managers; using Content.Shared.Administration.Managers;
using Content.Shared.Players.PlayTimeTracking; using Content.Shared.Players.PlayTimeTracking;

View File

@@ -2,12 +2,10 @@ using System.Linq;
using Content.Shared.Preferences; using Content.Shared.Preferences;
using Robust.Client; using Robust.Client;
using Robust.Client.Player; using Robust.Client.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Network; using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility; using Robust.Shared.Utility;
namespace Content.Client.Preferences namespace Content.Client.Lobby
{ {
/// <summary> /// <summary>
/// Receives <see cref="PlayerPreferences" /> and <see cref="GameSettings" /> from the server during the initial /// Receives <see cref="PlayerPreferences" /> and <see cref="GameSettings" /> from the server during the initial

View File

@@ -1,7 +1,6 @@
using System;
using Content.Shared.Preferences; using Content.Shared.Preferences;
namespace Content.Client.Preferences namespace Content.Client.Lobby
{ {
public interface IClientPreferencesManager public interface IClientPreferencesManager
{ {

View File

@@ -3,8 +3,6 @@ using Content.Client.GameTicking.Managers;
using Content.Client.LateJoin; using Content.Client.LateJoin;
using Content.Client.Lobby.UI; using Content.Client.Lobby.UI;
using Content.Client.Message; using Content.Client.Message;
using Content.Client.Preferences;
using Content.Client.Preferences.UI;
using Content.Client.UserInterface.Systems.Chat; using Content.Client.UserInterface.Systems.Chat;
using Content.Client.Voting; using Content.Client.Voting;
using Robust.Client; using Robust.Client;
@@ -12,8 +10,6 @@ using Robust.Client.Console;
using Robust.Client.ResourceManagement; using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing; using Robust.Shared.Timing;
@@ -25,20 +21,15 @@ namespace Content.Client.Lobby
[Dependency] private readonly IClientConsoleHost _consoleHost = default!; [Dependency] private readonly IClientConsoleHost _consoleHost = default!;
[Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!; [Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!; [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
[Dependency] private readonly IClientPreferencesManager _preferencesManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IVoteManager _voteManager = default!; [Dependency] private readonly IVoteManager _voteManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[ViewVariables] private CharacterSetupGui? _characterSetup;
private ClientGameTicker _gameTicker = default!; private ClientGameTicker _gameTicker = default!;
private ContentAudioSystem _contentAudioSystem = default!; private ContentAudioSystem _contentAudioSystem = default!;
protected override Type? LinkedScreenType { get; } = typeof(LobbyGui); protected override Type? LinkedScreenType { get; } = typeof(LobbyGui);
private LobbyGui? _lobby; public LobbyGui? Lobby;
protected override void Startup() protected override void Startup()
{ {
@@ -47,45 +38,23 @@ namespace Content.Client.Lobby
return; return;
} }
_lobby = (LobbyGui) _userInterfaceManager.ActiveScreen; Lobby = (LobbyGui) _userInterfaceManager.ActiveScreen;
var chatController = _userInterfaceManager.GetUIController<ChatUIController>(); var chatController = _userInterfaceManager.GetUIController<ChatUIController>();
_gameTicker = _entityManager.System<ClientGameTicker>(); _gameTicker = _entityManager.System<ClientGameTicker>();
_contentAudioSystem = _entityManager.System<ContentAudioSystem>(); _contentAudioSystem = _entityManager.System<ContentAudioSystem>();
_contentAudioSystem.LobbySoundtrackChanged += UpdateLobbySoundtrackInfo; _contentAudioSystem.LobbySoundtrackChanged += UpdateLobbySoundtrackInfo;
_characterSetup = new CharacterSetupGui(_entityManager, _resourceCache, _preferencesManager,
_prototypeManager, _configurationManager);
LayoutContainer.SetAnchorPreset(_characterSetup, LayoutContainer.LayoutPreset.Wide);
_lobby.CharacterSetupState.AddChild(_characterSetup);
chatController.SetMainChat(true); chatController.SetMainChat(true);
_voteManager.SetPopupContainer(_lobby.VoteContainer); _voteManager.SetPopupContainer(Lobby.VoteContainer);
LayoutContainer.SetAnchorPreset(Lobby, LayoutContainer.LayoutPreset.Wide);
_characterSetup.CloseButton.OnPressed += _ => Lobby.ServerName.Text = _baseClient.GameInfo?.ServerName; //The eye of refactor gazes upon you...
{
// Reset sliders etc.
_characterSetup?.UpdateControls();
var controller = _userInterfaceManager.GetUIController<LobbyUIController>();
controller.SetClothes(true);
controller.UpdateProfile();
_lobby.SwitchState(LobbyGui.LobbyGuiState.Default);
};
_characterSetup.SaveButton.OnPressed += _ =>
{
_characterSetup.Save();
_userInterfaceManager.GetUIController<LobbyUIController>().ReloadProfile();
};
LayoutContainer.SetAnchorPreset(_lobby, LayoutContainer.LayoutPreset.Wide);
_lobby.ServerName.Text = _baseClient.GameInfo?.ServerName; //The eye of refactor gazes upon you...
UpdateLobbyUi(); UpdateLobbyUi();
_lobby.CharacterPreview.CharacterSetupButton.OnPressed += OnSetupPressed; Lobby.CharacterPreview.CharacterSetupButton.OnPressed += OnSetupPressed;
_lobby.ReadyButton.OnPressed += OnReadyPressed; Lobby.ReadyButton.OnPressed += OnReadyPressed;
_lobby.ReadyButton.OnToggled += OnReadyToggled; Lobby.ReadyButton.OnToggled += OnReadyToggled;
_gameTicker.InfoBlobUpdated += UpdateLobbyUi; _gameTicker.InfoBlobUpdated += UpdateLobbyUi;
_gameTicker.LobbyStatusUpdated += LobbyStatusUpdated; _gameTicker.LobbyStatusUpdated += LobbyStatusUpdated;
@@ -103,20 +72,23 @@ namespace Content.Client.Lobby
_voteManager.ClearPopupContainer(); _voteManager.ClearPopupContainer();
_lobby!.CharacterPreview.CharacterSetupButton.OnPressed -= OnSetupPressed; Lobby!.CharacterPreview.CharacterSetupButton.OnPressed -= OnSetupPressed;
_lobby!.ReadyButton.OnPressed -= OnReadyPressed; Lobby!.ReadyButton.OnPressed -= OnReadyPressed;
_lobby!.ReadyButton.OnToggled -= OnReadyToggled; Lobby!.ReadyButton.OnToggled -= OnReadyToggled;
_lobby = null; Lobby = null;
}
_characterSetup?.Dispose(); public void SwitchState(LobbyGui.LobbyGuiState state)
_characterSetup = null; {
// Yeah I hate this but LobbyState contains all the badness for now.
Lobby?.SwitchState(state);
} }
private void OnSetupPressed(BaseButton.ButtonEventArgs args) private void OnSetupPressed(BaseButton.ButtonEventArgs args)
{ {
SetReady(false); SetReady(false);
_lobby!.SwitchState(LobbyGui.LobbyGuiState.CharacterSetup); Lobby?.SwitchState(LobbyGui.LobbyGuiState.CharacterSetup);
} }
private void OnReadyPressed(BaseButton.ButtonEventArgs args) private void OnReadyPressed(BaseButton.ButtonEventArgs args)
@@ -138,13 +110,13 @@ namespace Content.Client.Lobby
{ {
if (_gameTicker.IsGameStarted) if (_gameTicker.IsGameStarted)
{ {
_lobby!.StartTime.Text = string.Empty; Lobby!.StartTime.Text = string.Empty;
var roundTime = _gameTiming.CurTime.Subtract(_gameTicker.RoundStartTimeSpan); var roundTime = _gameTiming.CurTime.Subtract(_gameTicker.RoundStartTimeSpan);
_lobby!.StationTime.Text = Loc.GetString("lobby-state-player-status-round-time", ("hours", roundTime.Hours), ("minutes", roundTime.Minutes)); Lobby!.StationTime.Text = Loc.GetString("lobby-state-player-status-round-time", ("hours", roundTime.Hours), ("minutes", roundTime.Minutes));
return; return;
} }
_lobby!.StationTime.Text = Loc.GetString("lobby-state-player-status-round-not-started"); Lobby!.StationTime.Text = Loc.GetString("lobby-state-player-status-round-not-started");
string text; string text;
if (_gameTicker.Paused) if (_gameTicker.Paused)
@@ -153,7 +125,7 @@ namespace Content.Client.Lobby
} }
else if (_gameTicker.StartTime < _gameTiming.CurTime) else if (_gameTicker.StartTime < _gameTiming.CurTime)
{ {
_lobby!.StartTime.Text = Loc.GetString("lobby-state-soon"); Lobby!.StartTime.Text = Loc.GetString("lobby-state-soon");
return; return;
} }
else else
@@ -170,7 +142,7 @@ namespace Content.Client.Lobby
} }
} }
_lobby!.StartTime.Text = Loc.GetString("lobby-state-round-start-countdown-text", ("timeLeft", text)); Lobby!.StartTime.Text = Loc.GetString("lobby-state-round-start-countdown-text", ("timeLeft", text));
} }
private void LobbyStatusUpdated() private void LobbyStatusUpdated()
@@ -181,31 +153,31 @@ namespace Content.Client.Lobby
private void LobbyLateJoinStatusUpdated() private void LobbyLateJoinStatusUpdated()
{ {
_lobby!.ReadyButton.Disabled = _gameTicker.DisallowedLateJoin; Lobby!.ReadyButton.Disabled = _gameTicker.DisallowedLateJoin;
} }
private void UpdateLobbyUi() private void UpdateLobbyUi()
{ {
if (_gameTicker.IsGameStarted) if (_gameTicker.IsGameStarted)
{ {
_lobby!.ReadyButton.Text = Loc.GetString("lobby-state-ready-button-join-state"); Lobby!.ReadyButton.Text = Loc.GetString("lobby-state-ready-button-join-state");
_lobby!.ReadyButton.ToggleMode = false; Lobby!.ReadyButton.ToggleMode = false;
_lobby!.ReadyButton.Pressed = false; Lobby!.ReadyButton.Pressed = false;
_lobby!.ObserveButton.Disabled = false; Lobby!.ObserveButton.Disabled = false;
} }
else else
{ {
_lobby!.StartTime.Text = string.Empty; Lobby!.StartTime.Text = string.Empty;
_lobby!.ReadyButton.Text = Loc.GetString(_lobby!.ReadyButton.Pressed ? "lobby-state-player-status-ready": "lobby-state-player-status-not-ready"); Lobby!.ReadyButton.Text = Loc.GetString(Lobby!.ReadyButton.Pressed ? "lobby-state-player-status-ready": "lobby-state-player-status-not-ready");
_lobby!.ReadyButton.ToggleMode = true; Lobby!.ReadyButton.ToggleMode = true;
_lobby!.ReadyButton.Disabled = false; Lobby!.ReadyButton.Disabled = false;
_lobby!.ReadyButton.Pressed = _gameTicker.AreWeReady; Lobby!.ReadyButton.Pressed = _gameTicker.AreWeReady;
_lobby!.ObserveButton.Disabled = true; Lobby!.ObserveButton.Disabled = true;
} }
if (_gameTicker.ServerInfoBlob != null) if (_gameTicker.ServerInfoBlob != null)
{ {
_lobby!.ServerInfo.SetInfoBlob(_gameTicker.ServerInfoBlob); Lobby!.ServerInfo.SetInfoBlob(_gameTicker.ServerInfoBlob);
} }
} }
@@ -213,7 +185,7 @@ namespace Content.Client.Lobby
{ {
if (ev.SoundtrackFilename == null) if (ev.SoundtrackFilename == null)
{ {
_lobby!.LobbySong.SetMarkup(Loc.GetString("lobby-state-song-no-song-text")); Lobby!.LobbySong.SetMarkup(Loc.GetString("lobby-state-song-no-song-text"));
} }
else if ( else if (
ev.SoundtrackFilename != null ev.SoundtrackFilename != null
@@ -234,7 +206,7 @@ namespace Content.Client.Lobby
("songTitle", title), ("songTitle", title),
("songArtist", artist)); ("songArtist", artist));
_lobby!.LobbySong.SetMarkup(markup); Lobby!.LobbySong.SetMarkup(markup);
} }
} }
@@ -242,11 +214,11 @@ namespace Content.Client.Lobby
{ {
if (_gameTicker.LobbyBackground != null) if (_gameTicker.LobbyBackground != null)
{ {
_lobby!.Background.Texture = _resourceCache.GetResource<TextureResource>(_gameTicker.LobbyBackground ); Lobby!.Background.Texture = _resourceCache.GetResource<TextureResource>(_gameTicker.LobbyBackground );
} }
else else
{ {
_lobby!.Background.Texture = null; Lobby!.Background.Texture = null;
} }
} }

View File

@@ -2,190 +2,292 @@ using System.Linq;
using Content.Client.Humanoid; using Content.Client.Humanoid;
using Content.Client.Inventory; using Content.Client.Inventory;
using Content.Client.Lobby.UI; using Content.Client.Lobby.UI;
using Content.Client.Preferences; using Content.Client.Players.PlayTimeTracking;
using Content.Client.Preferences.UI;
using Content.Client.Station; using Content.Client.Station;
using Content.Shared.CCVar;
using Content.Shared.Clothing; using Content.Shared.Clothing;
using Content.Shared.GameTicking; using Content.Shared.GameTicking;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Markings;
using Content.Shared.Humanoid.Prototypes; using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences; using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts; using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
using Content.Shared.Roles; using Content.Shared.Roles;
using Content.Shared.Traits;
using Robust.Client.Player;
using Robust.Client.ResourceManagement;
using Robust.Client.State; using Robust.Client.State;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers; using Robust.Client.UserInterface.Controllers;
using Robust.Shared.Configuration;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Utility;
namespace Content.Client.Lobby; namespace Content.Client.Lobby;
public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState>, IOnStateExited<LobbyState> public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState>, IOnStateExited<LobbyState>
{ {
[Dependency] private readonly IClientPreferencesManager _preferencesManager = default!; [Dependency] private readonly IClientPreferencesManager _preferencesManager = default!;
[Dependency] private readonly IStateManager _stateManager = default!; [Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IFileDialogManager _dialogManager = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IStateManager _stateManager = default!;
[Dependency] private readonly JobRequirementsManager _requirements = default!;
[Dependency] private readonly MarkingManager _markings = default!;
[UISystemDependency] private readonly HumanoidAppearanceSystem _humanoid = default!; [UISystemDependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
[UISystemDependency] private readonly ClientInventorySystem _inventory = default!; [UISystemDependency] private readonly ClientInventorySystem _inventory = default!;
[UISystemDependency] private readonly StationSpawningSystem _spawn = default!; [UISystemDependency] private readonly StationSpawningSystem _spawn = default!;
private LobbyCharacterPreviewPanel? _previewPanel; private CharacterSetupGui? _characterSetup;
private HumanoidProfileEditor? _profileEditor;
private bool _showClothes = true;
/*
* Each character profile has its own dummy. There is also a dummy for the lobby screen + character editor
* that is shared too.
*/
/// <summary> /// <summary>
/// Preview dummy for role gear. /// This is the characher preview panel in the chat. This should only update if their character updates.
/// </summary> /// </summary>
private EntityUid? _previewDummy; private LobbyCharacterPreviewPanel? PreviewPanel => GetLobbyPreview();
/// <summary> /// <summary>
/// If we currently have a job prototype selected. /// This is the modified profile currently being edited.
/// </summary> /// </summary>
private JobPrototype? _dummyJob; private HumanoidCharacterProfile? EditedProfile => _profileEditor?.Profile;
// TODO: Load the species directly and don't update entity ever. private int? EditedSlot => _profileEditor?.CharacterSlot;
public event Action<EntityUid>? PreviewDummyUpdated;
private HumanoidCharacterProfile? _profile;
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
_prototypeManager.PrototypesReloaded += OnProtoReload;
_preferencesManager.OnServerDataLoaded += PreferencesDataLoaded; _preferencesManager.OnServerDataLoaded += PreferencesDataLoaded;
_requirements.Updated += OnRequirementsUpdated;
_configurationManager.OnValueChanged(CCVars.FlavorText, args =>
{
_profileEditor?.RefreshFlavorText();
});
_configurationManager.OnValueChanged(CCVars.GameRoleTimers, args =>
{
_profileEditor?.RefreshAntags();
_profileEditor?.RefreshJobs();
_profileEditor?.RefreshLoadouts();
});
}
private LobbyCharacterPreviewPanel? GetLobbyPreview()
{
if (_stateManager.CurrentState is LobbyState lobby)
{
return lobby.Lobby?.CharacterPreview;
}
return null;
}
private void OnRequirementsUpdated()
{
if (_profileEditor != null)
{
_profileEditor.RefreshAntags();
_profileEditor.RefreshJobs();
}
}
private void OnProtoReload(PrototypesReloadedEventArgs obj)
{
if (_profileEditor != null)
{
if (obj.WasModified<AntagPrototype>())
{
_profileEditor.RefreshAntags();
}
if (obj.WasModified<JobPrototype>() ||
obj.WasModified<DepartmentPrototype>())
{
_profileEditor.RefreshJobs();
}
if (obj.WasModified<LoadoutPrototype>() ||
obj.WasModified<LoadoutGroupPrototype>() ||
obj.WasModified<RoleLoadoutPrototype>())
{
_profileEditor.RefreshLoadouts();
}
if (obj.WasModified<SpeciesPrototype>())
{
_profileEditor.RefreshSpecies();
}
if (obj.WasModified<TraitPrototype>())
{
_profileEditor.RefreshTraits();
}
}
} }
private void PreferencesDataLoaded() private void PreferencesDataLoaded()
{ {
UpdateProfile(); PreviewPanel?.SetLoaded(true);
}
public void OnStateEntered(LobbyState state)
{
}
public void OnStateExited(LobbyState state)
{
EntityManager.DeleteEntity(_previewDummy);
_previewDummy = null;
}
public void SetPreviewPanel(LobbyCharacterPreviewPanel? panel)
{
_previewPanel = panel;
ReloadProfile();
}
public void SetClothes(bool value)
{
if (_showClothes == value)
return;
_showClothes = value;
ReloadCharacterUI();
}
public void SetDummyJob(JobPrototype? job)
{
_dummyJob = job;
ReloadCharacterUI();
}
/// <summary>
/// Updates the character only with the specified profile change.
/// </summary>
public void ReloadProfile()
{
// Test moment
if (_profile == null || _stateManager.CurrentState is not LobbyState)
return;
// Ignore job clothes and the likes so we don't spam entities out every frame of color changes.
var previewDummy = EnsurePreviewDummy(_profile);
_humanoid.LoadProfile(previewDummy, _profile);
}
/// <summary>
/// Updates the currently selected character's preview.
/// </summary>
public void ReloadCharacterUI()
{
// Test moment
if (_profile == null || _stateManager.CurrentState is not LobbyState)
return;
EntityManager.DeleteEntity(_previewDummy);
_previewDummy = null;
_previewDummy = EnsurePreviewDummy(_profile);
_previewPanel?.SetSprite(_previewDummy.Value);
_previewPanel?.SetSummaryText(_profile.Summary);
_humanoid.LoadProfile(_previewDummy.Value, _profile);
if (_showClothes)
GiveDummyJobClothesLoadout(_previewDummy.Value, _profile);
}
/// <summary>
/// Updates character profile to the default.
/// </summary>
public void UpdateProfile()
{
if (!_preferencesManager.ServerDataLoaded)
{
_profile = null;
return;
}
if (_preferencesManager.Preferences?.SelectedCharacter is HumanoidCharacterProfile selectedCharacter)
{
_profile = selectedCharacter;
_previewPanel?.SetLoaded(true);
}
else
{
_previewPanel?.SetSummaryText(string.Empty);
_previewPanel?.SetLoaded(false);
}
ReloadCharacterUI();
}
public void UpdateProfile(HumanoidCharacterProfile? profile)
{
if (_profile?.Equals(profile) == true)
return;
if (_stateManager.CurrentState is not LobbyState) if (_stateManager.CurrentState is not LobbyState)
return; return;
_profile = profile; ReloadCharacterSetup();
} }
private EntityUid EnsurePreviewDummy(HumanoidCharacterProfile profile) public void OnStateEntered(LobbyState state)
{ {
if (_previewDummy != null) PreviewPanel?.SetLoaded(_preferencesManager.ServerDataLoaded);
return _previewDummy.Value; ReloadCharacterSetup();
_previewDummy = EntityManager.SpawnEntity(_prototypeManager.Index<SpeciesPrototype>(profile.Species).DollPrototype, MapCoordinates.Nullspace);
PreviewDummyUpdated?.Invoke(_previewDummy.Value);
return _previewDummy.Value;
} }
public void OnStateExited(LobbyState state)
{
PreviewPanel?.SetLoaded(false);
_profileEditor?.Dispose();
_characterSetup?.Dispose();
_characterSetup = null;
_profileEditor = null;
}
/// <summary>
/// Reloads every single character setup control.
/// </summary>
public void ReloadCharacterSetup()
{
RefreshLobbyPreview();
var (characterGui, profileEditor) = EnsureGui();
characterGui.ReloadCharacterPickers();
profileEditor.SetProfile(
(HumanoidCharacterProfile?) _preferencesManager.Preferences?.SelectedCharacter,
_preferencesManager.Preferences?.SelectedCharacterIndex);
}
/// <summary>
/// Refreshes the character preview in the lobby chat.
/// </summary>
private void RefreshLobbyPreview()
{
if (PreviewPanel == null)
return;
// Get selected character, load it, then set it
var character = _preferencesManager.Preferences?.SelectedCharacter;
if (character is not HumanoidCharacterProfile humanoid)
{
PreviewPanel.SetSprite(EntityUid.Invalid);
PreviewPanel.SetSummaryText(string.Empty);
return;
}
var dummy = LoadProfileEntity(humanoid, null, true);
PreviewPanel.SetSprite(dummy);
PreviewPanel.SetSummaryText(humanoid.Summary);
}
private void SaveProfile()
{
DebugTools.Assert(EditedProfile != null);
if (EditedProfile == null || EditedSlot == null)
return;
var selected = _preferencesManager.Preferences?.SelectedCharacterIndex;
if (selected == null)
return;
_preferencesManager.UpdateCharacter(EditedProfile, EditedSlot.Value);
ReloadCharacterSetup();
}
private (CharacterSetupGui, HumanoidProfileEditor) EnsureGui()
{
if (_characterSetup != null && _profileEditor != null)
{
_characterSetup.Visible = true;
_profileEditor.Visible = true;
return (_characterSetup, _profileEditor);
}
_profileEditor = new HumanoidProfileEditor(
_preferencesManager,
_configurationManager,
EntityManager,
_dialogManager,
_logManager,
_playerManager,
_prototypeManager,
_requirements,
_markings);
_characterSetup = new CharacterSetupGui(EntityManager, _prototypeManager, _resourceCache, _preferencesManager, _profileEditor);
_characterSetup.CloseButton.OnPressed += _ =>
{
// Reset sliders etc.
_profileEditor.SetProfile(null, null);
_profileEditor.Visible = false;
if (_stateManager.CurrentState is LobbyState lobbyGui)
{
lobbyGui.SwitchState(LobbyGui.LobbyGuiState.Default);
}
};
_profileEditor.Save += SaveProfile;
_characterSetup.SelectCharacter += args =>
{
_preferencesManager.SelectCharacter(args);
ReloadCharacterSetup();
};
_characterSetup.DeleteCharacter += args =>
{
_preferencesManager.DeleteCharacter(args);
// Reload everything
if (EditedSlot == args)
{
ReloadCharacterSetup();
}
else
{
// Only need to reload character pickers
_characterSetup?.ReloadCharacterPickers();
}
};
if (_stateManager.CurrentState is LobbyState lobby)
{
lobby.Lobby?.CharacterSetupState.AddChild(_characterSetup);
}
return (_characterSetup, _profileEditor);
}
#region Helpers
/// <summary> /// <summary>
/// Applies the highest priority job's clothes to the dummy. /// Applies the highest priority job's clothes to the dummy.
/// </summary> /// </summary>
public void GiveDummyJobClothesLoadout(EntityUid dummy, HumanoidCharacterProfile profile) public void GiveDummyJobClothesLoadout(EntityUid dummy, JobPrototype? jobProto, HumanoidCharacterProfile profile)
{ {
var job = _dummyJob ?? GetPreferredJob(profile); var job = jobProto ?? GetPreferredJob(profile);
GiveDummyJobClothes(dummy, profile, job); GiveDummyJobClothes(dummy, profile, job);
if (_prototypeManager.HasIndex<RoleLoadoutPrototype>(LoadoutSystem.GetJobPrototype(job.ID))) if (_prototypeManager.HasIndex<RoleLoadoutPrototype>(LoadoutSystem.GetJobPrototype(job.ID)))
{ {
var loadout = profile.GetLoadoutOrDefault(LoadoutSystem.GetJobPrototype(job.ID), EntityManager, _prototypeManager); var loadout = profile.GetLoadoutOrDefault(LoadoutSystem.GetJobPrototype(job.ID), profile.Species, EntityManager, _prototypeManager);
GiveDummyLoadout(dummy, loadout); GiveDummyLoadout(dummy, loadout);
} }
} }
@@ -279,8 +381,39 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
} }
} }
public EntityUid? GetPreviewDummy() /// <summary>
/// Loads the profile onto a dummy entity.
/// </summary>
public EntityUid LoadProfileEntity(HumanoidCharacterProfile? humanoid, JobPrototype? job, bool jobClothes)
{ {
return _previewDummy; EntityUid dummyEnt;
if (humanoid is not null)
{
var dummy = _prototypeManager.Index<SpeciesPrototype>(humanoid.Species).DollPrototype;
dummyEnt = EntityManager.SpawnEntity(dummy, MapCoordinates.Nullspace);
}
else
{
dummyEnt = EntityManager.SpawnEntity(_prototypeManager.Index<SpeciesPrototype>(SharedHumanoidAppearanceSystem.DefaultSpecies).DollPrototype, MapCoordinates.Nullspace);
}
_humanoid.LoadProfile(dummyEnt, humanoid);
if (humanoid != null && jobClothes)
{
job ??= GetPreferredJob(humanoid);
GiveDummyJobClothes(dummyEnt, humanoid, job);
if (_prototypeManager.HasIndex<RoleLoadoutPrototype>(LoadoutSystem.GetJobPrototype(job.ID)))
{
var loadout = humanoid.GetLoadoutOrDefault(LoadoutSystem.GetJobPrototype(job.ID), humanoid.Species, EntityManager, _prototypeManager);
GiveDummyLoadout(dummyEnt, loadout);
}
}
return dummyEnt;
} }
#endregion
} }

View File

@@ -0,0 +1,22 @@
<ContainerButton xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:style="clr-namespace:Content.Client.Stylesheets">
<BoxContainer Orientation="Horizontal"
HorizontalExpand="True"
SeparationOverride="0"
Name="InternalHBox">
<SpriteView Scale="2 2"
OverrideDirection="South"
Name="View"/>
<Label Name="DescriptionLabel"
ClipText="True"
HorizontalExpand="True"/>
<Button Name="DeleteButton"
Text="{Loc 'character-setup-gui-character-picker-button-delete-button'}"/>
<Button Name="ConfirmDeleteButton"
Text="{Loc 'character-setup-gui-character-picker-button-confirm-delete-button'}"
Visible="False"
ModulateSelfOverride="{x:Static style:StyleNano.ButtonColorCautionDefault}"/>
</BoxContainer>
</ContainerButton>

View File

@@ -0,0 +1,92 @@
using System.Linq;
using Content.Client.Humanoid;
using Content.Shared.Clothing;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.Client.Lobby.UI;
/// <summary>
/// Holds character data on the side of the setup GUI.
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class CharacterPickerButton : ContainerButton
{
private IEntityManager _entManager;
private EntityUid _previewDummy;
/// <summary>
/// Invoked if we should delete the attached character
/// </summary>
public event Action? OnDeletePressed;
public CharacterPickerButton(
IEntityManager entityManager,
IPrototypeManager prototypeManager,
ButtonGroup group,
ICharacterProfile profile,
bool isSelected)
{
RobustXamlLoader.Load(this);
_entManager = entityManager;
AddStyleClass(StyleClassButton);
ToggleMode = true;
Group = group;
var description = profile.Name;
if (profile is not HumanoidCharacterProfile humanoid)
{
_previewDummy = entityManager.SpawnEntity(prototypeManager.Index<SpeciesPrototype>(SharedHumanoidAppearanceSystem.DefaultSpecies).DollPrototype, MapCoordinates.Nullspace);
}
else
{
_previewDummy = UserInterfaceManager.GetUIController<LobbyUIController>()
.LoadProfileEntity(humanoid, null, true);
var highPriorityJob = humanoid.JobPriorities.SingleOrDefault(p => p.Value == JobPriority.High).Key;
if (highPriorityJob != null)
{
var jobName = prototypeManager.Index<JobPrototype>(highPriorityJob).LocalizedName;
description = $"{description}\n{jobName}";
}
}
Pressed = isSelected;
DeleteButton.Visible = !isSelected;
View.SetEntity(_previewDummy);
DescriptionLabel.Text = description;
ConfirmDeleteButton.OnPressed += _ =>
{
Parent?.RemoveChild(this);
Parent?.RemoveChild(ConfirmDeleteButton);
OnDeletePressed?.Invoke();
};
DeleteButton.OnPressed += _ =>
{
DeleteButton.Visible = false;
ConfirmDeleteButton.Visible = true;
};
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
_entManager.DeleteEntity(_previewDummy);
_previewDummy = default;
}
}

View File

@@ -17,10 +17,6 @@
<Button Name="RulesButton" <Button Name="RulesButton"
Text="{Loc 'character-setup-gui-character-setup-rules-button'}" Text="{Loc 'character-setup-gui-character-setup-rules-button'}"
StyleClasses="ButtonBig"/> StyleClasses="ButtonBig"/>
<Button Name="SaveButton"
Access="Public"
Text="{Loc 'character-setup-gui-character-setup-save-button'}"
StyleClasses="ButtonBig"/>
<Button Name="CloseButton" <Button Name="CloseButton"
Access="Public" Access="Public"
Text="{Loc 'character-setup-gui-character-setup-close-button'}" Text="{Loc 'character-setup-gui-character-setup-close-button'}"

View File

@@ -0,0 +1,118 @@
using Content.Client.Info;
using Content.Client.Info.PlaytimeStats;
using Content.Client.Resources;
using Content.Shared.Preferences;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
namespace Content.Client.Lobby.UI
{
/// <summary>
/// Holds the entire character setup GUI, from character picks to individual character editing.
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class CharacterSetupGui : Control
{
private readonly IClientPreferencesManager _preferencesManager;
private readonly IEntityManager _entManager;
private readonly IPrototypeManager _protomanager;
private readonly Button _createNewCharacterButton;
public event Action<int>? SelectCharacter;
public event Action<int>? DeleteCharacter;
public CharacterSetupGui(
IEntityManager entManager,
IPrototypeManager protoManager,
IResourceCache resourceCache,
IClientPreferencesManager preferencesManager,
HumanoidProfileEditor profileEditor)
{
RobustXamlLoader.Load(this);
_preferencesManager = preferencesManager;
_entManager = entManager;
_protomanager = protoManager;
var panelTex = resourceCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png");
var back = new StyleBoxTexture
{
Texture = panelTex,
Modulate = new Color(37, 37, 42)
};
back.SetPatchMargin(StyleBox.Margin.All, 10);
BackgroundPanel.PanelOverride = back;
_createNewCharacterButton = new Button
{
Text = Loc.GetString("character-setup-gui-create-new-character-button"),
};
_createNewCharacterButton.OnPressed += args =>
{
preferencesManager.CreateCharacter(HumanoidCharacterProfile.Random());
ReloadCharacterPickers();
args.Event.Handle();
};
CharEditor.AddChild(profileEditor);
RulesButton.OnPressed += _ => new RulesAndInfoWindow().Open();
StatsButton.OnPressed += _ => new PlaytimeStatsWindow().OpenCentered();
}
/// <summary>
/// Disposes and reloads all character picker buttons from the preferences data.
/// </summary>
public void ReloadCharacterPickers()
{
_createNewCharacterButton.Orphan();
Characters.DisposeAllChildren();
var numberOfFullSlots = 0;
var characterButtonsGroup = new ButtonGroup();
if (!_preferencesManager.ServerDataLoaded)
{
return;
}
_createNewCharacterButton.ToolTip =
Loc.GetString("character-setup-gui-create-new-character-button-tooltip",
("maxCharacters", _preferencesManager.Settings!.MaxCharacterSlots));
var selectedSlot = _preferencesManager.Preferences?.SelectedCharacterIndex;
foreach (var (slot, character) in _preferencesManager.Preferences!.Characters)
{
numberOfFullSlots++;
var characterPickerButton = new CharacterPickerButton(_entManager,
_protomanager,
characterButtonsGroup,
character,
slot == selectedSlot);
Characters.AddChild(characterPickerButton);
characterPickerButton.OnPressed += args =>
{
SelectCharacter?.Invoke(slot);
};
characterPickerButton.OnDeletePressed += () =>
{
DeleteCharacter?.Invoke(slot);
};
}
_createNewCharacterButton.Disabled = numberOfFullSlots >= _preferencesManager.Settings.MaxCharacterSlots;
Characters.AddChild(_createNewCharacterButton);
}
}
}

View File

@@ -2,7 +2,7 @@ using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML; using Robust.Client.UserInterface.XAML;
namespace Content.Client.Preferences.UI; namespace Content.Client.Lobby.UI;
[GenerateTypedNameReferences] [GenerateTypedNameReferences]
public sealed partial class HighlightedContainer : PanelContainer public sealed partial class HighlightedContainer : PanelContainer

View File

@@ -1,8 +1,8 @@
<BoxContainer xmlns="https://spacestation14.io" <BoxContainer xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prefUi="clr-namespace:Content.Client.Preferences.UI"
xmlns:humanoid="clr-namespace:Content.Client.Humanoid" xmlns:humanoid="clr-namespace:Content.Client.Humanoid"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls" xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:ui="clr-namespace:Content.Client.Lobby.UI"
HorizontalExpand="True"> HorizontalExpand="True">
<!-- Left side --> <!-- Left side -->
<BoxContainer Orientation="Vertical" Margin="10 10 10 10" HorizontalExpand="True"> <BoxContainer Orientation="Vertical" Margin="10 10 10 10" HorizontalExpand="True">
@@ -10,45 +10,41 @@
<BoxContainer Orientation="Horizontal" SeparationOverride="10"> <BoxContainer Orientation="Horizontal" SeparationOverride="10">
<!-- Name box--> <!-- Name box-->
<BoxContainer Orientation="Vertical"> <BoxContainer Orientation="Vertical">
<prefUi:HighlightedContainer> <ui:HighlightedContainer>
<BoxContainer Orientation="Vertical"> <BoxContainer Orientation="Vertical">
<prefUi:HighlightedContainer> <ui:HighlightedContainer>
<BoxContainer Orientation="Vertical"> <BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal" VerticalExpand="True"> <BoxContainer Orientation="Horizontal" VerticalExpand="True">
<Label Text="{Loc 'humanoid-profile-editor-name-label'}" /> <Label Text="{Loc 'humanoid-profile-editor-name-label'}" />
<LineEdit Name="CNameEdit" MinSize="270 0" VerticalAlignment="Center" Margin="5 0 0 0" /> <LineEdit Name="NameEdit" MinSize="270 0" VerticalAlignment="Center" Margin="5 0 0 0" />
<Button Name="CNameRandomize" Text="{Loc 'humanoid-profile-editor-name-random-button'}" /> <Button Name="NameRandomize" Text="{Loc 'humanoid-profile-editor-name-random-button'}" />
</BoxContainer> </BoxContainer>
<Button Name="CRandomizeEverything" HorizontalAlignment="Center" <Button Name="RandomizeEverythingButton" HorizontalAlignment="Center"
HorizontalExpand="False" MaxWidth="256" HorizontalExpand="False" MaxWidth="256"
Text="{Loc 'humanoid-profile-editor-randomize-everything-button'}" /> Text="{Loc 'humanoid-profile-editor-randomize-everything-button'}" />
<RichTextLabel Name="CWarningLabel" HorizontalExpand="False" <RichTextLabel Name="WarningLabel" HorizontalExpand="False"
VerticalExpand="True" MaxWidth="425" VerticalExpand="True" MaxWidth="425"
HorizontalAlignment="Left" /> HorizontalAlignment="Left" />
</BoxContainer> </BoxContainer>
</prefUi:HighlightedContainer> </ui:HighlightedContainer>
</BoxContainer> </BoxContainer>
</prefUi:HighlightedContainer> </ui:HighlightedContainer>
</BoxContainer> </BoxContainer>
<!-- Import/Export --> <!-- Import/Export -->
<BoxContainer Orientation="Vertical"> <BoxContainer Orientation="Vertical">
<prefUi:HighlightedContainer> <ui:HighlightedContainer>
<BoxContainer Orientation="Horizontal"> <BoxContainer Orientation="Horizontal" SeparationOverride="5">
<Button Text="{Loc 'humanoid-profile-editor-import-button'}" Disabled="True" <Button Name="ImportButton" Text="{Loc 'humanoid-profile-editor-import-button'}"/>
ToolTip="{Loc 'generic-not-yet-implemented'}" /> <Button Name="ExportButton" Text="{Loc 'humanoid-profile-editor-export-button'}"/>
<Button Text="{Loc 'humanoid-profile-editor-export-button'}" Disabled="True" <Button Name="SaveButton" Text="{Loc 'humanoid-profile-editor-save-button'}" />
ToolTip="{Loc 'generic-not-yet-implemented'}" /> <Button Name="ResetButton" Disabled="True" Text="{Loc 'humanoid-profile-editor-reset-button'}"/>
</BoxContainer> </BoxContainer>
</prefUi:HighlightedContainer> </ui:HighlightedContainer>
<!-- Save -->
<prefUi:HighlightedContainer>
<Button Name="CSaveButton" Text="{Loc 'humanoid-profile-editor-save-button'}" HorizontalAlignment="Center" />
</prefUi:HighlightedContainer>
</BoxContainer> </BoxContainer>
</BoxContainer> </BoxContainer>
<Control MinHeight="10" /> <Control MinHeight="10" />
<!-- tabContainer --> <!-- tabContainer -->
<TabContainer Name="CTabContainer" VerticalExpand="True"> <TabContainer Name="TabContainer" VerticalExpand="True">
<BoxContainer Orientation="Vertical"> <BoxContainer Orientation="Vertical">
<ScrollContainer VerticalExpand="True"> <ScrollContainer VerticalExpand="True">
<!-- appearanceList --> <!-- appearanceList -->
@@ -61,25 +57,25 @@
<TextureButton Name="SpeciesInfoButton" Scale="0.3 0.3" <TextureButton Name="SpeciesInfoButton" Scale="0.3 0.3"
VerticalAlignment="Center" VerticalAlignment="Center"
ToolTip="{Loc 'humanoid-profile-editor-guidebook-button-tooltip'}"/> ToolTip="{Loc 'humanoid-profile-editor-guidebook-button-tooltip'}"/>
<OptionButton Name="CSpeciesButton" HorizontalAlignment="Right" /> <OptionButton Name="SpeciesButton" HorizontalAlignment="Right" />
</BoxContainer> </BoxContainer>
<!-- Age --> <!-- Age -->
<BoxContainer HorizontalExpand="True"> <BoxContainer HorizontalExpand="True">
<Label Text="{Loc 'humanoid-profile-editor-age-label'}" /> <Label Text="{Loc 'humanoid-profile-editor-age-label'}" />
<Control HorizontalExpand="True"/> <Control HorizontalExpand="True"/>
<LineEdit Name="CAgeEdit" MinSize="40 0" HorizontalAlignment="Right" /> <LineEdit Name="AgeEdit" MinSize="40 0" HorizontalAlignment="Right" />
</BoxContainer> </BoxContainer>
<!-- Sex --> <!-- Sex -->
<BoxContainer HorizontalExpand="True"> <BoxContainer HorizontalExpand="True">
<Label Text="{Loc 'humanoid-profile-editor-sex-label'}" /> <Label Text="{Loc 'humanoid-profile-editor-sex-label'}" />
<Control HorizontalExpand="True"/> <Control HorizontalExpand="True"/>
<OptionButton Name="CSexButton" HorizontalAlignment="Right" /> <OptionButton Name="SexButton" HorizontalAlignment="Right" />
</BoxContainer> </BoxContainer>
<!-- Pronouns --> <!-- Pronouns -->
<BoxContainer HorizontalExpand="True"> <BoxContainer HorizontalExpand="True">
<Label Text="{Loc 'humanoid-profile-editor-pronouns-label'}" /> <Label Text="{Loc 'humanoid-profile-editor-pronouns-label'}" />
<Control HorizontalExpand="True"/> <Control HorizontalExpand="True"/>
<OptionButton Name="CPronounsButton" HorizontalAlignment="Right" /> <OptionButton Name="PronounsButton" HorizontalAlignment="Right" />
</BoxContainer> </BoxContainer>
<!-- Show clothing --> <!-- Show clothing -->
<BoxContainer HorizontalExpand="True"> <BoxContainer HorizontalExpand="True">
@@ -91,62 +87,62 @@
<BoxContainer HorizontalExpand="True"> <BoxContainer HorizontalExpand="True">
<Label Text="{Loc 'humanoid-profile-editor-spawn-priority-label'}" /> <Label Text="{Loc 'humanoid-profile-editor-spawn-priority-label'}" />
<Control HorizontalExpand="True"/> <Control HorizontalExpand="True"/>
<OptionButton Name="CSpawnPriorityButton" HorizontalAlignment="Right" /> <OptionButton Name="SpawnPriorityButton" HorizontalAlignment="Right" />
</BoxContainer> </BoxContainer>
</BoxContainer> </BoxContainer>
<!-- Skin --> <!-- Skin -->
<BoxContainer Margin="10" HorizontalExpand="True" Orientation="Vertical"> <BoxContainer Margin="10" HorizontalExpand="True" Orientation="Vertical">
<Label Text="{Loc 'humanoid-profile-editor-skin-color-label'}" /> <Label Text="{Loc 'humanoid-profile-editor-skin-color-label'}" />
<Slider HorizontalExpand="True" Name="CSkin" MinValue="0" MaxValue="100" Value="20" /> <Slider HorizontalExpand="True" Name="Skin" MinValue="0" MaxValue="100" Value="20" />
<BoxContainer Name="CRgbSkinColorContainer" Visible="False" Orientation="Vertical" HorizontalExpand="True"></BoxContainer> <BoxContainer Name="RgbSkinColorContainer" Visible="False" Orientation="Vertical" HorizontalExpand="True"></BoxContainer>
</BoxContainer> </BoxContainer>
<!-- Hair --> <!-- Hair -->
<BoxContainer Margin="10" Orientation="Horizontal"> <BoxContainer Margin="10" Orientation="Horizontal">
<humanoid:SingleMarkingPicker Name="CHairStylePicker" Category="Hair" /> <humanoid:SingleMarkingPicker Name="HairStylePicker" Category="Hair" />
<humanoid:SingleMarkingPicker Name="CFacialHairPicker" Category="FacialHair" /> <humanoid:SingleMarkingPicker Name="FacialHairPicker" Category="FacialHair" />
</BoxContainer> </BoxContainer>
<!-- Eyes --> <!-- Eyes -->
<BoxContainer Margin="10" Orientation="Vertical"> <BoxContainer Margin="10" Orientation="Vertical">
<Label Text="{Loc 'humanoid-profile-editor-eyes-label'}" /> <Label Text="{Loc 'humanoid-profile-editor-eyes-label'}" />
<humanoid:EyeColorPicker Name="CEyeColorPicker" /> <humanoid:EyeColorPicker Name="EyeColorPicker" />
</BoxContainer> </BoxContainer>
</BoxContainer> </BoxContainer>
</ScrollContainer> </ScrollContainer>
</BoxContainer> </BoxContainer>
<BoxContainer Orientation="Vertical"> <BoxContainer Orientation="Vertical">
<!-- Jobs --> <!-- Jobs -->
<OptionButton Name="CPreferenceUnavailableButton" /> <OptionButton Name="PreferenceUnavailableButton" />
<ScrollContainer VerticalExpand="True"> <ScrollContainer VerticalExpand="True">
<BoxContainer Name="CJobList" Orientation="Vertical" /> <BoxContainer Name="JobList" Orientation="Vertical" />
</ScrollContainer> </ScrollContainer>
</BoxContainer> </BoxContainer>
<BoxContainer Orientation="Vertical" Margin="10"> <BoxContainer Orientation="Vertical" Margin="10">
<!-- Antags --> <!-- Antags -->
<ScrollContainer VerticalExpand="True"> <ScrollContainer VerticalExpand="True">
<BoxContainer Name="CAntagList" Orientation="Vertical" /> <BoxContainer Name="AntagList" Orientation="Vertical" />
</ScrollContainer> </ScrollContainer>
</BoxContainer> </BoxContainer>
<BoxContainer Orientation="Vertical" Margin="10"> <BoxContainer Orientation="Vertical" Margin="10">
<!-- Traits --> <!-- Traits -->
<ScrollContainer VerticalExpand="True"> <ScrollContainer VerticalExpand="True">
<BoxContainer Name="CTraitsList" Orientation="Vertical" /> <BoxContainer Name="TraitsList" Orientation="Vertical" />
</ScrollContainer> </ScrollContainer>
</BoxContainer> </BoxContainer>
<BoxContainer Name="CMarkingsTab" Orientation="Vertical" Margin="10"> <BoxContainer Name="MarkingsTab" Orientation="Vertical" Margin="10">
<!-- Markings --> <!-- Markings -->
<ScrollContainer VerticalExpand="True"> <ScrollContainer VerticalExpand="True">
<humanoid:MarkingPicker Name="CMarkings" IgnoreCategories="Hair,FacialHair" /> <humanoid:MarkingPicker Name="Markings" IgnoreCategories="Hair,FacialHair" />
</ScrollContainer> </ScrollContainer>
</BoxContainer> </BoxContainer>
</TabContainer> </TabContainer>
</BoxContainer> </BoxContainer>
<!-- Right side --> <!-- Right side -->
<BoxContainer Orientation="Vertical" VerticalExpand="True" VerticalAlignment="Center"> <BoxContainer Orientation="Vertical" VerticalExpand="True" VerticalAlignment="Center">
<SpriteView Name="CSpriteView" Scale="8 8" SizeFlagsStretchRatio="1" /> <SpriteView Name="SpriteView" Scale="8 8" SizeFlagsStretchRatio="1" />
<BoxContainer Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 5"> <BoxContainer Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 5">
<Button Name="CSpriteRotateLeft" Text="◀" StyleClasses="OpenRight" /> <Button Name="SpriteRotateLeft" Text="◀" StyleClasses="OpenRight" />
<cc:VSeparator Margin="2 0 3 0" /> <cc:VSeparator Margin="2 0 3 0" />
<Button Name="CSpriteRotateRight" Text="▶" StyleClasses="OpenLeft" /> <Button Name="SpriteRotateRight" Text="▶" StyleClasses="OpenLeft" />
</BoxContainer> </BoxContainer>
</BoxContainer> </BoxContainer>
</BoxContainer> </BoxContainer>

View File

@@ -8,7 +8,7 @@ using Robust.Shared.Map;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Utility; using Robust.Shared.Utility;
namespace Content.Client.Preferences.UI; namespace Content.Client.Lobby.UI.Loadouts;
[GenerateTypedNameReferences] [GenerateTypedNameReferences]
public sealed partial class LoadoutContainer : BoxContainer public sealed partial class LoadoutContainer : BoxContainer
@@ -45,7 +45,7 @@ public sealed partial class LoadoutContainer : BoxContainer
var spriteTooltip = new Tooltip(); var spriteTooltip = new Tooltip();
spriteTooltip.SetMessage(FormattedMessage.FromUnformatted(_entManager.GetComponent<MetaDataComponent>(_entity.Value).EntityDescription)); spriteTooltip.SetMessage(FormattedMessage.FromUnformatted(_entManager.GetComponent<MetaDataComponent>(_entity.Value).EntityDescription));
Sprite.TooltipSupplier = _ => spriteTooltip; TooltipSupplier = _ => spriteTooltip;
} }
} }
} }

View File

@@ -1,5 +1,6 @@
using System.Linq; using System.Linq;
using Content.Shared.Clothing; using Content.Shared.Clothing;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts; using Content.Shared.Preferences.Loadouts;
using Robust.Client.AutoGenerated; using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
@@ -7,7 +8,7 @@ using Robust.Client.UserInterface.XAML;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
namespace Content.Client.Preferences.UI; namespace Content.Client.Lobby.UI.Loadouts;
[GenerateTypedNameReferences] [GenerateTypedNameReferences]
public sealed partial class LoadoutGroupContainer : BoxContainer public sealed partial class LoadoutGroupContainer : BoxContainer
@@ -17,18 +18,18 @@ public sealed partial class LoadoutGroupContainer : BoxContainer
public event Action<ProtoId<LoadoutPrototype>>? OnLoadoutPressed; public event Action<ProtoId<LoadoutPrototype>>? OnLoadoutPressed;
public event Action<ProtoId<LoadoutPrototype>>? OnLoadoutUnpressed; public event Action<ProtoId<LoadoutPrototype>>? OnLoadoutUnpressed;
public LoadoutGroupContainer(RoleLoadout loadout, LoadoutGroupPrototype groupProto, ICommonSession session, IDependencyCollection collection) public LoadoutGroupContainer(HumanoidCharacterProfile profile, RoleLoadout loadout, LoadoutGroupPrototype groupProto, ICommonSession session, IDependencyCollection collection)
{ {
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
_groupProto = groupProto; _groupProto = groupProto;
RefreshLoadouts(loadout, session, collection); RefreshLoadouts(profile, loadout, session, collection);
} }
/// <summary> /// <summary>
/// Updates button availabilities and buttons. /// Updates button availabilities and buttons.
/// </summary> /// </summary>
public void RefreshLoadouts(RoleLoadout loadout, ICommonSession session, IDependencyCollection collection) public void RefreshLoadouts(HumanoidCharacterProfile profile, RoleLoadout loadout, ICommonSession session, IDependencyCollection collection)
{ {
var protoMan = collection.Resolve<IPrototypeManager>(); var protoMan = collection.Resolve<IPrototypeManager>();
var loadoutSystem = collection.Resolve<IEntityManager>().System<LoadoutSystem>(); var loadoutSystem = collection.Resolve<IEntityManager>().System<LoadoutSystem>();
@@ -74,7 +75,7 @@ public sealed partial class LoadoutGroupContainer : BoxContainer
var matchingLoadout = selected.FirstOrDefault(e => e.Prototype == loadoutProto); var matchingLoadout = selected.FirstOrDefault(e => e.Prototype == loadoutProto);
var pressed = matchingLoadout != null; var pressed = matchingLoadout != null;
var enabled = loadout.IsValid(session, loadoutProto, collection, out var reason); var enabled = loadout.IsValid(profile, session, loadoutProto, collection, out var reason);
var loadoutContainer = new LoadoutContainer(loadoutProto, !enabled, reason); var loadoutContainer = new LoadoutContainer(loadoutProto, !enabled, reason);
loadoutContainer.Select.Pressed = pressed; loadoutContainer.Select.Pressed = pressed;
loadoutContainer.Text = loadoutSystem.GetName(loadProto); loadoutContainer.Text = loadoutSystem.GetName(loadProto);

View File

@@ -1,13 +1,12 @@
using Content.Client.Lobby;
using Content.Client.UserInterface.Controls; using Content.Client.UserInterface.Controls;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts; using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
using Robust.Client.AutoGenerated; using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML; using Robust.Client.UserInterface.XAML;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
namespace Content.Client.Preferences.UI; namespace Content.Client.Lobby.UI.Loadouts;
[GenerateTypedNameReferences] [GenerateTypedNameReferences]
public sealed partial class LoadoutWindow : FancyWindow public sealed partial class LoadoutWindow : FancyWindow
@@ -17,9 +16,12 @@ public sealed partial class LoadoutWindow : FancyWindow
private List<LoadoutGroupContainer> _groups = new(); private List<LoadoutGroupContainer> _groups = new();
public LoadoutWindow(RoleLoadout loadout, RoleLoadoutPrototype proto, ICommonSession session, IDependencyCollection collection) public HumanoidCharacterProfile Profile;
public LoadoutWindow(HumanoidCharacterProfile profile, RoleLoadout loadout, RoleLoadoutPrototype proto, ICommonSession session, IDependencyCollection collection)
{ {
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
Profile = profile;
var protoManager = collection.Resolve<IPrototypeManager>(); var protoManager = collection.Resolve<IPrototypeManager>();
foreach (var group in proto.Groups) foreach (var group in proto.Groups)
@@ -27,7 +29,7 @@ public sealed partial class LoadoutWindow : FancyWindow
if (!protoManager.TryIndex(group, out var groupProto)) if (!protoManager.TryIndex(group, out var groupProto))
continue; continue;
var container = new LoadoutGroupContainer(loadout, protoManager.Index(group), session, collection); var container = new LoadoutGroupContainer(profile, loadout, protoManager.Index(group), session, collection);
LoadoutGroupsContainer.AddTab(container, Loc.GetString(groupProto.Name)); LoadoutGroupsContainer.AddTab(container, Loc.GetString(groupProto.Name));
_groups.Add(container); _groups.Add(container);
@@ -43,18 +45,11 @@ public sealed partial class LoadoutWindow : FancyWindow
} }
} }
public override void Close()
{
base.Close();
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.SetDummyJob(null);
}
public void RefreshLoadouts(RoleLoadout loadout, ICommonSession session, IDependencyCollection collection) public void RefreshLoadouts(RoleLoadout loadout, ICommonSession session, IDependencyCollection collection)
{ {
foreach (var group in _groups) foreach (var group in _groups)
{ {
group.RefreshLoadouts(loadout, session, collection); group.RefreshLoadouts(Profile, loadout, session, collection);
} }
} }
} }

View File

@@ -10,12 +10,16 @@ namespace Content.Client.Lobby.UI;
[GenerateTypedNameReferences] [GenerateTypedNameReferences]
public sealed partial class LobbyCharacterPreviewPanel : Control public sealed partial class LobbyCharacterPreviewPanel : Control
{ {
[Dependency] private readonly IEntityManager _entManager = default!;
public Button CharacterSetupButton => CharacterSetup; public Button CharacterSetupButton => CharacterSetup;
private EntityUid? _previewDummy;
public LobbyCharacterPreviewPanel() public LobbyCharacterPreviewPanel()
{ {
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
UserInterfaceManager.GetUIController<LobbyUIController>().SetPreviewPanel(this); IoCManager.InjectDependencies(this);
} }
public void SetLoaded(bool value) public void SetLoaded(bool value)
@@ -26,11 +30,18 @@ public sealed partial class LobbyCharacterPreviewPanel : Control
public void SetSummaryText(string value) public void SetSummaryText(string value)
{ {
Summary.Text = string.Empty; Summary.Text = value;
} }
public void SetSprite(EntityUid uid) public void SetSprite(EntityUid uid)
{ {
if (_previewDummy != null)
{
_entManager.DeleteEntity(_previewDummy);
}
_previewDummy = uid;
ViewBox.DisposeAllChildren(); ViewBox.DisposeAllChildren();
var spriteView = new SpriteView var spriteView = new SpriteView
{ {
@@ -42,4 +53,11 @@ public sealed partial class LobbyCharacterPreviewPanel : Control
spriteView.SetEntity(uid); spriteView.SetEntity(uid);
ViewBox.AddChild(spriteView); ViewBox.AddChild(spriteView);
} }
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_entManager.DeleteEntity(_previewDummy);
_previewDummy = null;
}
} }

View File

@@ -8,10 +8,9 @@ using Robust.Client.UserInterface.XAML;
namespace Content.Client.Lobby.UI namespace Content.Client.Lobby.UI
{ {
[GenerateTypedNameReferences] [GenerateTypedNameReferences]
internal sealed partial class LobbyGui : UIScreen public sealed partial class LobbyGui : UIScreen
{ {
[Dependency] private readonly IClientConsoleHost _consoleHost = default!; [Dependency] private readonly IClientConsoleHost _consoleHost = default!;
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
public LobbyGui() public LobbyGui()
{ {
@@ -23,7 +22,7 @@ namespace Content.Client.Lobby.UI
LobbySong.SetMarkup(Loc.GetString("lobby-state-song-no-song-text")); LobbySong.SetMarkup(Loc.GetString("lobby-state-song-no-song-text"));
LeaveButton.OnPressed += _ => _consoleHost.ExecuteCommand("disconnect"); LeaveButton.OnPressed += _ => _consoleHost.ExecuteCommand("disconnect");
OptionsButton.OnPressed += _ => _userInterfaceManager.GetUIController<OptionsUIController>().ToggleWindow(); OptionsButton.OnPressed += _ => UserInterfaceManager.GetUIController<OptionsUIController>().ToggleWindow();
} }
public void SwitchState(LobbyGuiState state) public void SwitchState(LobbyGuiState state)
@@ -40,7 +39,7 @@ namespace Content.Client.Lobby.UI
case LobbyGuiState.CharacterSetup: case LobbyGuiState.CharacterSetup:
CharacterSetupState.Visible = true; CharacterSetupState.Visible = true;
var actualWidth = (float) _userInterfaceManager.RootControl.PixelWidth; var actualWidth = (float) UserInterfaceManager.RootControl.PixelWidth;
var setupWidth = (float) LeftSide.PixelWidth; var setupWidth = (float) LeftSide.PixelWidth;
if (1 - (setupWidth / actualWidth) > 0.30) if (1 - (setupWidth / actualWidth) > 0.30)
@@ -48,6 +47,8 @@ namespace Content.Client.Lobby.UI
RightSide.Visible = false; RightSide.Visible = false;
} }
UserInterfaceManager.GetUIController<LobbyUIController>().ReloadCharacterSetup();
break; break;
} }
} }

View File

@@ -2,23 +2,20 @@ using JetBrains.Annotations;
using Robust.Client.AutoGenerated; using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML; using Robust.Client.UserInterface.XAML;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
namespace Content.Client.Lobby.UI namespace Content.Client.Lobby.UI;
[GenerateTypedNameReferences]
[UsedImplicitly]
public sealed partial class ObserveWarningWindow : DefaultWindow
{ {
[GenerateTypedNameReferences] public ObserveWarningWindow()
[UsedImplicitly]
internal sealed partial class ObserveWarningWindow : DefaultWindow
{ {
public ObserveWarningWindow() Title = Loc.GetString("observe-warning-window-title");
{ RobustXamlLoader.Load(this);
Title = Loc.GetString("observe-warning-window-title"); IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
ObserveButton.OnPressed += _ => { this.Close(); }; ObserveButton.OnPressed += _ => { this.Close(); };
NevermindButton.OnPressed += _ => { this.Close(); }; NevermindButton.OnPressed += _ => { this.Close(); };
}
} }
} }

View File

@@ -0,0 +1,9 @@
<BoxContainer xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Orientation="Horizontal">
<Label Name="TitleLabel"
Margin="5 0"
MouseFilter="Stop"/>
<BoxContainer Name="OptionsContainer"
SetWidth="400"/>
</BoxContainer>

View File

@@ -0,0 +1,118 @@
using System.Numerics;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Utility;
namespace Content.Client.Lobby.UI.Roles;
/// <summary>
/// A generic locking selector.
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class RequirementsSelector : BoxContainer
{
private readonly RadioOptions<int> _options;
private readonly StripeBack _lockStripe;
public event Action<int>? OnSelected;
public int Selected => _options.SelectedId;
public RequirementsSelector()
{
RobustXamlLoader.Load(this);
_options = new RadioOptions<int>(RadioOptionsLayout.Horizontal)
{
FirstButtonStyle = StyleBase.ButtonOpenRight,
ButtonStyle = StyleBase.ButtonOpenBoth,
LastButtonStyle = StyleBase.ButtonOpenLeft,
HorizontalExpand = true,
};
//Override default radio option button width
_options.GenerateItem = GenerateButton;
_options.OnItemSelected += args =>
{
_options.Select(args.Id);
OnSelected?.Invoke(args.Id);
};
var requirementsLabel = new Label()
{
Text = Loc.GetString("role-timer-locked"),
Visible = true,
HorizontalAlignment = HAlignment.Center,
StyleClasses = {StyleBase.StyleClassLabelSubText},
};
_lockStripe = new StripeBack()
{
Visible = false,
HorizontalExpand = true,
HasMargins = false,
MouseFilter = MouseFilterMode.Stop,
Children =
{
requirementsLabel
}
};
}
/// <summary>
/// Actually adds the controls.
/// </summary>
public void Setup((string, int)[] items, string title, int titleSize, string? description, TextureRect? icon = null)
{
foreach (var (text, value) in items)
{
_options.AddItem(Loc.GetString(text), value);
}
TitleLabel.Text = title;
TitleLabel.MinSize = new Vector2(titleSize, 0f);
TitleLabel.ToolTip = description;
if (icon != null)
{
AddChild(icon);
icon.SetPositionFirst();
}
OptionsContainer.AddChild(_options);
OptionsContainer.AddChild(_lockStripe);
}
public void LockRequirements(FormattedMessage requirements)
{
var tooltip = new Tooltip();
tooltip.SetMessage(requirements);
_lockStripe.TooltipSupplier = _ => tooltip;
_lockStripe.Visible = true;
_options.Visible = false;
}
public void UnlockRequirements()
{
_lockStripe.Visible = false;
_options.Visible = true;
}
private Button GenerateButton(string text, int value)
{
return new Button
{
Text = text,
MinWidth = 90,
HorizontalExpand = true,
};
}
public void Select(int id)
{
_options.Select(id);
}
}

View File

@@ -0,0 +1,7 @@
<Control xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<BoxContainer Name="Container"
Orientation="Horizontal">
<CheckBox Name="Checkbox"/>
</BoxContainer>
</Control>

View File

@@ -0,0 +1,36 @@
using Content.Shared.Traits;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Lobby.UI.Roles;
[GenerateTypedNameReferences]
public sealed partial class TraitPreferenceSelector : Control
{
public bool Preference
{
get => Checkbox.Pressed;
set => Checkbox.Pressed = value;
}
public event Action<bool>? PreferenceChanged;
public TraitPreferenceSelector(TraitPrototype trait)
{
RobustXamlLoader.Load(this);
Checkbox.Text = Loc.GetString(trait.Name);
Checkbox.OnToggled += OnCheckBoxToggled;
if (trait.Description is { } desc)
{
Checkbox.ToolTip = Loc.GetString(desc);
}
}
private void OnCheckBoxToggled(BaseButton.ButtonToggledEventArgs args)
{
PreferenceChanged?.Invoke(Preference);
}
}

View File

@@ -1,41 +0,0 @@
using Content.Client.Players.PlayTimeTracking;
using Content.Shared.Roles;
using Robust.Client.UserInterface.Controls;
namespace Content.Client.Preferences.UI;
public sealed class AntagPreferenceSelector : RequirementsSelector<AntagPrototype>
{
// 0 is yes and 1 is no
public bool Preference
{
get => Options.SelectedValue == 0;
set => Options.Select((value && !Disabled) ? 0 : 1);
}
public event Action<bool>? PreferenceChanged;
public AntagPreferenceSelector(AntagPrototype proto, ButtonGroup btnGroup)
: base(proto, btnGroup)
{
Options.OnItemSelected += args => PreferenceChanged?.Invoke(Preference);
var items = new[]
{
("humanoid-profile-editor-antag-preference-yes-button", 0),
("humanoid-profile-editor-antag-preference-no-button", 1)
};
var title = Loc.GetString(proto.Name);
var description = Loc.GetString(proto.Objective);
// Not supported yet get fucked.
Setup(null, items, title, 250, description);
// immediately lock requirements if they arent met.
// another function checks Disabled after creating the selector so this has to be done now
var requirements = IoCManager.Resolve<JobRequirementsManager>();
if (proto.Requirements != null && !requirements.CheckRoleTime(proto.Requirements, out var reason))
{
LockRequirements(reason);
}
}
}

View File

@@ -1,276 +0,0 @@
using System.Linq;
using System.Numerics;
using Content.Client.Humanoid;
using Content.Client.Info;
using Content.Client.Info.PlaytimeStats;
using Content.Client.Lobby;
using Content.Client.Resources;
using Content.Client.Stylesheets;
using Content.Shared.Clothing;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using static Robust.Client.UserInterface.Controls.BoxContainer;
using Direction = Robust.Shared.Maths.Direction;
namespace Content.Client.Preferences.UI
{
[GenerateTypedNameReferences]
public sealed partial class CharacterSetupGui : Control
{
private readonly IClientPreferencesManager _preferencesManager;
private readonly IEntityManager _entityManager;
private readonly IPrototypeManager _prototypeManager;
private readonly Button _createNewCharacterButton;
private readonly HumanoidProfileEditor _humanoidProfileEditor;
public CharacterSetupGui(
IEntityManager entityManager,
IResourceCache resourceCache,
IClientPreferencesManager preferencesManager,
IPrototypeManager prototypeManager,
IConfigurationManager configurationManager)
{
RobustXamlLoader.Load(this);
_entityManager = entityManager;
_prototypeManager = prototypeManager;
_preferencesManager = preferencesManager;
var panelTex = resourceCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png");
var back = new StyleBoxTexture
{
Texture = panelTex,
Modulate = new Color(37, 37, 42)
};
back.SetPatchMargin(StyleBox.Margin.All, 10);
BackgroundPanel.PanelOverride = back;
_createNewCharacterButton = new Button
{
Text = Loc.GetString("character-setup-gui-create-new-character-button"),
};
_createNewCharacterButton.OnPressed += args =>
{
preferencesManager.CreateCharacter(HumanoidCharacterProfile.Random());
UpdateUI();
args.Event.Handle();
};
_humanoidProfileEditor = new HumanoidProfileEditor(preferencesManager, prototypeManager, configurationManager);
_humanoidProfileEditor.OnProfileChanged += ProfileChanged;
CharEditor.AddChild(_humanoidProfileEditor);
UpdateUI();
RulesButton.OnPressed += _ => new RulesAndInfoWindow().Open();
StatsButton.OnPressed += _ => new PlaytimeStatsWindow().OpenCentered();
preferencesManager.OnServerDataLoaded += UpdateUI;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
_preferencesManager.OnServerDataLoaded -= UpdateUI;
}
public void Save() => _humanoidProfileEditor.Save();
private void ProfileChanged(ICharacterProfile profile, int profileSlot)
{
_humanoidProfileEditor.UpdateControls();
UpdateUI();
}
public void UpdateControls()
{
// Reset sliders etc. upon going going back to GUI.
_humanoidProfileEditor.LoadServerData();
}
private void UpdateUI()
{
var numberOfFullSlots = 0;
var characterButtonsGroup = new ButtonGroup();
Characters.RemoveAllChildren();
if (!_preferencesManager.ServerDataLoaded)
{
return;
}
_createNewCharacterButton.ToolTip =
Loc.GetString("character-setup-gui-create-new-character-button-tooltip",
("maxCharacters", _preferencesManager.Settings!.MaxCharacterSlots));
foreach (var (slot, character) in _preferencesManager.Preferences!.Characters)
{
numberOfFullSlots++;
var characterPickerButton = new CharacterPickerButton(_entityManager,
_preferencesManager,
_prototypeManager,
characterButtonsGroup,
character);
Characters.AddChild(characterPickerButton);
var characterIndexCopy = slot;
characterPickerButton.OnPressed += args =>
{
_humanoidProfileEditor.Profile = (HumanoidCharacterProfile)character;
_humanoidProfileEditor.CharacterSlot = characterIndexCopy;
_humanoidProfileEditor.UpdateControls();
_preferencesManager.SelectCharacter(character);
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.UpdateProfile(_humanoidProfileEditor.Profile);
controller.ReloadCharacterUI();
UpdateUI();
args.Event.Handle();
};
}
_createNewCharacterButton.Disabled =
numberOfFullSlots >= _preferencesManager.Settings.MaxCharacterSlots;
Characters.AddChild(_createNewCharacterButton);
// TODO: Move this shit to the Lobby UI controller
}
/// <summary>
/// Shows individual characters on the side of the character GUI.
/// </summary>
private sealed class CharacterPickerButton : ContainerButton
{
private EntityUid _previewDummy;
public CharacterPickerButton(
IEntityManager entityManager,
IClientPreferencesManager preferencesManager,
IPrototypeManager prototypeManager,
ButtonGroup group,
ICharacterProfile profile)
{
AddStyleClass(StyleClassButton);
ToggleMode = true;
Group = group;
var humanoid = profile as HumanoidCharacterProfile;
if (humanoid is not null)
{
var dummy = prototypeManager.Index<SpeciesPrototype>(humanoid.Species).DollPrototype;
_previewDummy = entityManager.SpawnEntity(dummy, MapCoordinates.Nullspace);
}
else
{
_previewDummy = entityManager.SpawnEntity(prototypeManager.Index<SpeciesPrototype>(SharedHumanoidAppearanceSystem.DefaultSpecies).DollPrototype, MapCoordinates.Nullspace);
}
EntitySystem.Get<HumanoidAppearanceSystem>().LoadProfile(_previewDummy, (HumanoidCharacterProfile)profile);
if (humanoid != null)
{
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
var job = controller.GetPreferredJob(humanoid);
controller.GiveDummyJobClothes(_previewDummy, humanoid, job);
if (prototypeManager.HasIndex<RoleLoadoutPrototype>(LoadoutSystem.GetJobPrototype(job.ID)))
{
var loadout = humanoid.GetLoadoutOrDefault(LoadoutSystem.GetJobPrototype(job.ID), entityManager, prototypeManager);
controller.GiveDummyLoadout(_previewDummy, loadout);
}
}
var isSelectedCharacter = profile == preferencesManager.Preferences?.SelectedCharacter;
if (isSelectedCharacter)
Pressed = true;
var view = new SpriteView
{
Scale = new Vector2(2, 2),
OverrideDirection = Direction.South
};
view.SetEntity(_previewDummy);
var description = profile.Name;
var highPriorityJob = humanoid?.JobPriorities.SingleOrDefault(p => p.Value == JobPriority.High).Key;
if (highPriorityJob != null)
{
var jobName = IoCManager.Resolve<IPrototypeManager>().Index<JobPrototype>(highPriorityJob).LocalizedName;
description = $"{description}\n{jobName}";
}
var descriptionLabel = new Label
{
Text = description,
ClipText = true,
HorizontalExpand = true
};
var deleteButton = new Button
{
Text = Loc.GetString("character-setup-gui-character-picker-button-delete-button"),
Visible = !isSelectedCharacter,
};
var confirmDeleteButton = new Button
{
Text = Loc.GetString("character-setup-gui-character-picker-button-confirm-delete-button"),
Visible = false,
};
confirmDeleteButton.ModulateSelfOverride = StyleNano.ButtonColorCautionDefault;
confirmDeleteButton.OnPressed += _ =>
{
Parent?.RemoveChild(this);
Parent?.RemoveChild(confirmDeleteButton);
preferencesManager.DeleteCharacter(profile);
};
deleteButton.OnPressed += _ =>
{
deleteButton.Visible = false;
confirmDeleteButton.Visible = true;
};
var internalHBox = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
HorizontalExpand = true,
SeparationOverride = 0,
Children =
{
view,
descriptionLabel,
deleteButton,
confirmDeleteButton
}
};
AddChild(internalHBox);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
IoCManager.Resolve<IEntityManager>().DeleteEntity(_previewDummy);
_previewDummy = default;
}
}
}
}

View File

@@ -1,23 +0,0 @@
using Content.Shared.Preferences;
using Robust.Shared.Prototypes;
namespace Content.Client.Preferences.UI
{
public sealed partial class HumanoidProfileEditor
{
private void RandomizeEverything()
{
Profile = HumanoidCharacterProfile.Random();
UpdateControls();
IsDirty = true;
}
private void RandomizeName()
{
if (Profile == null) return;
var name = HumanoidCharacterProfile.GetName(Profile.Species, Profile.Gender);
SetName(name);
UpdateNameEdit();
}
}
}

View File

@@ -1,46 +0,0 @@
using System.Numerics;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
using Content.Shared.Roles;
using Content.Shared.StatusIcon;
using Robust.Client.UserInterface.Controls;
using Robust.Client.Utility;
using Robust.Shared.Prototypes;
namespace Content.Client.Preferences.UI;
public sealed class JobPrioritySelector : RequirementsSelector<JobPrototype>
{
public JobPriority Priority
{
get => (JobPriority) Options.SelectedValue;
set => Options.SelectByValue((int) value);
}
public event Action<JobPriority>? PriorityChanged;
public JobPrioritySelector(RoleLoadout? loadout, JobPrototype proto, ButtonGroup btnGroup, IPrototypeManager protoMan)
: base(proto, btnGroup)
{
Options.OnItemSelected += args => PriorityChanged?.Invoke(Priority);
var items = new[]
{
("humanoid-profile-editor-job-priority-high-button", (int) JobPriority.High),
("humanoid-profile-editor-job-priority-medium-button", (int) JobPriority.Medium),
("humanoid-profile-editor-job-priority-low-button", (int) JobPriority.Low),
("humanoid-profile-editor-job-priority-never-button", (int) JobPriority.Never),
};
var icon = new TextureRect
{
TextureScale = new Vector2(2, 2),
VerticalAlignment = VAlignment.Center
};
var jobIcon = protoMan.Index<StatusIconPrototype>(proto.Icon);
icon.Texture = jobIcon.Icon.Frame0();
Setup(loadout, items, proto.LocalizedName, 200, proto.LocalizedDescription, icon);
}
}

View File

@@ -1,222 +0,0 @@
using System.Numerics;
using Content.Client.Lobby;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls;
using Content.Shared.Clothing;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
using Content.Shared.Roles;
using Robust.Client.Player;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.Preferences.UI;
public abstract class RequirementsSelector<T> : BoxContainer where T : IPrototype
{
private ButtonGroup _loadoutGroup;
public T Proto { get; }
public bool Disabled => _lockStripe.Visible;
protected readonly RadioOptions<int> Options;
private readonly StripeBack _lockStripe;
private LoadoutWindow? _loadoutWindow;
private RoleLoadout? _loadout;
/// <summary>
/// Raised if a loadout has been updated.
/// </summary>
public event Action<RoleLoadout>? LoadoutUpdated;
protected RequirementsSelector(T proto, ButtonGroup loadoutGroup)
{
_loadoutGroup = loadoutGroup;
Proto = proto;
Options = new RadioOptions<int>(RadioOptionsLayout.Horizontal)
{
FirstButtonStyle = StyleBase.ButtonOpenRight,
ButtonStyle = StyleBase.ButtonOpenBoth,
LastButtonStyle = StyleBase.ButtonOpenLeft,
HorizontalExpand = true,
};
//Override default radio option button width
Options.GenerateItem = GenerateButton;
Options.OnItemSelected += args => Options.Select(args.Id);
var requirementsLabel = new Label()
{
Text = Loc.GetString("role-timer-locked"),
Visible = true,
HorizontalAlignment = HAlignment.Center,
StyleClasses = {StyleBase.StyleClassLabelSubText},
};
_lockStripe = new StripeBack()
{
Visible = false,
HorizontalExpand = true,
HasMargins = false,
MouseFilter = MouseFilterMode.Stop,
Children =
{
requirementsLabel
}
};
// Setup must be called after
}
/// <summary>
/// Actually adds the controls, must be called in the inheriting class' constructor.
/// </summary>
protected void Setup(RoleLoadout? loadout, (string, int)[] items, string title, int titleSize, string? description, TextureRect? icon = null)
{
_loadout = loadout;
foreach (var (text, value) in items)
{
Options.AddItem(Loc.GetString(text), value);
}
var titleLabel = new Label()
{
Margin = new Thickness(5f, 0, 5f, 0),
Text = title,
MinSize = new Vector2(titleSize, 0),
MouseFilter = MouseFilterMode.Stop,
ToolTip = description
};
if (icon != null)
AddChild(icon);
AddChild(titleLabel);
AddChild(Options);
AddChild(_lockStripe);
var loadoutWindowBtn = new Button()
{
Text = Loc.GetString("loadout-window"),
HorizontalAlignment = HAlignment.Right,
Group = _loadoutGroup,
Margin = new Thickness(3f, 0f, 0f, 0f),
};
var collection = IoCManager.Instance!;
var protoManager = collection.Resolve<IPrototypeManager>();
// If no loadout found then disabled button
if (!protoManager.HasIndex<RoleLoadoutPrototype>(LoadoutSystem.GetJobPrototype(Proto.ID)))
{
loadoutWindowBtn.Disabled = true;
}
// else
else
{
var session = collection.Resolve<IPlayerManager>().LocalSession!;
// TODO: Most of lobby state should be a uicontroller
// trying to handle all this shit is a big-ass mess.
// Every time I touch it I try to make it slightly better but it needs a howitzer dropped on it.
loadoutWindowBtn.OnPressed += args =>
{
if (args.Button.Pressed)
{
// We only create a loadout when necessary to avoid unnecessary DB entries.
_loadout ??= new RoleLoadout(LoadoutSystem.GetJobPrototype(Proto.ID));
_loadout.SetDefault(protoManager);
_loadoutWindow = new LoadoutWindow(_loadout, protoManager.Index(_loadout.Role), session, collection)
{
Title = Loc.GetString(Proto.ID + "-loadout"),
};
_loadoutWindow.RefreshLoadouts(_loadout, session, collection);
// If it's a job preview then refresh it.
if (Proto is JobPrototype jobProto)
{
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.SetDummyJob(jobProto);
}
_loadoutWindow.OnLoadoutUnpressed += (selectedGroup, selectedLoadout) =>
{
if (!_loadout.RemoveLoadout(selectedGroup, selectedLoadout, protoManager))
return;
_loadout.EnsureValid(session, collection);
_loadoutWindow.RefreshLoadouts(_loadout, session, collection);
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.ReloadProfile();
LoadoutUpdated?.Invoke(_loadout);
};
_loadoutWindow.OnLoadoutPressed += (selectedGroup, selectedLoadout) =>
{
if (!_loadout.AddLoadout(selectedGroup, selectedLoadout, protoManager))
return;
_loadout.EnsureValid(session, collection);
_loadoutWindow.RefreshLoadouts(_loadout, session, collection);
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.ReloadProfile();
LoadoutUpdated?.Invoke(_loadout);
};
_loadoutWindow.OpenCenteredLeft();
_loadoutWindow.OnClose += () =>
{
loadoutWindowBtn.Pressed = false;
_loadoutWindow?.Dispose();
_loadoutWindow = null;
};
}
else
{
CloseLoadout();
}
};
}
AddChild(loadoutWindowBtn);
}
public void CloseLoadout()
{
_loadoutWindow?.Close();
_loadoutWindow?.Dispose();
_loadoutWindow = null;
}
public void LockRequirements(FormattedMessage requirements)
{
var tooltip = new Tooltip();
tooltip.SetMessage(requirements);
_lockStripe.TooltipSupplier = _ => tooltip;
_lockStripe.Visible = true;
Options.Visible = false;
}
// TODO: Subscribe to roletimers event. I am too lazy to do this RN But I doubt most people will notice fn
public void UnlockRequirements()
{
_lockStripe.Visible = false;
Options.Visible = true;
}
private Button GenerateButton(string text, int value)
{
return new Button
{
Text = text,
MinWidth = 90,
HorizontalExpand = true,
};
}
}

View File

@@ -1,5 +1,4 @@
using Content.Client.Lobby; using Content.Client.Lobby;
using Content.Client.Preferences;
using Content.Server.Preferences.Managers; using Content.Server.Preferences.Managers;
using Content.Shared.Preferences; using Content.Shared.Preferences;
using Robust.Client.State; using Robust.Client.State;

View File

@@ -39,32 +39,21 @@ namespace Content.IntegrationTests.Tests.Preferences
private static HumanoidCharacterProfile CharlieCharlieson() private static HumanoidCharacterProfile CharlieCharlieson()
{ {
return new( return new()
"Charlie Charlieson", {
"The biggest boy around.", Name = "Charlie Charlieson",
"Human", FlavorText = "The biggest boy around.",
21, Species = "Human",
Sex.Male, Age = 21,
Gender.Epicene, Appearance = new(
new HumanoidCharacterAppearance(
"Afro", "Afro",
Color.Aqua, Color.Aqua,
"Shaved", "Shaved",
Color.Aquamarine, Color.Aquamarine,
Color.Azure, Color.Azure,
Color.Beige, Color.Beige,
new () new ())
), };
SpawnPriorityPreference.None,
new Dictionary<string, JobPriority>
{
{SharedGameTicker.FallbackOverflowJob, JobPriority.High}
},
PreferenceUnavailableMode.StayInLobby,
new List<string> (),
new List<string>(),
new Dictionary<string, RoleLoadout>()
);
} }
private static ServerDbSqlite GetDb(RobustIntegrationTest.ServerIntegrationInstance server) private static ServerDbSqlite GetDb(RobustIntegrationTest.ServerIntegrationInstance server)

View File

@@ -14,7 +14,6 @@ using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Markings; using Content.Shared.Humanoid.Markings;
using Content.Shared.Preferences; using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts; using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Robust.Shared.Enums; using Robust.Shared.Enums;
using Robust.Shared.Network; using Robust.Shared.Network;
@@ -253,8 +252,8 @@ namespace Content.Server.Database
spawnPriority, spawnPriority,
jobs, jobs,
(PreferenceUnavailableMode) profile.PreferenceUnavailable, (PreferenceUnavailableMode) profile.PreferenceUnavailable,
antags.ToList(), antags.ToHashSet(),
traits.ToList(), traits.ToHashSet(),
loadouts loadouts
); );
} }

View File

@@ -9,8 +9,29 @@ namespace Content.Shared.Humanoid;
[DataDefinition] [DataDefinition]
[Serializable, NetSerializable] [Serializable, NetSerializable]
public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance, IEquatable<HumanoidCharacterAppearance>
{ {
[DataField("hair")]
public string HairStyleId { get; set; } = HairStyles.DefaultHairStyle;
[DataField]
public Color HairColor { get; set; } = Color.Black;
[DataField("facialHair")]
public string FacialHairStyleId { get; set; } = HairStyles.DefaultFacialHairStyle;
[DataField]
public Color FacialHairColor { get; set; } = Color.Black;
[DataField]
public Color EyeColor { get; set; } = Color.Black;
[DataField]
public Color SkinColor { get; set; } = Humanoid.SkinColor.ValidHumanSkinTone;
[DataField]
public List<Marking> Markings { get; set; } = new();
public HumanoidCharacterAppearance(string hairStyleId, public HumanoidCharacterAppearance(string hairStyleId,
Color hairColor, Color hairColor,
string facialHairStyleId, string facialHairStyleId,
@@ -28,26 +49,11 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance
Markings = markings; Markings = markings;
} }
[DataField("hair")] public HumanoidCharacterAppearance(HumanoidCharacterAppearance other) :
public string HairStyleId { get; private set; } this(other.HairStyleId, other.HairColor, other.FacialHairStyleId, other.FacialHairColor, other.EyeColor, other.SkinColor, new(other.Markings))
{
[DataField("hairColor")] }
public Color HairColor { get; private set; }
[DataField("facialHair")]
public string FacialHairStyleId { get; private set; }
[DataField("facialHairColor")]
public Color FacialHairColor { get; private set; }
[DataField("eyeColor")]
public Color EyeColor { get; private set; }
[DataField("skinColor")]
public Color SkinColor { get; private set; }
[DataField("markings")]
public List<Marking> Markings { get; private set; }
public HumanoidCharacterAppearance WithHairStyleName(string newName) public HumanoidCharacterAppearance WithHairStyleName(string newName)
{ {
@@ -84,18 +90,6 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance
return new(HairStyleId, HairColor, FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, newMarkings); return new(HairStyleId, HairColor, FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, newMarkings);
} }
public HumanoidCharacterAppearance() : this(
HairStyles.DefaultHairStyle,
Color.Black,
HairStyles.DefaultFacialHairStyle,
Color.Black,
Color.Black,
Humanoid.SkinColor.ValidHumanSkinTone,
new ()
)
{
}
public static HumanoidCharacterAppearance DefaultWithSpecies(string species) public static HumanoidCharacterAppearance DefaultWithSpecies(string species)
{ {
var speciesPrototype = IoCManager.Resolve<IPrototypeManager>().Index<SpeciesPrototype>(species); var speciesPrototype = IoCManager.Resolve<IPrototypeManager>().Index<SpeciesPrototype>(species);
@@ -245,4 +239,32 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance
if (!Markings.SequenceEqual(other.Markings)) return false; if (!Markings.SequenceEqual(other.Markings)) return false;
return true; return true;
} }
public bool Equals(HumanoidCharacterAppearance? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return HairStyleId == other.HairStyleId &&
HairColor.Equals(other.HairColor) &&
FacialHairStyleId == other.FacialHairStyleId &&
FacialHairColor.Equals(other.FacialHairColor) &&
EyeColor.Equals(other.EyeColor) &&
SkinColor.Equals(other.SkinColor) &&
Markings.SequenceEqual(other.Markings);
}
public override bool Equals(object? obj)
{
return ReferenceEquals(this, obj) || obj is HumanoidCharacterAppearance other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(HairStyleId, HairColor, FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, Markings);
}
public HumanoidCharacterAppearance Clone()
{
return new(this);
}
} }

View File

@@ -0,0 +1,19 @@
using Content.Shared.Preferences;
namespace Content.Shared.Humanoid;
/// <summary>
/// Holds all of the data for importing / exporting character profiles.
/// </summary>
[DataDefinition]
public sealed partial class HumanoidProfileExport
{
[DataField]
public string ForkId;
[DataField]
public int Version = 1;
[DataField(required: true)]
public HumanoidCharacterProfile Profile = default!;
}

View File

@@ -1,13 +1,22 @@
using System.IO;
using System.Linq; using System.Linq;
using Content.Shared.CCVar;
using Content.Shared.Decals; using Content.Shared.Decals;
using Content.Shared.Examine; using Content.Shared.Examine;
using Content.Shared.Humanoid.Markings; using Content.Shared.Humanoid.Markings;
using Content.Shared.Humanoid.Prototypes; using Content.Shared.Humanoid.Prototypes;
using Content.Shared.IdentityManagement; using Content.Shared.IdentityManagement;
using Content.Shared.Preferences; using Content.Shared.Preferences;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects.Components.Localization; using Robust.Shared.GameObjects.Components.Localization;
using Robust.Shared.Network; using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Utility;
using YamlDotNet.RepresentationModel;
namespace Content.Shared.Humanoid; namespace Content.Shared.Humanoid;
@@ -22,8 +31,10 @@ namespace Content.Shared.Humanoid;
/// </summary> /// </summary>
public abstract class SharedHumanoidAppearanceSystem : EntitySystem public abstract class SharedHumanoidAppearanceSystem : EntitySystem
{ {
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
[Dependency] private readonly INetManager _netManager = default!; [Dependency] private readonly INetManager _netManager = default!;
[Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly ISerializationManager _serManager = default!;
[Dependency] private readonly MarkingManager _markingManager = default!; [Dependency] private readonly MarkingManager _markingManager = default!;
[ValidatePrototypeId<SpeciesPrototype>] [ValidatePrototypeId<SpeciesPrototype>]
@@ -37,6 +48,37 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem
SubscribeLocalEvent<HumanoidAppearanceComponent, ExaminedEvent>(OnExamined); SubscribeLocalEvent<HumanoidAppearanceComponent, ExaminedEvent>(OnExamined);
} }
public DataNode ToDataNode(HumanoidCharacterProfile profile)
{
var export = new HumanoidProfileExport()
{
ForkId = _cfgManager.GetCVar(CVars.BuildForkId),
Profile = profile,
};
var dataNode = _serManager.WriteValue(export, alwaysWrite: true, notNullableOverride: true);
return dataNode;
}
public HumanoidCharacterProfile FromStream(Stream stream, ICommonSession session)
{
using var reader = new StreamReader(stream, EncodingHelpers.UTF8);
var yamlStream = new YamlStream();
yamlStream.Load(reader);
var root = yamlStream.Documents[0].RootNode;
var export = _serManager.Read<HumanoidProfileExport>(root.ToDataNode(), notNullableOverride: true);
/*
* Add custom handling here for forks / version numbers if you care.
*/
var profile = export.Profile;
var collection = IoCManager.Instance;
profile.EnsureValid(session, collection!);
return profile;
}
private void OnInit(EntityUid uid, HumanoidAppearanceComponent humanoid, ComponentInit args) private void OnInit(EntityUid uid, HumanoidAppearanceComponent humanoid, ComponentInit args)
{ {
if (string.IsNullOrEmpty(humanoid.Species) || _netManager.IsClient && !IsClientSide(uid)) if (string.IsNullOrEmpty(humanoid.Species) || _netManager.IsClient && !IsClientSide(uid))

View File

@@ -5,7 +5,6 @@ using Content.Shared.GameTicking;
using Content.Shared.Humanoid; using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Prototypes; using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences.Loadouts; using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
using Content.Shared.Roles; using Content.Shared.Roles;
using Content.Shared.Traits; using Content.Shared.Traits;
using Robust.Shared.Collections; using Robust.Shared.Collections;
@@ -32,16 +31,101 @@ namespace Content.Shared.Preferences
public const int MaxNameLength = 32; public const int MaxNameLength = 32;
public const int MaxDescLength = 512; public const int MaxDescLength = 512;
private readonly Dictionary<string, JobPriority> _jobPriorities; /// <summary>
private readonly List<string> _antagPreferences; /// Job preferences for initial spawn.
private readonly List<string> _traitPreferences; /// </summary>
[DataField]
private Dictionary<string, JobPriority> _jobPriorities = new()
{
{
SharedGameTicker.FallbackOverflowJob, JobPriority.High
}
};
/// <summary>
/// Antags we have opted in to.
/// </summary>
[DataField]
private HashSet<string> _antagPreferences = new();
/// <summary>
/// Enabled traits.
/// </summary>
[DataField]
private HashSet<string> _traitPreferences = new();
/// <summary>
/// <see cref="_loadouts"/>
/// </summary>
public IReadOnlyDictionary<string, RoleLoadout> Loadouts => _loadouts; public IReadOnlyDictionary<string, RoleLoadout> Loadouts => _loadouts;
private Dictionary<string, RoleLoadout> _loadouts; [DataField]
private Dictionary<string, RoleLoadout> _loadouts = new();
// What in the lord is happening here. [DataField]
private HumanoidCharacterProfile( public string Name { get; set; } = "John Doe";
/// <summary>
/// Detailed text that can appear for the character if <see cref="CCVars.FlavorText"/> is enabled.
/// </summary>
[DataField]
public string FlavorText { get; set; } = string.Empty;
/// <summary>
/// Associated <see cref="SpeciesPrototype"/> for this profile.
/// </summary>
[DataField]
public string Species { get; set; } = SharedHumanoidAppearanceSystem.DefaultSpecies;
[DataField]
public int Age { get; set; } = 18;
[DataField]
public Sex Sex { get; private set; } = Sex.Male;
[DataField]
public Gender Gender { get; private set; } = Gender.Male;
/// <summary>
/// <see cref="Appearance"/>
/// </summary>
public ICharacterAppearance CharacterAppearance => Appearance;
/// <summary>
/// Stores markings, eye colors, etc for the profile.
/// </summary>
[DataField]
public HumanoidCharacterAppearance Appearance { get; set; } = new();
/// <summary>
/// When spawning into a round what's the preferred spot to spawn.
/// </summary>
[DataField]
public SpawnPriorityPreference SpawnPriority { get; private set; } = SpawnPriorityPreference.None;
/// <summary>
/// <see cref="_jobPriorities"/>
/// </summary>
public IReadOnlyDictionary<string, JobPriority> JobPriorities => _jobPriorities;
/// <summary>
/// <see cref="_antagPreferences"/>
/// </summary>
public IReadOnlySet<string> AntagPreferences => _antagPreferences;
/// <summary>
/// <see cref="_traitPreferences"/>
/// </summary>
public IReadOnlySet<string> TraitPreferences => _traitPreferences;
/// <summary>
/// If we're unable to get one of our preferred jobs do we spawn as a fallback job or do we stay in lobby.
/// </summary>
[DataField]
public PreferenceUnavailableMode PreferenceUnavailable { get; private set; } =
PreferenceUnavailableMode.SpawnAsOverflow;
public HumanoidCharacterProfile(
string name, string name,
string flavortext, string flavortext,
string species, string species,
@@ -52,8 +136,8 @@ namespace Content.Shared.Preferences
SpawnPriorityPreference spawnPriority, SpawnPriorityPreference spawnPriority,
Dictionary<string, JobPriority> jobPriorities, Dictionary<string, JobPriority> jobPriorities,
PreferenceUnavailableMode preferenceUnavailable, PreferenceUnavailableMode preferenceUnavailable,
List<string> antagPreferences, HashSet<string> antagPreferences,
List<string> traitPreferences, HashSet<string> traitPreferences,
Dictionary<string, RoleLoadout> loadouts) Dictionary<string, RoleLoadout> loadouts)
{ {
Name = name; Name = name;
@@ -71,40 +155,21 @@ namespace Content.Shared.Preferences
_loadouts = loadouts; _loadouts = loadouts;
} }
/// <summary>Copy constructor but with overridable references (to prevent useless copies)</summary>
private HumanoidCharacterProfile(
HumanoidCharacterProfile other,
Dictionary<string, JobPriority> jobPriorities,
List<string> antagPreferences,
List<string> traitPreferences,
Dictionary<string, RoleLoadout> loadouts)
: this(other.Name, other.FlavorText, other.Species, other.Age, other.Sex, other.Gender, other.Appearance, other.SpawnPriority,
jobPriorities, other.PreferenceUnavailable, antagPreferences, traitPreferences, loadouts)
{
}
/// <summary>Copy constructor</summary> /// <summary>Copy constructor</summary>
private HumanoidCharacterProfile(HumanoidCharacterProfile other) public HumanoidCharacterProfile(HumanoidCharacterProfile other)
: this(other, new Dictionary<string, JobPriority>(other.JobPriorities), new List<string>(other.AntagPreferences), new List<string>(other.TraitPreferences), new Dictionary<string, RoleLoadout>(other.Loadouts)) : this(other.Name,
{ other.FlavorText,
} other.Species,
other.Age,
public HumanoidCharacterProfile( other.Sex,
string name, other.Gender,
string flavortext, other.Appearance.Clone(),
string species, other.SpawnPriority,
int age, new Dictionary<string, JobPriority>(other.JobPriorities),
Sex sex, other.PreferenceUnavailable,
Gender gender, new HashSet<string>(other.AntagPreferences),
HumanoidCharacterAppearance appearance, new HashSet<string>(other.TraitPreferences),
SpawnPriorityPreference spawnPriority, new Dictionary<string, RoleLoadout>(other.Loadouts))
IReadOnlyDictionary<string, JobPriority> jobPriorities,
PreferenceUnavailableMode preferenceUnavailable,
IReadOnlyList<string> antagPreferences,
IReadOnlyList<string> traitPreferences,
Dictionary<string, RoleLoadout> loadouts)
: this(name, flavortext, species, age, sex, gender, appearance, spawnPriority, new Dictionary<string, JobPriority>(jobPriorities),
preferenceUnavailable, new List<string>(antagPreferences), new List<string>(traitPreferences), new Dictionary<string, RoleLoadout>(loadouts))
{ {
} }
@@ -113,23 +178,7 @@ namespace Content.Shared.Preferences
/// Defaults to <see cref="SharedHumanoidAppearanceSystem.DefaultSpecies"/> for the species. /// Defaults to <see cref="SharedHumanoidAppearanceSystem.DefaultSpecies"/> for the species.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public HumanoidCharacterProfile() : this( public HumanoidCharacterProfile()
"John Doe",
"",
SharedHumanoidAppearanceSystem.DefaultSpecies,
18,
Sex.Male,
Gender.Male,
new HumanoidCharacterAppearance(),
SpawnPriorityPreference.None,
new Dictionary<string, JobPriority>
{
{SharedGameTicker.FallbackOverflowJob, JobPriority.High}
},
PreferenceUnavailableMode.SpawnAsOverflow,
new List<string>(),
new List<string>(),
new Dictionary<string, RoleLoadout>())
{ {
} }
@@ -140,23 +189,10 @@ namespace Content.Shared.Preferences
/// <returns>Humanoid character profile with default settings.</returns> /// <returns>Humanoid character profile with default settings.</returns>
public static HumanoidCharacterProfile DefaultWithSpecies(string species = SharedHumanoidAppearanceSystem.DefaultSpecies) public static HumanoidCharacterProfile DefaultWithSpecies(string species = SharedHumanoidAppearanceSystem.DefaultSpecies)
{ {
return new( return new()
"John Doe", {
"", Species = species,
species, };
18,
Sex.Male,
Gender.Male,
HumanoidCharacterAppearance.DefaultWithSpecies(species),
SpawnPriorityPreference.None,
new Dictionary<string, JobPriority>
{
{SharedGameTicker.FallbackOverflowJob, JobPriority.High}
},
PreferenceUnavailableMode.SpawnAsOverflow,
new List<string>(),
new List<string>(),
new Dictionary<string, RoleLoadout>());
} }
// TODO: This should eventually not be a visual change only. // TODO: This should eventually not be a visual change only.
@@ -201,36 +237,17 @@ namespace Content.Shared.Preferences
var name = GetName(species, gender); var name = GetName(species, gender);
return new HumanoidCharacterProfile(name, "", species, age, sex, gender, HumanoidCharacterAppearance.Random(species, sex), SpawnPriorityPreference.None, return new HumanoidCharacterProfile()
new Dictionary<string, JobPriority> {
{ Name = name,
{SharedGameTicker.FallbackOverflowJob, JobPriority.High}, Sex = sex,
}, PreferenceUnavailableMode.StayInLobby, new List<string>(), new List<string>(), new Dictionary<string, RoleLoadout>()); Age = age,
Gender = gender,
Species = species,
Appearance = HumanoidCharacterAppearance.Random(species, sex),
};
} }
public string Name { get; private set; }
public string FlavorText { get; private set; }
public string Species { get; private set; }
[DataField("age")]
public int Age { get; private set; }
[DataField("sex")]
public Sex Sex { get; private set; }
[DataField("gender")]
public Gender Gender { get; private set; }
public ICharacterAppearance CharacterAppearance => Appearance;
[DataField("appearance")]
public HumanoidCharacterAppearance Appearance { get; private set; }
public SpawnPriorityPreference SpawnPriority { get; private set; }
public IReadOnlyDictionary<string, JobPriority> JobPriorities => _jobPriorities;
public IReadOnlyList<string> AntagPreferences => _antagPreferences;
public IReadOnlyList<string> TraitPreferences => _traitPreferences;
public PreferenceUnavailableMode PreferenceUnavailable { get; private set; }
public HumanoidCharacterProfile WithName(string name) public HumanoidCharacterProfile WithName(string name)
{ {
return new(this) { Name = name }; return new(this) { Name = name };
@@ -274,7 +291,10 @@ namespace Content.Shared.Preferences
public HumanoidCharacterProfile WithJobPriorities(IEnumerable<KeyValuePair<string, JobPriority>> jobPriorities) public HumanoidCharacterProfile WithJobPriorities(IEnumerable<KeyValuePair<string, JobPriority>> jobPriorities)
{ {
return new(this, new Dictionary<string, JobPriority>(jobPriorities), _antagPreferences, _traitPreferences, _loadouts); return new(this)
{
_jobPriorities = new Dictionary<string, JobPriority>(jobPriorities),
};
} }
public HumanoidCharacterProfile WithJobPriority(string jobId, JobPriority priority) public HumanoidCharacterProfile WithJobPriority(string jobId, JobPriority priority)
@@ -288,7 +308,11 @@ namespace Content.Shared.Preferences
{ {
dictionary[jobId] = priority; dictionary[jobId] = priority;
} }
return new(this, dictionary, _antagPreferences, _traitPreferences, _loadouts);
return new(this)
{
_jobPriorities = dictionary,
};
} }
public HumanoidCharacterProfile WithPreferenceUnavailable(PreferenceUnavailableMode mode) public HumanoidCharacterProfile WithPreferenceUnavailable(PreferenceUnavailableMode mode)
@@ -298,50 +322,47 @@ namespace Content.Shared.Preferences
public HumanoidCharacterProfile WithAntagPreferences(IEnumerable<string> antagPreferences) public HumanoidCharacterProfile WithAntagPreferences(IEnumerable<string> antagPreferences)
{ {
return new(this, _jobPriorities, new List<string>(antagPreferences), _traitPreferences, _loadouts); return new(this)
{
_antagPreferences = new HashSet<string>(antagPreferences),
};
} }
public HumanoidCharacterProfile WithAntagPreference(string antagId, bool pref) public HumanoidCharacterProfile WithAntagPreference(string antagId, bool pref)
{ {
var list = new List<string>(_antagPreferences); var list = new HashSet<string>(_antagPreferences);
if (pref) if (pref)
{ {
if (!list.Contains(antagId)) list.Add(antagId);
{
list.Add(antagId);
}
} }
else else
{ {
if (list.Contains(antagId)) list.Remove(antagId);
{
list.Remove(antagId);
}
} }
return new(this, _jobPriorities, list, _traitPreferences, _loadouts); return new(this)
{
_antagPreferences = list,
};
} }
public HumanoidCharacterProfile WithTraitPreference(string traitId, bool pref) public HumanoidCharacterProfile WithTraitPreference(string traitId, bool pref)
{ {
var list = new List<string>(_traitPreferences); var list = new HashSet<string>(_traitPreferences);
// TODO: Maybe just refactor this to HashSet? Same with _antagPreferences
if (pref) if (pref)
{ {
if (!list.Contains(traitId)) list.Add(traitId);
{
list.Add(traitId);
}
} }
else else
{ {
if (list.Contains(traitId)) list.Remove(traitId);
{
list.Remove(traitId);
}
} }
return new(this, _jobPriorities, _antagPreferences, list, _loadouts);
return new(this)
{
_traitPreferences = list,
};
} }
public string Summary => public string Summary =>
@@ -498,10 +519,10 @@ namespace Content.Shared.Preferences
PreferenceUnavailable = prefsUnavailableMode; PreferenceUnavailable = prefsUnavailableMode;
_antagPreferences.Clear(); _antagPreferences.Clear();
_antagPreferences.AddRange(antags); _antagPreferences.UnionWith(antags);
_traitPreferences.Clear(); _traitPreferences.Clear();
_traitPreferences.AddRange(traits); _traitPreferences.UnionWith(traits);
// Checks prototypes exist for all loadouts and dump / set to default if not. // Checks prototypes exist for all loadouts and dump / set to default if not.
var toRemove = new ValueList<string>(); var toRemove = new ValueList<string>();
@@ -514,7 +535,7 @@ namespace Content.Shared.Preferences
continue; continue;
} }
loadouts.EnsureValid(session, collection); loadouts.EnsureValid(this, session, collection);
} }
foreach (var value in toRemove) foreach (var value in toRemove)
@@ -540,27 +561,26 @@ namespace Content.Shared.Preferences
public override bool Equals(object? obj) public override bool Equals(object? obj)
{ {
return obj is HumanoidCharacterProfile other && MemberwiseEquals(other); return ReferenceEquals(this, obj) || obj is HumanoidCharacterProfile other && Equals(other);
} }
public override int GetHashCode() public override int GetHashCode()
{ {
return HashCode.Combine( var hashCode = new HashCode();
HashCode.Combine( hashCode.Add(_jobPriorities);
Name, hashCode.Add(_antagPreferences);
Species, hashCode.Add(_traitPreferences);
Age, hashCode.Add(_loadouts);
Sex, hashCode.Add(Name);
Gender, hashCode.Add(FlavorText);
Appearance hashCode.Add(Species);
), hashCode.Add(Age);
SpawnPriority, hashCode.Add((int)Sex);
PreferenceUnavailable, hashCode.Add((int)Gender);
_jobPriorities, hashCode.Add(Appearance);
_antagPreferences, hashCode.Add((int)SpawnPriority);
_traitPreferences, hashCode.Add((int)PreferenceUnavailable);
_loadouts return hashCode.ToHashCode();
);
} }
public void SetLoadout(RoleLoadout loadout) public void SetLoadout(RoleLoadout loadout)
@@ -582,10 +602,12 @@ namespace Content.Shared.Preferences
} }
copied[loadout.Role] = loadout.Clone(); copied[loadout.Role] = loadout.Clone();
return new(this, _jobPriorities, _antagPreferences, _traitPreferences, copied); var profile = Clone();
profile._loadouts = copied;
return profile;
} }
public RoleLoadout GetLoadoutOrDefault(string id, IEntityManager entManager, IPrototypeManager protoManager) public RoleLoadout GetLoadoutOrDefault(string id, ProtoId<SpeciesPrototype>? species, IEntityManager entManager, IPrototypeManager protoManager)
{ {
if (!_loadouts.TryGetValue(id, out var loadout)) if (!_loadouts.TryGetValue(id, out var loadout))
{ {
@@ -596,5 +618,10 @@ namespace Content.Shared.Preferences
loadout.SetDefault(protoManager); loadout.SetDefault(protoManager);
return loadout; return loadout;
} }
public HumanoidCharacterProfile Clone()
{
return new HumanoidCharacterProfile(this);
}
} }
} }

View File

@@ -13,13 +13,13 @@ public sealed partial class GroupLoadoutEffect : LoadoutEffect
[DataField(required: true)] [DataField(required: true)]
public ProtoId<LoadoutEffectGroupPrototype> Proto; public ProtoId<LoadoutEffectGroupPrototype> Proto;
public override bool Validate(RoleLoadout loadout, ICommonSession session, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason) public override bool Validate(HumanoidCharacterProfile profile, RoleLoadout loadout, ICommonSession session, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason)
{ {
var effectsProto = collection.Resolve<IPrototypeManager>().Index(Proto); var effectsProto = collection.Resolve<IPrototypeManager>().Index(Proto);
foreach (var effect in effectsProto.Effects) foreach (var effect in effectsProto.Effects)
{ {
if (!effect.Validate(loadout, session, collection, out reason)) if (!effect.Validate(profile, loadout, session, collection, out reason))
return false; return false;
} }

View File

@@ -15,7 +15,7 @@ public sealed partial class JobRequirementLoadoutEffect : LoadoutEffect
[DataField(required: true)] [DataField(required: true)]
public JobRequirement Requirement = default!; public JobRequirement Requirement = default!;
public override bool Validate(RoleLoadout loadout, ICommonSession session, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason) public override bool Validate(HumanoidCharacterProfile profile, RoleLoadout loadout, ICommonSession session, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason)
{ {
var manager = collection.Resolve<ISharedPlaytimeManager>(); var manager = collection.Resolve<ISharedPlaytimeManager>();
var playtimes = manager.GetPlayTimes(session); var playtimes = manager.GetPlayTimes(session);

View File

@@ -11,6 +11,7 @@ public abstract partial class LoadoutEffect
/// Tries to validate the effect. /// Tries to validate the effect.
/// </summary> /// </summary>
public abstract bool Validate( public abstract bool Validate(
HumanoidCharacterProfile profile,
RoleLoadout loadout, RoleLoadout loadout,
ICommonSession session, ICommonSession session,
IDependencyCollection collection, IDependencyCollection collection,

View File

@@ -11,6 +11,7 @@ public sealed partial class PointsCostLoadoutEffect : LoadoutEffect
public int Cost = 1; public int Cost = 1;
public override bool Validate( public override bool Validate(
HumanoidCharacterProfile profile,
RoleLoadout loadout, RoleLoadout loadout,
ICommonSession session, ICommonSession session,
IDependencyCollection collection, IDependencyCollection collection,

View File

@@ -1,6 +1,26 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Humanoid.Prototypes;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Shared.Preferences.Loadouts.Effects; namespace Content.Shared.Preferences.Loadouts.Effects;
public sealed class SpeciesLoadoutEffect public sealed partial class SpeciesLoadoutEffect : LoadoutEffect
{ {
[DataField(required: true)]
public List<ProtoId<SpeciesPrototype>> Species = new();
public override bool Validate(HumanoidCharacterProfile profile, RoleLoadout loadout, ICommonSession session, IDependencyCollection collection,
[NotNullWhen(false)] out FormattedMessage? reason)
{
if (Species.Contains(profile.Species))
{
reason = null;
return true;
}
reason = FormattedMessage.FromUnformatted(Loc.GetString("loadout-group-species-restriction"));
return false;
}
} }

View File

@@ -6,8 +6,9 @@ namespace Content.Shared.Preferences.Loadouts;
/// <summary> /// <summary>
/// Specifies the selected prototype and custom data for a loadout. /// Specifies the selected prototype and custom data for a loadout.
/// </summary> /// </summary>
[Serializable, NetSerializable] [Serializable, NetSerializable, DataDefinition]
public sealed class Loadout public sealed partial class Loadout
{ {
[DataField]
public ProtoId<LoadoutPrototype> Prototype; public ProtoId<LoadoutPrototype> Prototype;
} }

View File

@@ -1,4 +1,6 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Random; using Content.Shared.Random;
using Robust.Shared.Collections; using Robust.Shared.Collections;
using Robust.Shared.Player; using Robust.Shared.Player;
@@ -11,11 +13,13 @@ namespace Content.Shared.Preferences.Loadouts;
/// <summary> /// <summary>
/// Contains all of the selected data for a role's loadout. /// Contains all of the selected data for a role's loadout.
/// </summary> /// </summary>
[Serializable, NetSerializable] [Serializable, NetSerializable, DataDefinition]
public sealed class RoleLoadout public sealed partial class RoleLoadout : IEquatable<RoleLoadout>
{ {
public readonly ProtoId<RoleLoadoutPrototype> Role; [DataField]
public ProtoId<RoleLoadoutPrototype> Role;
[DataField]
public Dictionary<ProtoId<LoadoutGroupPrototype>, List<Loadout>> SelectedLoadouts = new(); public Dictionary<ProtoId<LoadoutGroupPrototype>, List<Loadout>> SelectedLoadouts = new();
/* /*
@@ -44,7 +48,7 @@ public sealed class RoleLoadout
/// <summary> /// <summary>
/// Ensures all prototypes exist and effects can be applied. /// Ensures all prototypes exist and effects can be applied.
/// </summary> /// </summary>
public void EnsureValid(ICommonSession session, IDependencyCollection collection) public void EnsureValid(HumanoidCharacterProfile profile, ICommonSession session, IDependencyCollection collection)
{ {
var groupRemove = new ValueList<string>(); var groupRemove = new ValueList<string>();
var protoManager = collection.Resolve<IPrototypeManager>(); var protoManager = collection.Resolve<IPrototypeManager>();
@@ -81,7 +85,7 @@ public sealed class RoleLoadout
} }
// Validate the loadout can be applied (e.g. points). // Validate the loadout can be applied (e.g. points).
if (!IsValid(session, loadout.Prototype, collection, out _)) if (!IsValid(profile, session, loadout.Prototype, collection, out _))
{ {
loadouts.RemoveAt(i); loadouts.RemoveAt(i);
continue; continue;
@@ -167,7 +171,7 @@ public sealed class RoleLoadout
/// <summary> /// <summary>
/// Returns whether a loadout is valid or not. /// Returns whether a loadout is valid or not.
/// </summary> /// </summary>
public bool IsValid(ICommonSession session, ProtoId<LoadoutPrototype> loadout, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason) public bool IsValid(HumanoidCharacterProfile profile, ICommonSession session, ProtoId<LoadoutPrototype> loadout, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason)
{ {
reason = null; reason = null;
@@ -180,7 +184,7 @@ public sealed class RoleLoadout
return false; return false;
} }
if (!protoManager.TryIndex(Role, out var roleProto)) if (!protoManager.HasIndex(Role))
{ {
reason = FormattedMessage.FromUnformatted("loadouts-prototype-missing"); reason = FormattedMessage.FromUnformatted("loadouts-prototype-missing");
return false; return false;
@@ -190,7 +194,7 @@ public sealed class RoleLoadout
foreach (var effect in loadoutProto.Effects) foreach (var effect in loadoutProto.Effects)
{ {
valid = valid && effect.Validate(this, session, collection, out reason); valid = valid && effect.Validate(profile, this, session, collection, out reason);
} }
return valid; return valid;
@@ -257,4 +261,21 @@ public sealed class RoleLoadout
return false; return false;
} }
public bool Equals(RoleLoadout? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Role.Equals(other.Role) && SelectedLoadouts.SequenceEqual(other.SelectedLoadouts) && Points == other.Points;
}
public override bool Equals(object? obj)
{
return ReferenceEquals(this, obj) || obj is RoleLoadout other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(Role, SelectedLoadouts, Points);
}
} }

View File

@@ -1,7 +1,6 @@
character-setup-gui-character-setup-label = Character setup character-setup-gui-character-setup-label = Character setup
character-setup-gui-character-setup-stats-button = Stats character-setup-gui-character-setup-stats-button = Stats
character-setup-gui-character-setup-rules-button = Rules character-setup-gui-character-setup-rules-button = Rules
character-setup-gui-character-setup-save-button = Save
character-setup-gui-character-setup-close-button = Close character-setup-gui-character-setup-close-button = Close
character-setup-gui-create-new-character-button = Create new slot... character-setup-gui-create-new-character-button = Create new slot...
character-setup-gui-create-new-character-button-tooltip = A maximum of {$maxCharacters} characters are allowed. character-setup-gui-create-new-character-button-tooltip = A maximum of {$maxCharacters} characters are allowed.

View File

@@ -19,6 +19,7 @@ humanoid-profile-editor-pronouns-neuter-text = It / It
humanoid-profile-editor-import-button = Import humanoid-profile-editor-import-button = Import
humanoid-profile-editor-export-button = Export humanoid-profile-editor-export-button = Export
humanoid-profile-editor-save-button = Save humanoid-profile-editor-save-button = Save
humanoid-profile-editor-reset-button = Reset
humanoid-profile-editor-spawn-priority-label = Spawn priority: humanoid-profile-editor-spawn-priority-label = Spawn priority:
humanoid-profile-editor-eyes-label = Eye color: humanoid-profile-editor-eyes-label = Eye color:
humanoid-profile-editor-jobs-tab = Jobs humanoid-profile-editor-jobs-tab = Jobs

View File

@@ -1,7 +1,7 @@
- type: species - type: species
id: Vox id: Vox
name: species-name-vox name: species-name-vox
roundStart: false # sad... roundStart: false # sad
prototype: MobVox prototype: MobVox
sprites: MobVoxSprites sprites: MobVoxSprites
markingLimits: MobVoxMarkingLimits markingLimits: MobVoxMarkingLimits