Port Ready Manifest from Impstation (#5187)

Meow meow moew meow emweo
This commit is contained in:
Dorragon 2026-01-18 21:47:07 +03:00 committed by GitHub
parent 428ca45cdb
commit 0f3eb79aa7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 504 additions and 1 deletions

View File

@ -14,6 +14,7 @@ using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Configuration;
using Robust.Shared.Timing;
using Content.Client._Impstation.ReadyManifest; // Impstation - Ready Manifest
namespace Content.Client.Lobby
{
@ -31,6 +32,7 @@ namespace Content.Client.Lobby
private ClientGameTicker _gameTicker = default!;
private ContentAudioSystem _contentAudioSystem = default!;
private ReadyManifestSystem _readyManifest = default!; // Impstation - Ready Manifest
protected override Type? LinkedScreenType { get; } = typeof(LobbyGui);
public LobbyGui? Lobby;
@ -48,6 +50,7 @@ namespace Content.Client.Lobby
_gameTicker = _entityManager.System<ClientGameTicker>();
_contentAudioSystem = _entityManager.System<ContentAudioSystem>();
_contentAudioSystem.LobbySoundtrackChanged += UpdateLobbySoundtrackInfo;
_readyManifest = _entityManager.EntitySysManager.GetEntitySystem<ReadyManifestSystem>(); // Impstation - Ready Manifest
chatController.SetMainChat(true);
@ -69,6 +72,7 @@ namespace Content.Client.Lobby
Lobby.CharacterPreview.CharacterSetupButton.OnPressed += OnSetupPressed;
Lobby.ReadyButton.OnPressed += OnReadyPressed;
Lobby.ReadyButton.OnToggled += OnReadyToggled;
Lobby.ManifestButton.OnPressed += OnManifestPressed; // Impstation - Ready Manifest
_gameTicker.InfoBlobUpdated += UpdateLobbyUi;
_gameTicker.LobbyStatusUpdated += LobbyStatusUpdated;
@ -89,6 +93,7 @@ namespace Content.Client.Lobby
Lobby!.CharacterPreview.CharacterSetupButton.OnPressed -= OnSetupPressed;
Lobby!.ReadyButton.OnPressed -= OnReadyPressed;
Lobby!.ReadyButton.OnToggled -= OnReadyToggled;
Lobby!.ManifestButton.OnPressed -= OnManifestPressed; // Impstation - Ready Manifest
Lobby = null;
}
@ -120,6 +125,12 @@ namespace Content.Client.Lobby
SetReady(args.Pressed);
}
// Impstation - Ready Manifest
private void OnManifestPressed(BaseButton.ButtonEventArgs args)
{
_readyManifest.RequestReadyManifest();
}
public override void FrameUpdate(FrameEventArgs e)
{
if (_gameTicker.IsGameStarted)
@ -182,6 +193,7 @@ namespace Content.Client.Lobby
Lobby!.ReadyButton.ToggleMode = false;
Lobby!.ReadyButton.Pressed = false;
Lobby!.ObserveButton.Disabled = false;
Lobby!.ManifestButton.Disabled = true; //imp
}
else
{
@ -191,6 +203,7 @@ namespace Content.Client.Lobby
Lobby!.ReadyButton.ToggleMode = true;
Lobby!.ReadyButton.Disabled = false;
Lobby!.ObserveButton.Disabled = true;
Lobby!.ManifestButton.Disabled = false; // imp
}
if (_gameTicker.ServerInfoBlob != null)

View File

@ -36,6 +36,10 @@
Align="Left"
FontColorOverride="{x:Static maths:Color.DarkGray}"
StyleClasses="LabelBig" HorizontalExpand="True" />
<!-- Imp manifest button -->
<Button Name="ManifestButton" Access="Public"
Text="Manifest"
StyleClasses="ButtonBig" MinWidth="137" />
<Button Name="ReadyButton" Access="Public" ToggleMode="True"
Text="{Loc 'ui-lobby-ready-up-button'}"
StyleClasses="ButtonBig" MinWidth="137" />

View File

@ -0,0 +1,47 @@
using Content.Client.Eui;
using Content.Shared.Eui;
using Content.Shared.ReadyManifest;
using JetBrains.Annotations;
namespace Content.Client._Impstation.ReadyManifest;
[UsedImplicitly]
public sealed class ReadyManifestEui : BaseEui
{
private readonly ReadyManifestUi _window;
public ReadyManifestEui()
{
_window = new();
_window.OnClose += () =>
{
SendMessage(new CloseEuiMessage());
};
}
public override void Opened()
{
base.Opened();
_window.OpenCentered();
}
public override void Closed()
{
base.Closed();
_window.Close();
}
public override void HandleState(EuiStateBase state)
{
base.HandleState(state);
if (state is not ReadyManifestEuiState cast)
{
return;
}
_window.RebuildUI(cast.JobCounts);
}
}

View File

@ -0,0 +1,20 @@
using Content.Shared.ReadyManifest;
namespace Content.Client._Impstation.ReadyManifest;
public sealed class ReadyManifestSystem : EntitySystem
{
private HashSet<string> _departments = new();
public IReadOnlySet<string> Departments => _departments;
public override void Initialize()
{
base.Initialize();
}
public void RequestReadyManifest()
{
RaiseNetworkEvent(new RequestReadyManifestMessage());
}
}

View File

@ -0,0 +1,10 @@
<DefaultWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="Ready Manifest"
SetSize="450 750">
<BoxContainer HorizontalExpand="True" VerticalExpand="True">
<ScrollContainer HorizontalExpand="True" VerticalExpand="True">
<BoxContainer Name="ReadyManifestListing" Orientation="Vertical"/>
</ScrollContainer>
</BoxContainer>
</DefaultWindow>

View File

@ -0,0 +1,137 @@
using System.Linq;
using System.Numerics;
using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Content.Client._Impstation.ReadyManifest;
[GenerateTypedNameReferences]
public sealed partial class ReadyManifestUi : DefaultWindow
{
private readonly IEntitySystemManager _entitySystem;
private readonly IPrototypeManager _prototypeManager;
private readonly ISharedPlayerManager _playerManager;
private readonly Dictionary<string, BoxContainer> _jobCategories;
private readonly SpriteSystem _sprite;
public ReadyManifestUi()
{
RobustXamlLoader.Load(this);
_jobCategories = new Dictionary<string, BoxContainer>();
_prototypeManager = IoCManager.Resolve<IPrototypeManager>();
_playerManager = IoCManager.Resolve<ISharedPlayerManager>();
_entitySystem = IoCManager.Resolve<IEntitySystemManager>();
_sprite = _entitySystem.GetEntitySystem<SpriteSystem>();
}
// Currently rebuilds the UI every time it's updated, should probably split department lookup and job counts into separate functions
public void RebuildUI(Dictionary<ProtoId<JobPrototype>, int> jobCounts)
{
ReadyManifestListing.DisposeAllChildren();
_jobCategories.Clear();
var departments = new List<DepartmentPrototype>();
foreach (var department in _prototypeManager.EnumeratePrototypes<DepartmentPrototype>())
{
if (department.EditorHidden)
continue;
departments.Add(department);
}
departments.Sort(DepartmentUIComparer.Instance);
foreach (var department in departments)
{
var departmentName = Loc.GetString($"department-{department.ID}");
if (!_jobCategories.TryGetValue(department.ID, out var category))
{
category = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Vertical,
HorizontalExpand = true,
Name = department.ID,
ToolTip = Loc.GetString("humanoid-profile-editor-jobs-amount-in-department-tooltip",
("departmentName", departmentName))
};
category.AddChild(new Label()
{
StyleClasses = { "LabelBig" },
Text = Loc.GetString($"department-{department.ID}")
});
_jobCategories[department.ID] = category;
ReadyManifestListing.AddChild(category);
}
var jobs = department.Roles.Select(jobId => _prototypeManager.Index(jobId))
.Where(job => job.SetPreference)
.ToArray();
Array.Sort(jobs, JobUIComparer.Instance);
foreach (var job in jobs)
{
var gridContainer = new GridContainer()
{
HorizontalExpand = true,
Columns = 2
};
var jobContainer = new BoxContainer()
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
};
var title = new RichTextLabel()
{
HorizontalExpand = true
};
title.SetMessage(job.LocalizedName + ":");
var icon = new TextureRect
{
TextureScale = new Vector2(2, 2),
VerticalAlignment = VAlignment.Center,
Margin = new Thickness(0, 0, 4, 0)
};
var readyCount = new RichTextLabel()
{
HorizontalExpand = true
};
if (jobCounts.ContainsKey(job.ID))
{
var jobCount = jobCounts[job.ID];
var color = jobCount > 0 ? Color.White : Color.Red;
readyCount.SetMessage(jobCount.ToString(), null, color);
}
else
{
readyCount.SetMessage("0", null, Color.Red);
}
var jobIcon = _prototypeManager.Index(job.Icon);
icon.Texture = _sprite.Frame0(jobIcon.Icon);
jobContainer.AddChild(icon);
jobContainer.AddChild(title);
gridContainer.AddChild(jobContainer);
gridContainer.AddChild(readyCount);
category.AddChild(gridContainer);
}
}
}
}

View File

@ -157,6 +157,7 @@ namespace Content.Server.GameTicking
if (!_playerManager.TryGetSessionById(playerUserId, out var playerSession))
continue;
RaiseNetworkEvent(GetStatusMsg(playerSession), playerSession.Channel);
RaiseLocalEvent(new PlayerToggleReadyEvent(playerSession)); //imp edit, for preround ready manifest // imp ready manifest
}
}
@ -173,8 +174,16 @@ namespace Content.Server.GameTicking
return;
}
// imp edit start, no need to update if the player is already (un)readied
var status = ready ? PlayerGameStatus.ReadyToPlay : PlayerGameStatus.NotReadyToPlay;
if (_playerGameStatuses[player.UserId] == status)
{
return;
}
// imp edit end
_playerGameStatuses[player.UserId] = ready ? PlayerGameStatus.ReadyToPlay : PlayerGameStatus.NotReadyToPlay;
RaiseNetworkEvent(GetStatusMsg(player), player.Channel);
RaiseLocalEvent(new PlayerToggleReadyEvent(player)); //imp edit, for preround ready manifest
// update server info to reflect new ready count
UpdateInfoText();
}
@ -185,4 +194,15 @@ namespace Content.Server.GameTicking
public bool UserHasJoinedGame(NetUserId userId)
=> PlayerGameStatuses.TryGetValue(userId, out var status) && status == PlayerGameStatus.JoinedGame;
}
//imp addition, for preround ready manifest
public sealed class PlayerToggleReadyEvent : EntityEventArgs
{
public readonly ICommonSession PlayerSession;
public PlayerToggleReadyEvent(ICommonSession playerSession)
{
PlayerSession = playerSession;
}
}
}

View File

@ -0,0 +1,35 @@
using Content.Server.EUI;
using Content.Shared.ReadyManifest;
namespace Content.Server.ReadyManifest;
public sealed class ReadyManifestEui : BaseEui
{
private readonly ReadyManifestSystem _readyManifest;
/// <summary>
/// Current owner of this UI, if it has one. This is
/// to ensure that if a BUI is closed, the EUIs related
/// to the BUI are closed as well.
/// </summary>
public readonly EntityUid? Owner;
public ReadyManifestEui(EntityUid? owner, ReadyManifestSystem readyManifestSystem)
{
Owner = owner;
_readyManifest = readyManifestSystem;
}
public override ReadyManifestEuiState GetNewState()
{
var entries = _readyManifest.GetReadyManifest();
return new(entries);
}
public override void Closed()
{
base.Closed();
_readyManifest.CloseEui(Player, Owner);
}
}

View File

@ -0,0 +1,188 @@
using System.Linq;
using Content.Server.EUI;
using Content.Shared.CCVar;
using Content.Shared.GameTicking;
using Content.Shared.Roles;
using Content.Shared.Preferences;
using Robust.Shared.Configuration;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Content.Shared.ReadyManifest;
using Content.Server.GameTicking;
using Content.Server.Preferences.Managers;
using Content.Server.GameTicking.Events;
namespace Content.Server.ReadyManifest;
public sealed class ReadyManifestSystem : EntitySystem
{
[Dependency] private readonly EuiManager _euiManager = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly IServerPreferencesManager _prefsManager = default!;
private readonly Dictionary<ICommonSession, ReadyManifestEui> _openEuis = new();
private Dictionary<ProtoId<JobPrototype>, int> _jobCounts = new();
public override void Initialize()
{
SubscribeNetworkEvent<RequestReadyManifestMessage>(OnRequestReadyManifest);
SubscribeLocalEvent<RoundStartingEvent>(OnRoundStarting);
SubscribeLocalEvent<PlayerToggleReadyEvent>(OnPlayerToggleReady);
}
private void OnRoundStarting(RoundStartingEvent ev)
{
foreach (var (_, eui) in _openEuis)
{
eui.Close();
}
_openEuis.Clear();
}
private void OnRequestReadyManifest(RequestReadyManifestMessage message, EntitySessionEventArgs args)
{
if (args.SenderSession is not { } sessionCast
|| !_configManager.GetCVar(CCVars.CrewManifestWithoutEntity))
{
return;
}
BuildReadyManifest();
OpenEui(sessionCast, args.SenderSession.AttachedEntity);
}
private void OnPlayerToggleReady(PlayerToggleReadyEvent ev)
{
var userId = ev.PlayerSession.Data.UserId;
if (!_prefsManager.TryGetCachedPreferences(userId, out var preferences))
{
return;
}
HumanoidCharacterProfile profile = (HumanoidCharacterProfile) preferences.SelectedCharacter;
var profileJobs = FilterPlayerJobs(profile);
if (_gameTicker.PlayerGameStatuses[userId] == PlayerGameStatus.ReadyToPlay)
{
foreach (var job in profileJobs)
{
if (_jobCounts.ContainsKey(job))
{
_jobCounts[job]++;
}
else
{
_jobCounts.Add(job, 1);
}
}
}
else
{
foreach (var job in profileJobs)
{
if (_jobCounts.ContainsKey(job))
{
_jobCounts[job]--;
}
}
}
UpdateEuis();
}
private void BuildReadyManifest()
{
var jobCounts = new Dictionary<ProtoId<JobPrototype>, int>();
foreach (var (userId, status) in _gameTicker.PlayerGameStatuses)
{
if (status == PlayerGameStatus.ReadyToPlay)
{
HumanoidCharacterProfile profile;
if (_prefsManager.TryGetCachedPreferences(userId, out var preferences))
{
profile = (HumanoidCharacterProfile) preferences.SelectedCharacter;
var profileJobs = FilterPlayerJobs(profile);
foreach (var jobId in profileJobs)
{
if (jobCounts.ContainsKey(jobId))
{
jobCounts[jobId]++;
}
else
{
jobCounts.Add(jobId, 1);
}
}
}
}
}
_jobCounts = jobCounts;
}
private List<ProtoId<JobPrototype>> FilterPlayerJobs(HumanoidCharacterProfile profile)
{
var jobs = profile.JobPriorities.Keys.Select(k => new ProtoId<JobPrototype>(k)).ToList();
List<ProtoId<JobPrototype>> priorityJobs = new();
foreach (var job in jobs)
{
var priority = profile.JobPriorities[job];
if (priority == JobPriority.High || (_prototypeManager.Index(job).Weight >= 10 && priority > JobPriority.Never))
{
priorityJobs.Add(job);
}
}
return priorityJobs;
}
public Dictionary<ProtoId<JobPrototype>, int> GetReadyManifest()
{
return _jobCounts;
}
public void OpenEui(ICommonSession session, EntityUid? owner = null)
{
if (_openEuis.ContainsKey(session))
{
return;
}
var eui = new ReadyManifestEui(owner, this);
_openEuis.Add(session, eui);
_euiManager.OpenEui(eui, session);
eui.StateDirty();
}
private void UpdateEuis()
{
foreach (var (_, eui) in _openEuis)
{
eui.StateDirty();
}
}
/// <summary>
/// Closes an EUI for a given player.
/// </summary>
/// <param name="session">The player's session.</param>
/// <param name="owner">The owner of this EUI, if there was one.</param>
public void CloseEui(ICommonSession session, EntityUid? owner = null)
{
if (!_openEuis.TryGetValue(session, out var eui))
{
return;
}
if (eui.Owner == owner)
{
_openEuis.Remove(session);
eui.Close();
}
}
}

View File

@ -0,0 +1,29 @@
using Content.Shared.Eui;
using Content.Shared.Roles;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.ReadyManifest;
[Serializable, NetSerializable]
public sealed class RequestReadyManifestMessage : EntityEventArgs
{
public NetEntity Id { get; }
public RequestReadyManifestMessage()
{
//Id = id;
}
}
[Serializable, NetSerializable]
public sealed class ReadyManifestEuiState : EuiStateBase
{
public Dictionary<ProtoId<JobPrototype>, int> JobCounts { get; }
public ReadyManifestEuiState(Dictionary<ProtoId<JobPrototype>, int> jobCounts)
{
JobCounts = jobCounts;
}
}