using System.Linq; using Content.Client.Guidebook; using Content.Client.Humanoid; using Content.Client.Inventory; using Content.Client.Lobby.UI; using Content.Client.Players.PlayTimeTracking; using Content.Client.Station; using Content.Shared.CCVar; using Content.Shared.Clothing; using Content.Shared.GameTicking; using Content.Shared.Humanoid; using Content.Shared.Humanoid.Markings; using Content.Shared.Humanoid.Prototypes; using Content.Shared.Preferences; using Content.Shared.Preferences.Loadouts; using Content.Shared.Roles; using Content.Shared.Traits; using Robust.Client.Player; using Robust.Client.ResourceManagement; using Robust.Client.State; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controllers; using Robust.Shared.Configuration; using Robust.Shared.Map; using Robust.Shared.Prototypes; using Robust.Shared.Utility; namespace Content.Client.Lobby; public sealed class LobbyUIController : UIController, IOnStateEntered, IOnStateExited { [Dependency] private readonly IClientPreferencesManager _preferencesManager = 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 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 ClientInventorySystem _inventory = default!; [UISystemDependency] private readonly StationSpawningSystem _spawn = default!; [UISystemDependency] private readonly GuidebookSystem _guide = default!; [UISystemDependency] private readonly LoadoutSystem _loadouts = default!; private CharacterSetupGui? _characterSetup; private HumanoidProfileEditor? _profileEditor; private CharacterSetupGuiSavePanel? _savePanel; /// /// This is the characher preview panel in the chat. This should only update if their character updates. /// private LobbyCharacterPreviewPanel? PreviewPanel => GetLobbyPreview(); /// /// This is the modified profile currently being edited. /// private HumanoidCharacterProfile? EditedProfile => _profileEditor?.Profile; private int? EditedSlot => _profileEditor?.CharacterSlot; public override void Initialize() { base.Initialize(); _prototypeManager.PrototypesReloaded += OnProtoReload; _preferencesManager.OnServerDataLoaded += PreferencesDataLoaded; _requirements.Updated += OnRequirementsUpdated; _configurationManager.OnValueChanged(CCVars.FlavorText, args => { _profileEditor?.RefreshFlavorText(); }); _configurationManager.OnValueChanged(CCVars.GameRoleTimers, _ => RefreshProfileEditor()); _configurationManager.OnValueChanged(CCVars.GameRoleWhitelist, _ => RefreshProfileEditor()); } 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()) { _profileEditor.RefreshAntags(); } if (obj.WasModified() || obj.WasModified()) { _profileEditor.RefreshJobs(); } if (obj.WasModified() || obj.WasModified() || obj.WasModified()) { _profileEditor.RefreshLoadouts(); } if (obj.WasModified()) { _profileEditor.RefreshSpecies(); } if (obj.WasModified()) { _profileEditor.RefreshTraits(); } } } private void PreferencesDataLoaded() { PreviewPanel?.SetLoaded(true); if (_stateManager.CurrentState is not LobbyState) return; ReloadCharacterSetup(); } public void OnStateEntered(LobbyState state) { PreviewPanel?.SetLoaded(_preferencesManager.ServerDataLoaded); ReloadCharacterSetup(); } public void OnStateExited(LobbyState state) { PreviewPanel?.SetLoaded(false); _profileEditor?.Dispose(); _characterSetup?.Dispose(); _characterSetup = null; _profileEditor = null; } /// /// Reloads every single character setup control. /// public void ReloadCharacterSetup() { RefreshLobbyPreview(); var (characterGui, profileEditor) = EnsureGui(); characterGui.ReloadCharacterPickers(); profileEditor.SetProfile( (HumanoidCharacterProfile?) _preferencesManager.Preferences?.SelectedCharacter, _preferencesManager.Preferences?.SelectedCharacterIndex); } /// /// Refreshes the character preview in the lobby chat. /// 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 RefreshProfileEditor() { _profileEditor?.RefreshAntags(); _profileEditor?.RefreshJobs(); _profileEditor?.RefreshLoadouts(); } 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 void CloseProfileEditor() { if (_profileEditor == null) return; _profileEditor.SetProfile(null, null); _profileEditor.Visible = false; if (_stateManager.CurrentState is LobbyState lobbyGui) { lobbyGui.SwitchState(LobbyGui.LobbyGuiState.Default); } } private void OpenSavePanel() { if (_savePanel is { IsOpen: true }) return; _savePanel = new CharacterSetupGuiSavePanel(); _savePanel.SaveButton.OnPressed += _ => { SaveProfile(); _savePanel.Close(); CloseProfileEditor(); }; _savePanel.NoSaveButton.OnPressed += _ => { _savePanel.Close(); CloseProfileEditor(); }; _savePanel.OpenCentered(); } 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, _resourceCache, _requirements, _markings); _profileEditor.OnOpenGuidebook += _guide.OpenHelp; _characterSetup = new CharacterSetupGui(_profileEditor); _characterSetup.CloseButton.OnPressed += _ => { // Open the save panel if we have unsaved changes. if (_profileEditor.Profile != null && _profileEditor.IsDirty) { OpenSavePanel(); return; } // Reset sliders etc. CloseProfileEditor(); }; _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 /// /// Applies the highest priority job's clothes to the dummy. /// public void GiveDummyJobClothesLoadout(EntityUid dummy, JobPrototype? jobProto, HumanoidCharacterProfile profile) { var job = jobProto ?? GetPreferredJob(profile); GiveDummyJobClothes(dummy, profile, job); if (_prototypeManager.HasIndex(LoadoutSystem.GetJobPrototype(job.ID))) { var loadout = profile.GetLoadoutOrDefault(LoadoutSystem.GetJobPrototype(job.ID), _playerManager.LocalSession, profile.Species, EntityManager, _prototypeManager); GiveDummyLoadout(dummy, loadout); } } /// /// Gets the highest priority job for the profile. /// public JobPrototype GetPreferredJob(HumanoidCharacterProfile profile) { var highPriorityJob = profile.JobPriorities.FirstOrDefault(p => p.Value == JobPriority.High).Key; // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract (what is resharper smoking?) return _prototypeManager.Index(highPriorityJob.Id ?? SharedGameTicker.FallbackOverflowJob); } public void GiveDummyLoadout(EntityUid uid, RoleLoadout? roleLoadout) { if (roleLoadout == null) return; foreach (var group in roleLoadout.SelectedLoadouts.Values) { foreach (var loadout in group) { if (!_prototypeManager.TryIndex(loadout.Prototype, out var loadoutProto)) continue; _spawn.EquipStartingGear(uid, loadoutProto); } } } /// /// Applies the specified job's clothes to the dummy. /// public void GiveDummyJobClothes(EntityUid dummy, HumanoidCharacterProfile profile, JobPrototype job) { if (!_inventory.TryGetSlots(dummy, out var slots)) return; // Apply loadout if (profile.Loadouts.TryGetValue(job.ID, out var jobLoadout)) { foreach (var loadouts in jobLoadout.SelectedLoadouts.Values) { foreach (var loadout in loadouts) { if (!_prototypeManager.TryIndex(loadout.Prototype, out var loadoutProto)) continue; // TODO: Need some way to apply starting gear to an entity and replace existing stuff coz holy fucking shit dude. foreach (var slot in slots) { // Try startinggear first if (_prototypeManager.TryIndex(loadoutProto.StartingGear, out var loadoutGear)) { var itemType = ((IEquipmentLoadout) loadoutGear).GetGear(slot.Name); if (_inventory.TryUnequip(dummy, slot.Name, out var unequippedItem, silent: true, force: true, reparent: false)) { EntityManager.DeleteEntity(unequippedItem.Value); } if (itemType != string.Empty) { var item = EntityManager.SpawnEntity(itemType, MapCoordinates.Nullspace); _inventory.TryEquip(dummy, item, slot.Name, true, true); } } else { var itemType = ((IEquipmentLoadout) loadoutProto).GetGear(slot.Name); if (_inventory.TryUnequip(dummy, slot.Name, out var unequippedItem, silent: true, force: true, reparent: false)) { EntityManager.DeleteEntity(unequippedItem.Value); } if (itemType != string.Empty) { var item = EntityManager.SpawnEntity(itemType, MapCoordinates.Nullspace); _inventory.TryEquip(dummy, item, slot.Name, true, true); } } } } } } if (!_prototypeManager.TryIndex(job.StartingGear, out var gear)) return; foreach (var slot in slots) { var itemType = ((IEquipmentLoadout) gear).GetGear(slot.Name); if (_inventory.TryUnequip(dummy, slot.Name, out var unequippedItem, silent: true, force: true, reparent: false)) { EntityManager.DeleteEntity(unequippedItem.Value); } if (itemType != string.Empty) { var item = EntityManager.SpawnEntity(itemType, MapCoordinates.Nullspace); _inventory.TryEquip(dummy, item, slot.Name, true, true); } } } /// /// Loads the profile onto a dummy entity. /// public EntityUid LoadProfileEntity(HumanoidCharacterProfile? humanoid, JobPrototype? job, bool jobClothes) { EntityUid dummyEnt; if (humanoid is not null) { var dummy = _prototypeManager.Index(humanoid.Species).DollPrototype; dummyEnt = EntityManager.SpawnEntity(dummy, MapCoordinates.Nullspace); } else { dummyEnt = EntityManager.SpawnEntity(_prototypeManager.Index(SharedHumanoidAppearanceSystem.DefaultSpecies).DollPrototype, MapCoordinates.Nullspace); } _humanoid.LoadProfile(dummyEnt, humanoid); if (humanoid != null && jobClothes) { job ??= GetPreferredJob(humanoid); GiveDummyJobClothes(dummyEnt, humanoid, job); if (_prototypeManager.HasIndex(LoadoutSystem.GetJobPrototype(job.ID))) { var loadout = humanoid.GetLoadoutOrDefault(LoadoutSystem.GetJobPrototype(job.ID), _playerManager.LocalSession, humanoid.Species, EntityManager, _prototypeManager); GiveDummyLoadout(dummyEnt, loadout); } } return dummyEnt; } #endregion }