diff --git a/Content.Client/Administration/UI/CustomControls/HSeparator.cs b/Content.Client/Administration/UI/CustomControls/HSeparator.cs index 2dfb0b27fe8..5e99771d9bd 100644 --- a/Content.Client/Administration/UI/CustomControls/HSeparator.cs +++ b/Content.Client/Administration/UI/CustomControls/HSeparator.cs @@ -5,21 +5,53 @@ namespace Content.Client.Administration.UI.CustomControls; +/** + * FRONTIER CHANGE: Added SeparatorColor so it can be set in the UI. + */ public sealed class HSeparator : Control { - private static readonly Color SeparatorColor = Color.FromHex("#3D4059"); + private static readonly Color DefaultSeparatorColor = Color.FromHex("#3D4059"); + + private Color _separatorColor = DefaultSeparatorColor; + public Color SeparatorColor + { + get => _separatorColor; + set + { + _separatorColor = Color.FromHex(value.ToHex()); + UpdateSeparatorColor(); + } + } + + private PanelContainer? _panelContainer = null; public HSeparator(Color color) { - AddChild(new PanelContainer + SeparatorColor = color; + Initialize(); + } + + public HSeparator() : this(DefaultSeparatorColor) { } + + private void Initialize() + { + _panelContainer = new PanelContainer { PanelOverride = new StyleBoxFlat { - BackgroundColor = color, - ContentMarginBottomOverride = 2, ContentMarginLeftOverride = 2 + BackgroundColor = SeparatorColor, + ContentMarginBottomOverride = 2, + ContentMarginLeftOverride = 2 } - }); + }; + AddChild(_panelContainer); } - public HSeparator() : this(SeparatorColor) { } + private void UpdateSeparatorColor() + { + if (_panelContainer?.PanelOverride is StyleBoxFlat styleBox) + { + styleBox.BackgroundColor = SeparatorColor; + } + } } diff --git a/Content.Client/GameTicking/Managers/ClientGameTicker.cs b/Content.Client/GameTicking/Managers/ClientGameTicker.cs index fcf5ae91a49..d82724e6c7b 100644 --- a/Content.Client/GameTicking/Managers/ClientGameTicker.cs +++ b/Content.Client/GameTicking/Managers/ClientGameTicker.cs @@ -1,15 +1,14 @@ +using System.Linq; using Content.Client.Administration.Managers; using Content.Client.Gameplay; using Content.Client.Lobby; using Content.Client.RoundEnd; using Content.Shared.GameTicking; using Content.Shared.GameWindow; -using Content.Shared.Roles; using JetBrains.Annotations; using Robust.Client.Graphics; using Robust.Client.State; using Robust.Client.UserInterface; -using Robust.Shared.Prototypes; namespace Content.Client.GameTicking.Managers { @@ -21,8 +20,7 @@ public sealed class ClientGameTicker : SharedGameTicker [Dependency] private readonly IClyde _clyde = default!; [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!; - private Dictionary, int?>> _jobsAvailable = new(); - private Dictionary _stationNames = new(); + private Dictionary _stationJobInformationList = new(); [ViewVariables] public bool AreWeReady { get; private set; } [ViewVariables] public bool IsGameStarted { get; private set; } @@ -33,13 +31,21 @@ public sealed class ClientGameTicker : SharedGameTicker [ViewVariables] public TimeSpan StartTime { get; private set; } [ViewVariables] public new bool Paused { get; private set; } - [ViewVariables] public IReadOnlyDictionary, int?>> JobsAvailable => _jobsAvailable; - [ViewVariables] public IReadOnlyDictionary StationNames => _stationNames; + [ViewVariables] public IReadOnlyDictionary StationJobInformationList => _stationJobInformationList; + + // Frontier addition + // Replaced StationNames with a getter that uses _stationJobInformationList + [ViewVariables] + public IReadOnlyDictionary StationNames => + _stationJobInformationList.ToDictionary( + kvp => kvp.Key, + kvp => kvp.Value.StationName + ); public event Action? InfoBlobUpdated; public event Action? LobbyStatusUpdated; public event Action? LobbyLateJoinStatusUpdated; - public event Action, int?>>>? LobbyJobsAvailableUpdated; + public event Action>? LobbyJobsAvailableUpdated; public override void Initialize() { @@ -87,20 +93,9 @@ private void LateJoinStatus(TickerLateJoinStatusEvent message) private void UpdateJobsAvailable(TickerJobsAvailableEvent message) { - _jobsAvailable.Clear(); - - foreach (var (job, data) in message.JobsAvailableByStation) - { - _jobsAvailable[job] = data; - } - - _stationNames.Clear(); - foreach (var weh in message.StationNames) - { - _stationNames[weh.Key] = weh.Value; - } - - LobbyJobsAvailableUpdated?.Invoke(JobsAvailable); + _stationJobInformationList.Clear(); + _stationJobInformationList = message.StationJobList; + LobbyJobsAvailableUpdated?.Invoke(StationJobInformationList); } private void JoinLobby(TickerJoinLobbyEvent message) diff --git a/Content.Client/LateJoin/LateJoinGui.cs b/Content.Client/LateJoin/LateJoinGui.cs index 9e523364c29..e33f433052b 100644 --- a/Content.Client/LateJoin/LateJoinGui.cs +++ b/Content.Client/LateJoin/LateJoinGui.cs @@ -6,6 +6,7 @@ using Content.Client.UserInterface.Controls; using Content.Client.Players.PlayTimeTracking; using Content.Shared.CCVar; +using Content.Shared.GameTicking; using Content.Shared.Preferences; using Content.Shared.Roles; using Content.Shared.StatusIcon; @@ -171,12 +172,12 @@ private void RebuildUI() { var departmentName = Loc.GetString($"department-{department.ID}"); _jobCategories[id] = new Dictionary(); - var stationAvailable = _gameTicker.JobsAvailable[id]; + var stationAvailable = _gameTicker.StationJobInformationList[id]; var jobsAvailable = new List(); foreach (var jobId in department.Roles) { - if (!stationAvailable.ContainsKey(jobId)) + if (!stationAvailable.JobsAvailable.ContainsKey(jobId)) continue; jobsAvailable.Add(_prototypeManager.Index(jobId)); @@ -225,7 +226,8 @@ private void RebuildUI() foreach (var prototype in jobsAvailable) { - var value = stationAvailable[prototype.ID]; + // Frontier: stationAvailable[prototype.ID]; -> stationAvailable.JobsAvailable[prototype.ID]; + var value = stationAvailable.JobsAvailable[prototype.ID]; var jobLabel = new Label { @@ -292,29 +294,25 @@ private void RebuildUI() } } - private void JobsAvailableUpdated(IReadOnlyDictionary, int?>> updatedJobs) + private void JobsAvailableUpdated(IReadOnlyDictionary updatedJobs) { - foreach (var stationEntries in updatedJobs) + // Frontier: Made this more readable with simplified comparisons and LINQ expressions. + // Feel free to replace this with upstream code whenever, just mind that + // updatedJobs is now a dictionary of NetEntity to StationJobInformation. + // I changed this: jobInformation.TryGetValue to this: jobInformation.JobsAvailable.TryGetValue + // Godspeed. + foreach (var (stationNetEntity, jobInformation) in updatedJobs) { - if (_jobButtons.ContainsKey(stationEntries.Key)) + if (!_jobButtons.TryGetValue(stationNetEntity, out var existingJobEntries)) + continue; + foreach (var existingJobEntry in existingJobEntries) { - var jobsAvailable = stationEntries.Value; - - var existingJobEntries = _jobButtons[stationEntries.Key]; - foreach (var existingJobEntry in existingJobEntries) + if (!jobInformation.JobsAvailable.TryGetValue(existingJobEntry.Key, out var updatedJobValue)) + continue; + foreach (var matchingJobButton in existingJobEntry.Value.Where(matchingJobButton => matchingJobButton.Amount != updatedJobValue)) { - if (jobsAvailable.ContainsKey(existingJobEntry.Key)) - { - var updatedJobValue = jobsAvailable[existingJobEntry.Key]; - foreach (var matchingJobButton in existingJobEntry.Value) - { - if (matchingJobButton.Amount != updatedJobValue) - { - matchingJobButton.RefreshLabel(updatedJobValue); - matchingJobButton.Disabled |= matchingJobButton.Amount == 0; - } - } - } + matchingJobButton.RefreshLabel(updatedJobValue); + matchingJobButton.Disabled |= matchingJobButton.Amount == 0; } } } diff --git a/Content.Client/Lobby/LobbyState.cs b/Content.Client/Lobby/LobbyState.cs index f9aaad27a8f..d03cc0a725a 100644 --- a/Content.Client/Lobby/LobbyState.cs +++ b/Content.Client/Lobby/LobbyState.cs @@ -1,8 +1,7 @@ -using Content.Client._NF.Latejoin; -using Content.Client.Chat.Managers; +using Content.Client._NF.LateJoin; using Content.Client.Audio; +using Content.Client.Eui; using Content.Client.GameTicking.Managers; -using Content.Client.LateJoin; using Content.Client.Lobby.UI; using Content.Client.Message; using Content.Client.UserInterface.Systems.Chat; @@ -13,6 +12,7 @@ using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.Timing; +using PickerWindow = Content.Client._NF.LateJoin.Windows.PickerWindow; namespace Content.Client.Lobby @@ -33,6 +33,9 @@ public sealed class LobbyState : Robust.Client.State.State protected override Type? LinkedScreenType { get; } = typeof(LobbyGui); public LobbyGui? Lobby; + // Frontier - save pickerwindow so it opens only once + private PickerWindow? _pickerWindow = null; + protected override void Startup() { if (_userInterfaceManager.ActiveScreen == null) @@ -99,8 +102,10 @@ private void OnReadyPressed(BaseButton.ButtonEventArgs args) { return; } - - new NFLateJoinGui().OpenCentered(); + // Frontier to downstream: if you want to skip the first window and go straight to station picker, + // simply change the enum to station or crew in the PickerWindow constructor. + _pickerWindow ??= new PickerWindow(); + _pickerWindow.OpenCentered(); } private void OnReadyToggled(BaseButton.ButtonToggledEventArgs args) diff --git a/Content.Client/_NF/LateJoin/Controls/CrewPickerControl.xaml b/Content.Client/_NF/LateJoin/Controls/CrewPickerControl.xaml new file mode 100644 index 00000000000..0fc1fc1c2fd --- /dev/null +++ b/Content.Client/_NF/LateJoin/Controls/CrewPickerControl.xaml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Content.Client/_NF/LateJoin/Controls/CrewPickerControl.xaml.cs b/Content.Client/_NF/LateJoin/Controls/CrewPickerControl.xaml.cs new file mode 100644 index 00000000000..31ff5a851e8 --- /dev/null +++ b/Content.Client/_NF/LateJoin/Controls/CrewPickerControl.xaml.cs @@ -0,0 +1,158 @@ +using System.Linq; +using Content.Client._NF.LateJoin.Interfaces; +using Content.Client._NF.LateJoin.ListItems; +using Content.Client.Lobby; +using Content.Client.Players.PlayTimeTracking; +using Content.Shared.GameTicking; +using Content.Shared.Preferences; +using Robust.Client.AutoGenerated; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Prototypes; + +namespace Content.Client._NF.LateJoin.Controls; + +[GenerateTypedNameReferences] +public sealed partial class CrewPickerControl : PickerControl +{ + [Dependency] private readonly ILocalizationManager _loc = default!; + [Dependency] private readonly IEntitySystemManager _entitySystem = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly JobRequirementsManager _jobReqs = default!; + [Dependency] private readonly IClientPreferencesManager _preferencesManager = default!; + private readonly SpriteSystem _spriteSystem; + + public CrewPickerControl() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + _spriteSystem = _entitySystem.GetEntitySystem(); + } + + private Dictionary _lobbyJobs = new(); + private CrewListItem.ViewState? _lastSelectedStation; + public Action? OnJobJoined; + + public override void UpdateUi(IReadOnlyDictionary obj) + { + _lobbyJobs = new Dictionary(obj); + StationItemList.RemoveAllChildren(); + + foreach (var stationViewState in BuildStationViewStateList(_lobbyJobs)) + { + var item = new CrewListItem(stationViewState); + item.StationButton.OnPressed += _ => OnStationPressed(stationViewState); + StationItemList.AddChild(item); + } + + // Build station jobs, the right section of the screen. + StationJobItemList.RemoveAllChildren(); + foreach (var jobViewState in BuildJobViewStateList(obj[_lastSelectedStation!.StationEntity])) + { + var item = new JobListItem(jobViewState); + item.OnPressed += _ => + { + OnJobJoined?.Invoke(_lastSelectedStation.StationEntity, jobViewState.JobId); + }; + StationJobItemList.AddChild(item); + } + + StationName.Text = _lastSelectedStation?.StationName ?? ""; + StationDescription.Text = _lastSelectedStation?.StationDescription ?? ""; + } + + private void OnStationPressed(CrewListItem.ViewState stationItemViewState) + { + _lastSelectedStation = stationItemViewState; + UpdateUi(_lobbyJobs); + } + + private List BuildJobViewStateList(StationJobInformation jobInformation) + { + var viewStateList = new List(); + + foreach (var (jobPrototype, jobCount) in jobInformation.JobsAvailable) + { + if (_preferencesManager.Preferences?.SelectedCharacter is not HumanoidCharacterProfile profile) + { + continue; + } + + var prototype = _prototypeManager.Index(jobPrototype); + var jobName = prototype.LocalizedName + jobCount.WrapJobCountInParentheses(); + Texture? texture = null; + + if (_prototypeManager.TryIndex(prototype.Icon, out var jobIcon)) + { + texture = _spriteSystem.Frame0(jobIcon.Icon); + } + + var buttonTooltip = ""; + if (!_jobReqs.IsAllowed(prototype, profile, out var denyReason)) + { + buttonTooltip = denyReason.ToString(); + } + + var isButtonDisabled = jobCount == 0 || !_jobReqs.IsAllowed(prototype, profile, out _); + var viewState = new JobListItem.ViewState( + jobId: jobPrototype, + jobName: jobName, + toolTip: buttonTooltip, + disabled: isButtonDisabled, + jobIcon: texture + ); + viewStateList.Add(viewState); + } + + return viewStateList; + } + + /** + * Convert some raw dictionary data to a view state model that is more readable. + * + * @param obj Dictionary of station entities to job prototypes. + * @param stationNames Dictionary of station entities to station names. + * @return List of view states for each station. + */ + private List BuildStationViewStateList( + IReadOnlyDictionary obj) + { + var stationList = obj.Where(kvp => !kvp.Value.IsLateJoinStation).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + var viewStateList = new List(); + + foreach (var (stationEntity, stationJobInformation) in stationList) + { + var viewState = new CrewListItem.ViewState( + stationEntity, + stationJobInformation.GetStationNameWithJobCount(), + stationJobInformation.StationSubtext != null + ? _loc.GetString(stationJobInformation.StationSubtext) + : "", + stationJobInformation.StationDescription != null + ? _loc.GetString(stationJobInformation.StationDescription) + : "", + _lastSelectedStation?.StationEntity == stationEntity, + stationJobInformation.StationIcon?.CanonPath + ); + + // Always select the first station in the list if none is selected yet. + // This is because otherwise the right side of the screen would then be a blank space. + if (_lastSelectedStation == null) + { + _lastSelectedStation = viewState; + viewState.Selected = true; + } + + viewStateList.Add(viewState); + } + + // Sort 0 to the end of the list in the order it is in the dictionary. + // Sort 1 first, 2 second, etc. + return viewStateList + .OrderBy(viewState => obj[viewState.StationEntity].LobbySortOrder == 0 + ? int.MaxValue + : obj[viewState.StationEntity].LobbySortOrder) + .ToList(); + } +} diff --git a/Content.Client/_NF/LateJoin/Controls/StationOrCrewLargeControl.xaml b/Content.Client/_NF/LateJoin/Controls/StationOrCrewLargeControl.xaml new file mode 100644 index 00000000000..98715b770f2 --- /dev/null +++ b/Content.Client/_NF/LateJoin/Controls/StationOrCrewLargeControl.xaml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + diff --git a/Content.Client/_NF/LateJoin/Controls/StationOrCrewLargeControl.xaml.cs b/Content.Client/_NF/LateJoin/Controls/StationOrCrewLargeControl.xaml.cs new file mode 100644 index 00000000000..3c9a34c6bd2 --- /dev/null +++ b/Content.Client/_NF/LateJoin/Controls/StationOrCrewLargeControl.xaml.cs @@ -0,0 +1,53 @@ +using Content.Client._NF.LateJoin.Interfaces; +using Content.Client.GameTicking.Managers; +using Content.Shared.GameTicking; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client._NF.LateJoin.Controls; + +[GenerateTypedNameReferences] +public sealed partial class StationOrCrewLargeControl : PickerControl +{ + [Dependency] private readonly IEntitySystemManager _entitySystem = default!; + private readonly ClientGameTicker _gameTicker; + + public Action? OnTabChange; + + public StationOrCrewLargeControl() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + _gameTicker = _entitySystem.GetEntitySystem(); + _gameTicker.LobbyJobsAvailableUpdated += UpdateUi; + + StationButton.OnPressed += StationButtonOnOnPressed; + CrewButton.OnPressed += CrewButtonOnOnPressed; + UpdateUi(_gameTicker.StationJobInformationList); + } + + public override void UpdateUi(IReadOnlyDictionary obj) + { + StationButton.Disabled = !StationJobInformationExtensions.IsAnyStationAvailable(obj); + NoStationsAvailableLabel.Visible = !StationJobInformationExtensions.IsAnyStationAvailable(obj); + CrewButton.Disabled = !StationJobInformationExtensions.IsAnyCrewJobAvailable(obj); + NoCrewsAvailableLabel.Visible = !StationJobInformationExtensions.IsAnyCrewJobAvailable(obj); + } + + protected override void ExitedTree() + { + base.ExitedTree(); + _gameTicker.LobbyJobsAvailableUpdated -= UpdateUi; + } + + private void StationButtonOnOnPressed(BaseButton.ButtonEventArgs obj) + { + OnTabChange?.Invoke(Windows.PickerWindow.PickerType.Station); + } + + private void CrewButtonOnOnPressed(BaseButton.ButtonEventArgs obj) + { + OnTabChange?.Invoke(Windows.PickerWindow.PickerType.Crew); + } +} diff --git a/Content.Client/_NF/LateJoin/Controls/StationPickerControl.xaml b/Content.Client/_NF/LateJoin/Controls/StationPickerControl.xaml new file mode 100644 index 00000000000..7a3714fe76d --- /dev/null +++ b/Content.Client/_NF/LateJoin/Controls/StationPickerControl.xaml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Content.Client/_NF/LateJoin/Controls/StationPickerControl.xaml.cs b/Content.Client/_NF/LateJoin/Controls/StationPickerControl.xaml.cs new file mode 100644 index 00000000000..cb198909227 --- /dev/null +++ b/Content.Client/_NF/LateJoin/Controls/StationPickerControl.xaml.cs @@ -0,0 +1,158 @@ +using System.Linq; +using Content.Client._NF.LateJoin.Interfaces; +using Content.Client._NF.LateJoin.ListItems; +using Content.Client.Lobby; +using Content.Client.Players.PlayTimeTracking; +using Content.Shared.GameTicking; +using Content.Shared.Preferences; +using Robust.Client.AutoGenerated; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Prototypes; + +namespace Content.Client._NF.LateJoin.Controls; + +[GenerateTypedNameReferences] +public sealed partial class StationPickerControl : PickerControl +{ + [Dependency] private readonly ILocalizationManager _loc = default!; + [Dependency] private readonly IEntitySystemManager _entitySystem = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly JobRequirementsManager _jobReqs = default!; + [Dependency] private readonly IClientPreferencesManager _preferencesManager = default!; + private readonly SpriteSystem _spriteSystem; + + public StationPickerControl() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + _spriteSystem = _entitySystem.GetEntitySystem(); + } + + private Dictionary _lobbyJobs = new(); + private StationListItem.ViewState? _lastSelectedStation; + public Action? OnJobJoined; + + public override void UpdateUi(IReadOnlyDictionary obj) + { + _lobbyJobs = new Dictionary(obj); + StationItemList.RemoveAllChildren(); + + foreach (var stationViewState in BuildStationViewStateList(_lobbyJobs)) + { + var item = new StationListItem(stationViewState); + item.StationButton.OnPressed += _ => OnStationPressed(stationViewState); + StationItemList.AddChild(item); + } + + // Build station jobs, the right section of the screen. + StationJobItemList.RemoveAllChildren(); + foreach (var jobViewState in BuildJobViewStateList(obj[_lastSelectedStation!.StationEntity])) + { + var item = new JobListItem(jobViewState); + item.OnPressed += _ => + { + OnJobJoined?.Invoke(_lastSelectedStation.StationEntity, jobViewState.JobId); + }; + StationJobItemList.AddChild(item); + } + + StationName.Text = _lastSelectedStation?.StationName ?? ""; + StationDescription.Text = _lastSelectedStation?.StationDescription ?? ""; + } + + private void OnStationPressed(StationListItem.ViewState stationItemViewState) + { + _lastSelectedStation = stationItemViewState; + UpdateUi(_lobbyJobs); + } + + private List BuildJobViewStateList(StationJobInformation jobInformation) + { + var viewStateList = new List(); + + foreach (var (jobPrototype, jobCount) in jobInformation.JobsAvailable) + { + if (_preferencesManager.Preferences?.SelectedCharacter is not HumanoidCharacterProfile profile) + { + continue; + } + + var prototype = _prototypeManager.Index(jobPrototype); + var jobName = prototype.LocalizedName + jobCount.WrapJobCountInParentheses(); + Texture? texture = null; + + if (_prototypeManager.TryIndex(prototype.Icon, out var jobIcon)) + { + texture = _spriteSystem.Frame0(jobIcon.Icon); + } + + var buttonTooltip = ""; + if (!_jobReqs.IsAllowed(prototype, profile, out var denyReason)) + { + buttonTooltip = denyReason.ToString(); + } + + var isButtonDisabled = jobCount == 0 || !_jobReqs.IsAllowed(prototype, profile, out _); + var viewState = new JobListItem.ViewState( + jobId: jobPrototype, + jobName: jobName, + toolTip: buttonTooltip, + disabled: isButtonDisabled, + jobIcon: texture + ); + viewStateList.Add(viewState); + } + + return viewStateList; + } + + /** + * Convert some raw dictionary data to a view state model that is more readable. + * + * @param obj Dictionary of station entities to job prototypes. + * @param stationNames Dictionary of station entities to station names. + * @return List of view states for each station. + */ + private List BuildStationViewStateList( + IReadOnlyDictionary obj) + { + var stationList = obj.Where(kvp => kvp.Value.IsLateJoinStation).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + var viewStateList = new List(); + + foreach (var (stationEntity, stationJobInformation) in stationList) + { + var viewState = new StationListItem.ViewState( + stationEntity, + stationJobInformation.GetStationNameWithJobCount(), + stationJobInformation.StationSubtext != null + ? _loc.GetString(stationJobInformation.StationSubtext) + : "", + stationJobInformation.StationDescription != null + ? _loc.GetString(stationJobInformation.StationDescription) + : "", + _lastSelectedStation?.StationEntity == stationEntity, + stationJobInformation.StationIcon?.CanonPath + ); + + // Always select the first station in the list if none is selected yet. + // This is because otherwise the right side of the screen would then be a blank space. + if (_lastSelectedStation == null) + { + _lastSelectedStation = viewState; + viewState.Selected = true; + } + + viewStateList.Add(viewState); + } + + // Sort 0 to the end of the list in the order it is in the dictionary. + // Sort 1 first, 2 second, etc. + return viewStateList + .OrderBy(viewState => obj[viewState.StationEntity].LobbySortOrder == 0 + ? int.MaxValue + : obj[viewState.StationEntity].LobbySortOrder) + .ToList(); + } +} diff --git a/Content.Client/_NF/LateJoin/Extensions/StationJobInformationExtensions.cs b/Content.Client/_NF/LateJoin/Extensions/StationJobInformationExtensions.cs new file mode 100644 index 00000000000..0466622160a --- /dev/null +++ b/Content.Client/_NF/LateJoin/Extensions/StationJobInformationExtensions.cs @@ -0,0 +1,85 @@ +using System.Linq; +using Content.Shared.GameTicking; + +namespace Content.Client._NF.LateJoin; + +public static class StationJobInformationExtensions +{ + public static bool IsAnyStationAvailable(IReadOnlyDictionary obj) + { + return obj.Values.Any(station => + station is { IsLateJoinStation: true, JobsAvailable.Count: > 0 } + ); + } + + public static bool IsAnyCrewJobAvailable(IReadOnlyDictionary obj) + { + return obj.Values.Any(station => + station is { IsLateJoinStation: false, JobsAvailable.Count: > 0 } + ); + } + + public static string GetStationNameWithJobCount(this StationJobInformation stationJobInformation) + { + var jobCountString = stationJobInformation.GetJobCountString(); + var stationNameWithJobCount = string.IsNullOrEmpty(jobCountString) + ? stationJobInformation.StationName + : stationJobInformation.StationName + jobCountString; + + return stationNameWithJobCount; + } + + /** + * This method returns various strings that represent the job count of a station. + * If there are unlimited jobs available, it will return the job count followed by a "+". + * If there are no jobs available, it will return an empty string. + */ + public static string GetJobCountString(this StationJobInformation stationJobInformation) + { + var jobCount = stationJobInformation.GetJobCount(); + var hasUnlimitedJobs = stationJobInformation.HasUnlimitedJobs(); + return jobCount.WrapJobCountInParentheses(hasUnlimitedJobs); + } + + /** + * This method returns various strings that represent the job count of a list of stations. + * If there are unlimited jobs available, it will return the job count followed by a "+". + * If there are no jobs available, it will return an empty string. + */ + public static string GetJobSumCountString(this Dictionary obj) + { + var jobCount = obj.Values.Sum(stationJobInformation => stationJobInformation.GetJobCount()); + var hasUnlimitedJobs = obj.Values.Any(stationJobInformation => stationJobInformation.HasUnlimitedJobs()); + return jobCount.WrapJobCountInParentheses(hasUnlimitedJobs); + } + + /** + * One source of truth for the logic of whether a station has unlimited positions in one of its jobs. + * This is used to determine whether to display a "+" after the job count, or not to display the job count. + */ + private static bool HasUnlimitedJobs(this StationJobInformation stationJobInformation) + { + return stationJobInformation.JobsAvailable.Values.Any(count => count == null); + } + + private static int? GetJobCount(this StationJobInformation stationJobInformation) + { + return stationJobInformation.JobsAvailable.Values.Sum(); + } + + public static string WrapJobCountInParentheses(this int? jobCount, bool hasUnlimitedJobs = false) + { + if (jobCount is 0 or null) + { + return ""; + } + + var jobCountString = jobCount > 0 ? $"{jobCount}" : ""; + if (hasUnlimitedJobs && jobCount > 0) + { + jobCountString += "+"; + } + return $" ({jobCountString})"; + } + +} diff --git a/Content.Client/_NF/LateJoin/Interfaces/IPickerControl.cs b/Content.Client/_NF/LateJoin/Interfaces/IPickerControl.cs new file mode 100644 index 00000000000..e3c548e3633 --- /dev/null +++ b/Content.Client/_NF/LateJoin/Interfaces/IPickerControl.cs @@ -0,0 +1,9 @@ +using Content.Shared.GameTicking; +using Robust.Client.UserInterface.Controls; + +namespace Content.Client._NF.LateJoin.Interfaces; + +public abstract class PickerControl: PanelContainer +{ + public abstract void UpdateUi(IReadOnlyDictionary obj); +} diff --git a/Content.Client/_NF/LateJoin/ListItems/CrewListItem.xaml b/Content.Client/_NF/LateJoin/ListItems/CrewListItem.xaml new file mode 100644 index 00000000000..60cc7179a11 --- /dev/null +++ b/Content.Client/_NF/LateJoin/ListItems/CrewListItem.xaml @@ -0,0 +1,36 @@ + + + + + + diff --git a/Content.Client/_NF/LateJoin/ListItems/CrewListItem.xaml.cs b/Content.Client/_NF/LateJoin/ListItems/CrewListItem.xaml.cs new file mode 100644 index 00000000000..8bd2b8b7813 --- /dev/null +++ b/Content.Client/_NF/LateJoin/ListItems/CrewListItem.xaml.cs @@ -0,0 +1,43 @@ +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client._NF.LateJoin.ListItems; + +[GenerateTypedNameReferences] +public sealed partial class CrewListItem : PanelContainer +{ + public sealed class ViewState( + NetEntity stationEntity, + string stationName, + string stationSubtext, + string stationDescription, + bool selected, + string? iconPath) + { + public string StationName { get; } = stationName; + public string StationSubtext { get; } = stationSubtext; + public string StationDescription { get; } = stationDescription; + public NetEntity StationEntity { get; } = stationEntity; + + public bool Selected { get; set; } = selected; + + public string IconPath { get; } = iconPath ?? ""; + } + + public CrewListItem(ViewState state) + { + RobustXamlLoader.Load(this); + + StationName.Text = state.StationName; + StationSubtext.Text = state.StationSubtext; + + // Disallow repeated selection of same station that does UI reloads. + StationButton.Disabled = state.Selected; + + if (state.IconPath.Length > 0) + { + StationIcon.TexturePath = state.IconPath; + } + } +} diff --git a/Content.Client/_NF/Latejoin/NewFrontierLateJoinJobButton.xaml b/Content.Client/_NF/LateJoin/ListItems/JobListItem.xaml similarity index 81% rename from Content.Client/_NF/Latejoin/NewFrontierLateJoinJobButton.xaml rename to Content.Client/_NF/LateJoin/ListItems/JobListItem.xaml index 9d4f2f55829..26ee9a4916a 100644 --- a/Content.Client/_NF/Latejoin/NewFrontierLateJoinJobButton.xaml +++ b/Content.Client/_NF/LateJoin/ListItems/JobListItem.xaml @@ -1,6 +1,6 @@  diff --git a/Content.Client/_NF/LateJoin/ListItems/JobListItem.xaml.cs b/Content.Client/_NF/LateJoin/ListItems/JobListItem.xaml.cs new file mode 100644 index 00000000000..20d8c4a1046 --- /dev/null +++ b/Content.Client/_NF/LateJoin/ListItems/JobListItem.xaml.cs @@ -0,0 +1,31 @@ +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client._NF.LateJoin.ListItems; + +[GenerateTypedNameReferences] +public sealed partial class JobListItem : Button +{ + public sealed class ViewState(string jobId, string jobName, string toolTip, bool disabled, Texture? jobIcon) + { + public string JobId { get; } = jobId; + public string JobName { get; } = jobName; + + public bool Disabled { get; } = disabled; + + public string ToolTip { get; } = toolTip; + + public Texture? JobIcon { get; } = jobIcon; + } + + public JobListItem(ViewState state) + { + RobustXamlLoader.Load(this); + JobText.Text = state.JobName; + JobIcon.Texture = state.JobIcon; + ToolTip = state.ToolTip; + Disabled = state.Disabled; + } +} diff --git a/Content.Client/_NF/LateJoin/ListItems/StationListItem.xaml b/Content.Client/_NF/LateJoin/ListItems/StationListItem.xaml new file mode 100644 index 00000000000..1aad620f6bb --- /dev/null +++ b/Content.Client/_NF/LateJoin/ListItems/StationListItem.xaml @@ -0,0 +1,37 @@ + + + + + + diff --git a/Content.Client/_NF/LateJoin/ListItems/StationListItem.xaml.cs b/Content.Client/_NF/LateJoin/ListItems/StationListItem.xaml.cs new file mode 100644 index 00000000000..062c6a7ecf8 --- /dev/null +++ b/Content.Client/_NF/LateJoin/ListItems/StationListItem.xaml.cs @@ -0,0 +1,43 @@ +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client._NF.LateJoin.ListItems; + +[GenerateTypedNameReferences] +public sealed partial class StationListItem : PanelContainer +{ + public sealed class ViewState( + NetEntity stationEntity, + string stationName, + string stationSubtext, + string stationDescription, + bool selected, + string? iconPath) + { + public string StationName { get; } = stationName; + public string StationSubtext { get; } = stationSubtext; + public string StationDescription { get; } = stationDescription; + public NetEntity StationEntity { get; } = stationEntity; + + public bool Selected { get; set; } = selected; + + public string IconPath { get; } = iconPath ?? ""; + } + + public StationListItem(ViewState state) + { + RobustXamlLoader.Load(this); + + StationName.Text = state.StationName; + StationSubtext.Text = state.StationSubtext; + + // Disallow repeated selection of same station that does UI reloads. + StationButton.Disabled = state.Selected; + + if (state.IconPath.Length > 0) + { + StationIcon.TexturePath = state.IconPath; + } + } +} diff --git a/Content.Client/_NF/LateJoin/Windows/PickerWindow.xaml b/Content.Client/_NF/LateJoin/Windows/PickerWindow.xaml new file mode 100644 index 00000000000..39058119df5 --- /dev/null +++ b/Content.Client/_NF/LateJoin/Windows/PickerWindow.xaml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + diff --git a/Content.Client/_NF/LateJoin/Windows/PickerWindow.xaml.cs b/Content.Client/_NF/LateJoin/Windows/PickerWindow.xaml.cs new file mode 100644 index 00000000000..3170a824ca8 --- /dev/null +++ b/Content.Client/_NF/LateJoin/Windows/PickerWindow.xaml.cs @@ -0,0 +1,142 @@ +using System.Linq; +using Content.Client._NF.LateJoin.Controls; +using Content.Client._NF.LateJoin.Interfaces; +using Content.Client.GameTicking.Managers; +using Content.Client.UserInterface.Controls; +using Content.Shared.GameTicking; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Console; +using Robust.Shared.Utility; + +namespace Content.Client._NF.LateJoin.Windows; + +[GenerateTypedNameReferences] +public sealed partial class PickerWindow : FancyWindow +{ + [Dependency] private readonly IEntitySystemManager _entitySystem = default!; + [Dependency] private readonly ILocalizationManager _loc = default!; + [Dependency] private readonly IConsoleHost _consoleHost = default!; + private readonly ClientGameTicker _gameTicker; + private readonly ISawmill _sawmill; + + // Designed so you can implement your own tab controls, simply make your control and add the enum here. + public enum PickerType + { + StationOrCrewLarge, + Crew, + Station, + } + + public record PickerTab(PickerType Type, PickerControl Control); + + private PickerTab? _currentTab; + + public PickerWindow() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + _gameTicker = _entitySystem.GetEntitySystem(); + _gameTicker.LobbyJobsAvailableUpdated += UpdateUi; + _sawmill = Logger.GetSawmill("latejoin"); + + CrewTabButton.OnPressed += _ => + { + SetCurrentTab(PickerType.Crew); + UpdateUi(_gameTicker.StationJobInformationList); + }; + + StationTabButton.OnPressed += _ => + { + SetCurrentTab(PickerType.Station); + UpdateUi(_gameTicker.StationJobInformationList); + }; + + UpdateUi(_gameTicker.StationJobInformationList); + } + + public new void OpenCentered() + { + base.OpenCentered(); + + // This is the place to change the default tab. + if (_currentTab == null) + { + SetCurrentTab(PickerType.StationOrCrewLarge); + } + } + + protected override void ExitedTree() + { + base.ExitedTree(); + _gameTicker.LobbyJobsAvailableUpdated -= UpdateUi; + } + + private void UpdateUi(IReadOnlyDictionary obj) + { + // This is the place where it filters out cargo stations and others that shouldn't be shown in the latejoin ui. + var availableJobs = obj.Where(kvp => kvp.Value.JobsAvailable.Values.Count != 0) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + + var stationJobs = availableJobs.Where(kvp => kvp.Value.IsLateJoinStation) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + var crewJobs = availableJobs.Where(kvp => !kvp.Value.IsLateJoinStation) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + + StationTabLabel.Text = _loc.GetString("frontier-lobby-station-title") + stationJobs.GetJobSumCountString(); + StationTabButton.Disabled = !StationJobInformationExtensions.IsAnyStationAvailable(availableJobs) || + _currentTab?.Type == PickerType.Station; + + CrewTabLabel.Text = _loc.GetString("frontier-lobby-crew-title") + crewJobs.GetJobSumCountString(); + CrewTabButton.Disabled = !StationJobInformationExtensions.IsAnyCrewJobAvailable(availableJobs) || + _currentTab?.Type == PickerType.Crew; + + _currentTab?.Control.UpdateUi(availableJobs); + } + + private void SetCurrentTab(PickerType pickerType) + { + // Don't do anything when switching to same tab. + if (_currentTab != null && _currentTab.Type == pickerType) + { + return; + } + + ContentContainer.RemoveAllChildren(); + + switch (pickerType) + { + case PickerType.StationOrCrewLarge: + var stationOrCrewLargeControl = new StationOrCrewLargeControl(); + _currentTab = new PickerTab(pickerType, stationOrCrewLargeControl); + // Child panel can change tab from within, set the tab change callback. + stationOrCrewLargeControl.OnTabChange ??= SetCurrentTab; + break; + case PickerType.Crew: + var crewPickerControl = new CrewPickerControl(); + _currentTab = new PickerTab(pickerType, crewPickerControl); + crewPickerControl.OnJobJoined ??= JoinGame; + break; + case PickerType.Station: + var stationPickerControl = new StationPickerControl(); + _currentTab = new PickerTab(pickerType, stationPickerControl); + stationPickerControl.OnJobJoined ??= JoinGame; + break; + default: + throw new ArgumentOutOfRangeException(); // This will never happen. Trust. + } + + ContentContainer.AddChild(_currentTab.Control); + UpdateUi(_gameTicker.StationJobInformationList); + } + + private void JoinGame(NetEntity stationEntity, string jobId) + { + _sawmill.Info($"Late joining as ID: {jobId}"); + _consoleHost.ExecuteCommand( + $"joingame {CommandParsing.Escape(jobId)} {stationEntity}" + ); + + Close(); + } +} diff --git a/Content.Client/_NF/Latejoin/NFLateJoinGui.xaml b/Content.Client/_NF/Latejoin/NFLateJoinGui.xaml deleted file mode 100644 index 23752a03117..00000000000 --- a/Content.Client/_NF/Latejoin/NFLateJoinGui.xaml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - -