Voting (#3185)
* Basic voting * Rewrite lobby in XAML. Working lobby voting. * Escape menu is now XAML. * Vote menu works, custom votes, gamemode votes. * Vote timeouts & administration. Basically done now. * I will now pretend I was never planning to code voting hotkeys. * Make vote call UI a bit... funny. * Fix exception on round restart. * Fix some vote command definitions.
This commit is contained in:
committed by
GitHub
parent
db290fd91e
commit
cea87d6985
@@ -12,6 +12,7 @@ using Content.Client.UserInterface;
|
|||||||
using Content.Client.UserInterface.AdminMenu;
|
using Content.Client.UserInterface.AdminMenu;
|
||||||
using Content.Client.UserInterface.Stylesheets;
|
using Content.Client.UserInterface.Stylesheets;
|
||||||
using Content.Client.Utility;
|
using Content.Client.Utility;
|
||||||
|
using Content.Client.Voting;
|
||||||
using Content.Shared.Actions;
|
using Content.Shared.Actions;
|
||||||
using Content.Shared.Interfaces;
|
using Content.Shared.Interfaces;
|
||||||
using Content.Shared.Alert;
|
using Content.Shared.Alert;
|
||||||
@@ -43,6 +44,7 @@ namespace Content.Client
|
|||||||
IoCManager.Register<ActionManager, ActionManager>();
|
IoCManager.Register<ActionManager, ActionManager>();
|
||||||
IoCManager.Register<IClientAdminManager, ClientAdminManager>();
|
IoCManager.Register<IClientAdminManager, ClientAdminManager>();
|
||||||
IoCManager.Register<EuiManager, EuiManager>();
|
IoCManager.Register<EuiManager, EuiManager>();
|
||||||
|
IoCManager.Register<IVoteManager, VoteManager>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ using Content.Client.UserInterface;
|
|||||||
using Content.Client.UserInterface.AdminMenu;
|
using Content.Client.UserInterface.AdminMenu;
|
||||||
using Content.Client.UserInterface.Stylesheets;
|
using Content.Client.UserInterface.Stylesheets;
|
||||||
using Content.Client.Graphics.Overlays;
|
using Content.Client.Graphics.Overlays;
|
||||||
|
using Content.Client.Voting;
|
||||||
using Content.Shared.Actions;
|
using Content.Shared.Actions;
|
||||||
using Content.Shared.GameObjects.Components;
|
using Content.Shared.GameObjects.Components;
|
||||||
using Content.Shared.GameObjects.Components.Cargo;
|
using Content.Shared.GameObjects.Components.Cargo;
|
||||||
@@ -161,6 +162,7 @@ namespace Content.Client
|
|||||||
IoCManager.Resolve<EuiManager>().Initialize();
|
IoCManager.Resolve<EuiManager>().Initialize();
|
||||||
IoCManager.Resolve<AlertManager>().Initialize();
|
IoCManager.Resolve<AlertManager>().Initialize();
|
||||||
IoCManager.Resolve<ActionManager>().Initialize();
|
IoCManager.Resolve<ActionManager>().Initialize();
|
||||||
|
IoCManager.Resolve<IVoteManager>().Initialize();
|
||||||
|
|
||||||
_baseClient.RunLevelChanged += (sender, args) =>
|
_baseClient.RunLevelChanged += (sender, args) =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Content.Client.Chat;
|
using Content.Client.Chat;
|
||||||
using Content.Client.Interfaces.Chat;
|
using Content.Client.Interfaces.Chat;
|
||||||
using Content.Client.UserInterface;
|
using Content.Client.UserInterface;
|
||||||
|
using Content.Client.Voting;
|
||||||
using Content.Shared.Input;
|
using Content.Shared.Input;
|
||||||
using Robust.Client.Input;
|
using Robust.Client.Input;
|
||||||
using Robust.Client.UserInterface;
|
using Robust.Client.UserInterface;
|
||||||
@@ -18,6 +19,7 @@ namespace Content.Client.State
|
|||||||
[Dependency] private readonly IGameHud _gameHud = default!;
|
[Dependency] private readonly IGameHud _gameHud = default!;
|
||||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||||
[Dependency] private readonly IChatManager _chatManager = default!;
|
[Dependency] private readonly IChatManager _chatManager = default!;
|
||||||
|
[Dependency] private readonly IVoteManager _voteManager = default!;
|
||||||
|
|
||||||
[ViewVariables] private ChatBox _gameChat;
|
[ViewVariables] private ChatBox _gameChat;
|
||||||
|
|
||||||
@@ -35,6 +37,7 @@ namespace Content.Client.State
|
|||||||
|
|
||||||
_userInterfaceManager.StateRoot.AddChild(_gameHud.RootControl);
|
_userInterfaceManager.StateRoot.AddChild(_gameHud.RootControl);
|
||||||
_chatManager.SetChatBox(_gameChat);
|
_chatManager.SetChatBox(_gameChat);
|
||||||
|
_voteManager.SetPopupContainer(_gameHud.VoteContainer);
|
||||||
_gameChat.DefaultChatFormat = "say \"{0}\"";
|
_gameChat.DefaultChatFormat = "say \"{0}\"";
|
||||||
_gameChat.Input.PlaceHolder = Loc.GetString("Say something! [ for OOC");
|
_gameChat.Input.PlaceHolder = Loc.GetString("Say something! [ for OOC");
|
||||||
|
|
||||||
|
|||||||
@@ -31,14 +31,6 @@ namespace Content.Client.State
|
|||||||
|
|
||||||
public override void Startup()
|
public override void Startup()
|
||||||
{
|
{
|
||||||
var panelTex = ResC.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png");
|
|
||||||
var back = new StyleBoxTexture
|
|
||||||
{
|
|
||||||
Texture = panelTex,
|
|
||||||
Modulate = new Color(32, 32, 48),
|
|
||||||
};
|
|
||||||
back.SetPatchMargin(StyleBox.Margin.All, 10);
|
|
||||||
|
|
||||||
Button exitButton;
|
Button exitButton;
|
||||||
Button reconnectButton;
|
Button reconnectButton;
|
||||||
Button retryButton;
|
Button retryButton;
|
||||||
@@ -50,10 +42,7 @@ namespace Content.Client.State
|
|||||||
Stylesheet = _stylesheetManager.SheetSpace,
|
Stylesheet = _stylesheetManager.SheetSpace,
|
||||||
Children =
|
Children =
|
||||||
{
|
{
|
||||||
new PanelContainer
|
new PanelContainer {StyleClasses = {StyleBase.ClassAngleRect}},
|
||||||
{
|
|
||||||
PanelOverride = back
|
|
||||||
},
|
|
||||||
new VBoxContainer
|
new VBoxContainer
|
||||||
{
|
{
|
||||||
SeparationOverride = 0,
|
SeparationOverride = 0,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Linq;
|
|||||||
using Content.Client.Interfaces;
|
using Content.Client.Interfaces;
|
||||||
using Content.Client.Interfaces.Chat;
|
using Content.Client.Interfaces.Chat;
|
||||||
using Content.Client.UserInterface;
|
using Content.Client.UserInterface;
|
||||||
|
using Content.Client.Voting;
|
||||||
using Content.Shared.Input;
|
using Content.Shared.Input;
|
||||||
using Robust.Client;
|
using Robust.Client;
|
||||||
using Robust.Client.Console;
|
using Robust.Client.Console;
|
||||||
@@ -36,6 +37,7 @@ namespace Content.Client.State
|
|||||||
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
|
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
|
||||||
[Dependency] private readonly IClientPreferencesManager _preferencesManager = 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!;
|
||||||
|
|
||||||
[ViewVariables] private CharacterSetupGui _characterSetup;
|
[ViewVariables] private CharacterSetupGui _characterSetup;
|
||||||
[ViewVariables] private LobbyGui _lobby;
|
[ViewVariables] private LobbyGui _lobby;
|
||||||
@@ -58,12 +60,14 @@ namespace Content.Client.State
|
|||||||
_lobby.CharacterPreview.UpdateUI();
|
_lobby.CharacterPreview.UpdateUI();
|
||||||
};
|
};
|
||||||
|
|
||||||
_lobby = new LobbyGui(_entityManager, _resourceCache, _preferencesManager);
|
_lobby = new LobbyGui(_entityManager, _preferencesManager);
|
||||||
_userInterfaceManager.StateRoot.AddChild(_lobby);
|
_userInterfaceManager.StateRoot.AddChild(_lobby);
|
||||||
|
|
||||||
LayoutContainer.SetAnchorPreset(_lobby, LayoutContainer.LayoutPreset.Wide);
|
LayoutContainer.SetAnchorPreset(_lobby, LayoutContainer.LayoutPreset.Wide);
|
||||||
|
|
||||||
_chatManager.SetChatBox(_lobby.Chat);
|
_chatManager.SetChatBox(_lobby.Chat);
|
||||||
|
_voteManager.SetPopupContainer(_lobby.VoteContainer);
|
||||||
|
|
||||||
_lobby.Chat.DefaultChatFormat = "ooc \"{0}\"";
|
_lobby.Chat.DefaultChatFormat = "ooc \"{0}\"";
|
||||||
|
|
||||||
_lobby.ServerName.Text = _baseClient.GameInfo.ServerName;
|
_lobby.ServerName.Text = _baseClient.GameInfo.ServerName;
|
||||||
|
|||||||
12
Content.Client/UserInterface/EscapeMenu.xaml
Normal file
12
Content.Client/UserInterface/EscapeMenu.xaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<SS14Window xmlns="https://spacestation14.io"
|
||||||
|
xmlns:voting="clr-namespace:Content.Client.Voting"
|
||||||
|
Title="{Loc 'Esc Menu'}"
|
||||||
|
Resizable="False">
|
||||||
|
|
||||||
|
<VBoxContainer SeparationOverride="4">
|
||||||
|
<voting:VoteCallMenuButton />
|
||||||
|
<Button Name="OptionsButton" Text="{Loc 'Options'}" />
|
||||||
|
<Button Name="DisconnectButton" Text="{Loc 'Disconnect'}" />
|
||||||
|
<Button Name="QuitButton" Text="{Loc 'Quit game'}" />
|
||||||
|
</VBoxContainer>
|
||||||
|
</SS14Window>
|
||||||
@@ -1,51 +1,29 @@
|
|||||||
using Robust.Client.Console;
|
using Robust.Client.AutoGenerated;
|
||||||
|
using Robust.Client.Console;
|
||||||
using Robust.Client.UserInterface.Controls;
|
using Robust.Client.UserInterface.Controls;
|
||||||
using Robust.Client.UserInterface.CustomControls;
|
using Robust.Client.UserInterface.CustomControls;
|
||||||
using Robust.Shared.IoC;
|
using Robust.Client.UserInterface.XAML;
|
||||||
using Robust.Shared.Localization;
|
|
||||||
|
|
||||||
namespace Content.Client.UserInterface
|
namespace Content.Client.UserInterface
|
||||||
{
|
{
|
||||||
internal sealed class EscapeMenu : SS14Window
|
[GenerateTypedNameReferences]
|
||||||
|
internal partial class EscapeMenu : SS14Window
|
||||||
{
|
{
|
||||||
private readonly IClientConsoleHost _consoleHost;
|
private readonly IClientConsoleHost _consoleHost;
|
||||||
|
|
||||||
private BaseButton DisconnectButton;
|
|
||||||
private BaseButton QuitButton;
|
|
||||||
private BaseButton OptionsButton;
|
|
||||||
private OptionsMenu optionsMenu;
|
private OptionsMenu optionsMenu;
|
||||||
|
|
||||||
public EscapeMenu(IClientConsoleHost consoleHost)
|
public EscapeMenu(IClientConsoleHost consoleHost)
|
||||||
{
|
{
|
||||||
_consoleHost = consoleHost;
|
_consoleHost = consoleHost;
|
||||||
|
|
||||||
IoCManager.InjectDependencies(this);
|
RobustXamlLoader.Load(this);
|
||||||
|
|
||||||
PerformLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PerformLayout()
|
|
||||||
{
|
|
||||||
optionsMenu = new OptionsMenu();
|
optionsMenu = new OptionsMenu();
|
||||||
|
|
||||||
Resizable = false;
|
|
||||||
|
|
||||||
Title = "Esc Menu";
|
|
||||||
|
|
||||||
var vBox = new VBoxContainer {SeparationOverride = 4};
|
|
||||||
Contents.AddChild(vBox);
|
|
||||||
|
|
||||||
OptionsButton = new Button {Text = Loc.GetString("Options")};
|
|
||||||
OptionsButton.OnPressed += OnOptionsButtonClicked;
|
OptionsButton.OnPressed += OnOptionsButtonClicked;
|
||||||
vBox.AddChild(OptionsButton);
|
|
||||||
|
|
||||||
DisconnectButton = new Button {Text = Loc.GetString("Disconnect")};
|
|
||||||
DisconnectButton.OnPressed += OnDisconnectButtonClicked;
|
|
||||||
vBox.AddChild(DisconnectButton);
|
|
||||||
|
|
||||||
QuitButton = new Button {Text = Loc.GetString("Quit Game")};
|
|
||||||
QuitButton.OnPressed += OnQuitButtonClicked;
|
QuitButton.OnPressed += OnQuitButtonClicked;
|
||||||
vBox.AddChild(QuitButton);
|
DisconnectButton.OnPressed += OnDisconnectButtonClicked;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnQuitButtonClicked(BaseButton.ButtonEventArgs args)
|
private void OnQuitButtonClicked(BaseButton.ButtonEventArgs args)
|
||||||
@@ -15,6 +15,7 @@ using Robust.Shared.Maths;
|
|||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
using static Robust.Client.Input.Keyboard.Key;
|
using static Robust.Client.Input.Keyboard.Key;
|
||||||
using Control = Robust.Client.UserInterface.Control;
|
using Control = Robust.Client.UserInterface.Control;
|
||||||
|
using LC = Robust.Client.UserInterface.Controls.LayoutContainer;
|
||||||
|
|
||||||
namespace Content.Client.UserInterface
|
namespace Content.Client.UserInterface
|
||||||
{
|
{
|
||||||
@@ -70,6 +71,9 @@ namespace Content.Client.UserInterface
|
|||||||
Action<bool> OnCombatModeChanged { get; set; }
|
Action<bool> OnCombatModeChanged { get; set; }
|
||||||
Action<TargetingZone> OnTargetingZoneChanged { get; set; }
|
Action<TargetingZone> OnTargetingZoneChanged { get; set; }
|
||||||
|
|
||||||
|
Control VoteContainer { get; }
|
||||||
|
|
||||||
|
void AddTopNotification(TopNotification notification);
|
||||||
|
|
||||||
// Init logic.
|
// Init logic.
|
||||||
void Initialize();
|
void Initialize();
|
||||||
@@ -90,6 +94,7 @@ namespace Content.Client.UserInterface
|
|||||||
private TargetingDoll _targetingDoll;
|
private TargetingDoll _targetingDoll;
|
||||||
private Button _combatModeButton;
|
private Button _combatModeButton;
|
||||||
private VBoxContainer _combatPanelContainer;
|
private VBoxContainer _combatPanelContainer;
|
||||||
|
private VBoxContainer _topNotificationContainer;
|
||||||
|
|
||||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||||
@@ -120,10 +125,15 @@ namespace Content.Client.UserInterface
|
|||||||
public Action<bool> OnCombatModeChanged { get; set; }
|
public Action<bool> OnCombatModeChanged { get; set; }
|
||||||
public Action<TargetingZone> OnTargetingZoneChanged { get; set; }
|
public Action<TargetingZone> OnTargetingZoneChanged { get; set; }
|
||||||
|
|
||||||
|
public void AddTopNotification(TopNotification notification)
|
||||||
|
{
|
||||||
|
_topNotificationContainer.AddChild(notification);
|
||||||
|
}
|
||||||
|
|
||||||
public void Initialize()
|
public void Initialize()
|
||||||
{
|
{
|
||||||
RootControl = new LayoutContainer();
|
RootControl = new LC();
|
||||||
LayoutContainer.SetAnchorPreset(RootControl, LayoutContainer.LayoutPreset.Wide);
|
LC.SetAnchorPreset(RootControl, LC.LayoutPreset.Wide);
|
||||||
|
|
||||||
var escapeTexture = _resourceCache.GetTexture("/Textures/Interface/hamburger.svg.192dpi.png");
|
var escapeTexture = _resourceCache.GetTexture("/Textures/Interface/hamburger.svg.192dpi.png");
|
||||||
var characterTexture = _resourceCache.GetTexture("/Textures/Interface/character.svg.192dpi.png");
|
var characterTexture = _resourceCache.GetTexture("/Textures/Interface/character.svg.192dpi.png");
|
||||||
@@ -141,7 +151,7 @@ namespace Content.Client.UserInterface
|
|||||||
|
|
||||||
RootControl.AddChild(_topButtonsContainer);
|
RootControl.AddChild(_topButtonsContainer);
|
||||||
|
|
||||||
LayoutContainer.SetAnchorAndMarginPreset(_topButtonsContainer, LayoutContainer.LayoutPreset.TopLeft,
|
LC.SetAnchorAndMarginPreset(_topButtonsContainer, LC.LayoutPreset.TopLeft,
|
||||||
margin: 10);
|
margin: 10);
|
||||||
|
|
||||||
// the icon textures here should all have the same image height (32) but different widths, so in order to ensure
|
// the icon textures here should all have the same image height (32) but different widths, so in order to ensure
|
||||||
@@ -271,10 +281,10 @@ namespace Content.Client.UserInterface
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
LayoutContainer.SetGrowHorizontal(_combatPanelContainer, LayoutContainer.GrowDirection.Begin);
|
LC.SetGrowHorizontal(_combatPanelContainer, LC.GrowDirection.Begin);
|
||||||
LayoutContainer.SetGrowVertical(_combatPanelContainer, LayoutContainer.GrowDirection.Begin);
|
LC.SetGrowVertical(_combatPanelContainer, LC.GrowDirection.Begin);
|
||||||
LayoutContainer.SetAnchorAndMarginPreset(_combatPanelContainer, LayoutContainer.LayoutPreset.BottomRight);
|
LC.SetAnchorAndMarginPreset(_combatPanelContainer, LC.LayoutPreset.BottomRight);
|
||||||
LayoutContainer.SetMarginBottom(_combatPanelContainer, -10f);
|
LC.SetMarginBottom(_combatPanelContainer, -10f);
|
||||||
RootControl.AddChild(_combatPanelContainer);
|
RootControl.AddChild(_combatPanelContainer);
|
||||||
|
|
||||||
_combatModeButton.OnToggled += args => OnCombatModeChanged?.Invoke(args.Pressed);
|
_combatModeButton.OnToggled += args => OnCombatModeChanged?.Invoke(args.Pressed);
|
||||||
@@ -284,10 +294,10 @@ namespace Content.Client.UserInterface
|
|||||||
{
|
{
|
||||||
SeparationOverride = 5
|
SeparationOverride = 5
|
||||||
};
|
};
|
||||||
LayoutContainer.SetAnchorAndMarginPreset(centerBottomContainer, LayoutContainer.LayoutPreset.CenterBottom);
|
LC.SetAnchorAndMarginPreset(centerBottomContainer, LC.LayoutPreset.CenterBottom);
|
||||||
LayoutContainer.SetGrowHorizontal(centerBottomContainer, LayoutContainer.GrowDirection.Both);
|
LC.SetGrowHorizontal(centerBottomContainer, LC.GrowDirection.Both);
|
||||||
LayoutContainer.SetGrowVertical(centerBottomContainer, LayoutContainer.GrowDirection.Begin);
|
LC.SetGrowVertical(centerBottomContainer, LC.GrowDirection.Begin);
|
||||||
LayoutContainer.SetMarginBottom(centerBottomContainer, -10f);
|
LC.SetMarginBottom(centerBottomContainer, -10f);
|
||||||
RootControl.AddChild(centerBottomContainer);
|
RootControl.AddChild(centerBottomContainer);
|
||||||
|
|
||||||
HandsContainer = new MarginContainer
|
HandsContainer = new MarginContainer
|
||||||
@@ -313,10 +323,27 @@ namespace Content.Client.UserInterface
|
|||||||
|
|
||||||
RootControl.AddChild(SuspicionContainer);
|
RootControl.AddChild(SuspicionContainer);
|
||||||
|
|
||||||
LayoutContainer.SetAnchorAndMarginPreset(SuspicionContainer, LayoutContainer.LayoutPreset.BottomLeft,
|
LC.SetAnchorAndMarginPreset(SuspicionContainer, LC.LayoutPreset.BottomLeft,
|
||||||
margin: 10);
|
margin: 10);
|
||||||
LayoutContainer.SetGrowHorizontal(SuspicionContainer, LayoutContainer.GrowDirection.End);
|
LC.SetGrowHorizontal(SuspicionContainer, LC.GrowDirection.End);
|
||||||
LayoutContainer.SetGrowVertical(SuspicionContainer, LayoutContainer.GrowDirection.Begin);
|
LC.SetGrowVertical(SuspicionContainer, LC.GrowDirection.Begin);
|
||||||
|
|
||||||
|
_topNotificationContainer = new VBoxContainer
|
||||||
|
{
|
||||||
|
CustomMinimumSize = (600, 0)
|
||||||
|
};
|
||||||
|
RootControl.AddChild(_topNotificationContainer);
|
||||||
|
LC.SetAnchorPreset(_topNotificationContainer, LC.LayoutPreset.CenterTop);
|
||||||
|
LC.SetGrowHorizontal(_topNotificationContainer, LC.GrowDirection.Both);
|
||||||
|
LC.SetGrowVertical(_topNotificationContainer, LC.GrowDirection.End);
|
||||||
|
|
||||||
|
VoteContainer = new VBoxContainer();
|
||||||
|
RootControl.AddChild(VoteContainer);
|
||||||
|
LC.SetAnchorPreset(VoteContainer, LC.LayoutPreset.TopLeft);
|
||||||
|
LC.SetMarginLeft(VoteContainer, 180);
|
||||||
|
LC.SetMarginTop(VoteContainer, 100);
|
||||||
|
LC.SetGrowHorizontal(VoteContainer, LC.GrowDirection.End);
|
||||||
|
LC.SetGrowVertical(VoteContainer, LC.GrowDirection.End);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ButtonTutorialOnOnToggled()
|
private void ButtonTutorialOnOnToggled()
|
||||||
@@ -436,6 +463,8 @@ namespace Content.Client.UserInterface
|
|||||||
|
|
||||||
public Action<bool> SandboxButtonToggled { get; set; }
|
public Action<bool> SandboxButtonToggled { get; set; }
|
||||||
|
|
||||||
|
public Control VoteContainer { get; private set; }
|
||||||
|
|
||||||
public sealed class TopButton : ContainerButton
|
public sealed class TopButton : ContainerButton
|
||||||
{
|
{
|
||||||
public const string StyleClassLabelTopButton = "topButtonLabel";
|
public const string StyleClassLabelTopButton = "topButtonLabel";
|
||||||
|
|||||||
@@ -1,330 +0,0 @@
|
|||||||
using Content.Client.Chat;
|
|
||||||
using Content.Client.Interfaces;
|
|
||||||
using Content.Client.UserInterface.Stylesheets;
|
|
||||||
using Content.Client.Utility;
|
|
||||||
using Robust.Client.Graphics;
|
|
||||||
using Robust.Client.ResourceManagement;
|
|
||||||
using Robust.Client.UserInterface;
|
|
||||||
using Robust.Client.UserInterface.Controls;
|
|
||||||
using Robust.Shared.GameObjects;
|
|
||||||
using Robust.Shared.Localization;
|
|
||||||
using Robust.Shared.Maths;
|
|
||||||
|
|
||||||
namespace Content.Client.UserInterface
|
|
||||||
{
|
|
||||||
internal sealed class LobbyGui : Control
|
|
||||||
{
|
|
||||||
public Label ServerName { get; }
|
|
||||||
public Label StartTime { get; }
|
|
||||||
public Button ReadyButton { get; }
|
|
||||||
public Button ObserveButton { get; }
|
|
||||||
public Button OptionsButton { get; }
|
|
||||||
public Button LeaveButton { get; }
|
|
||||||
public ChatBox Chat { get; }
|
|
||||||
public LobbyPlayerList OnlinePlayerList { get; }
|
|
||||||
public ServerInfo ServerInfo { get; }
|
|
||||||
public LobbyCharacterPreviewPanel CharacterPreview { get; }
|
|
||||||
|
|
||||||
public LobbyGui(IEntityManager entityManager,
|
|
||||||
IResourceCache resourceCache,
|
|
||||||
IClientPreferencesManager preferencesManager)
|
|
||||||
{
|
|
||||||
var margin = new MarginContainer
|
|
||||||
{
|
|
||||||
MarginBottomOverride = 20,
|
|
||||||
MarginLeftOverride = 20,
|
|
||||||
MarginRightOverride = 20,
|
|
||||||
MarginTopOverride = 20,
|
|
||||||
};
|
|
||||||
|
|
||||||
AddChild(margin);
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
var panel = new PanelContainer
|
|
||||||
{
|
|
||||||
PanelOverride = back
|
|
||||||
};
|
|
||||||
|
|
||||||
margin.AddChild(panel);
|
|
||||||
|
|
||||||
var vBox = new VBoxContainer {SeparationOverride = 0};
|
|
||||||
|
|
||||||
margin.AddChild(vBox);
|
|
||||||
|
|
||||||
var topHBox = new HBoxContainer
|
|
||||||
{
|
|
||||||
CustomMinimumSize = (0, 40),
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new MarginContainer
|
|
||||||
{
|
|
||||||
MarginLeftOverride = 8,
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new Label
|
|
||||||
{
|
|
||||||
Text = Loc.GetString("Lobby"),
|
|
||||||
StyleClasses = {StyleNano.StyleClassLabelHeadingBigger},
|
|
||||||
VAlign = Label.VAlignMode.Center
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(ServerName = new Label
|
|
||||||
{
|
|
||||||
StyleClasses = {StyleNano.StyleClassLabelHeadingBigger},
|
|
||||||
VAlign = Label.VAlignMode.Center,
|
|
||||||
SizeFlagsHorizontal = SizeFlags.Expand | SizeFlags.ShrinkCenter
|
|
||||||
}),
|
|
||||||
(OptionsButton = new Button
|
|
||||||
{
|
|
||||||
SizeFlagsHorizontal = SizeFlags.ShrinkEnd,
|
|
||||||
Text = Loc.GetString("Options"),
|
|
||||||
StyleClasses = {StyleNano.StyleClassButtonBig},
|
|
||||||
}),
|
|
||||||
(LeaveButton = new Button
|
|
||||||
{
|
|
||||||
SizeFlagsHorizontal = SizeFlags.ShrinkEnd,
|
|
||||||
Text = Loc.GetString("Leave"),
|
|
||||||
StyleClasses = {StyleNano.StyleClassButtonBig},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
vBox.AddChild(topHBox);
|
|
||||||
|
|
||||||
vBox.AddChild(new PanelContainer
|
|
||||||
{
|
|
||||||
PanelOverride = new StyleBoxFlat
|
|
||||||
{
|
|
||||||
BackgroundColor = StyleNano.NanoGold,
|
|
||||||
ContentMarginTopOverride = 2
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
var hBox = new HBoxContainer
|
|
||||||
{
|
|
||||||
SizeFlagsVertical = SizeFlags.FillExpand,
|
|
||||||
SeparationOverride = 0
|
|
||||||
};
|
|
||||||
vBox.AddChild(hBox);
|
|
||||||
|
|
||||||
CharacterPreview = new LobbyCharacterPreviewPanel(
|
|
||||||
entityManager,
|
|
||||||
preferencesManager)
|
|
||||||
{
|
|
||||||
SizeFlagsHorizontal = SizeFlags.None
|
|
||||||
};
|
|
||||||
hBox.AddChild(new VBoxContainer
|
|
||||||
{
|
|
||||||
SizeFlagsHorizontal = SizeFlags.FillExpand,
|
|
||||||
SeparationOverride = 0,
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
CharacterPreview,
|
|
||||||
|
|
||||||
new StripeBack
|
|
||||||
{
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new MarginContainer
|
|
||||||
{
|
|
||||||
MarginRightOverride = 3,
|
|
||||||
MarginLeftOverride = 3,
|
|
||||||
MarginTopOverride = 3,
|
|
||||||
MarginBottomOverride = 3,
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new HBoxContainer
|
|
||||||
{
|
|
||||||
SeparationOverride = 6,
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
(ObserveButton = new Button
|
|
||||||
{
|
|
||||||
Text = Loc.GetString("Observe"),
|
|
||||||
StyleClasses = {StyleNano.StyleClassButtonBig}
|
|
||||||
}),
|
|
||||||
(StartTime = new Label
|
|
||||||
{
|
|
||||||
SizeFlagsHorizontal = SizeFlags.FillExpand,
|
|
||||||
Align = Label.AlignMode.Right,
|
|
||||||
FontColorOverride = Color.DarkGray,
|
|
||||||
StyleClasses = {StyleNano.StyleClassLabelBig}
|
|
||||||
}),
|
|
||||||
(ReadyButton = new Button
|
|
||||||
{
|
|
||||||
ToggleMode = true,
|
|
||||||
Text = Loc.GetString("Ready Up"),
|
|
||||||
StyleClasses = {StyleNano.StyleClassButtonBig}
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
new MarginContainer
|
|
||||||
{
|
|
||||||
MarginRightOverride = 3,
|
|
||||||
MarginLeftOverride = 3,
|
|
||||||
MarginTopOverride = 3,
|
|
||||||
MarginBottomOverride = 3,
|
|
||||||
SizeFlagsVertical = SizeFlags.FillExpand,
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
(Chat = new ChatBox
|
|
||||||
{
|
|
||||||
Input = {PlaceHolder = Loc.GetString("Say something!")}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
hBox.AddChild(new PanelContainer
|
|
||||||
{
|
|
||||||
PanelOverride = new StyleBoxFlat {BackgroundColor = StyleNano.NanoGold}, CustomMinimumSize = (2, 0)
|
|
||||||
});
|
|
||||||
|
|
||||||
{
|
|
||||||
hBox.AddChild(new VBoxContainer
|
|
||||||
{
|
|
||||||
SizeFlagsHorizontal = SizeFlags.FillExpand,
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new NanoHeading
|
|
||||||
{
|
|
||||||
Text = Loc.GetString("Online Players"),
|
|
||||||
},
|
|
||||||
new MarginContainer
|
|
||||||
{
|
|
||||||
SizeFlagsVertical = SizeFlags.FillExpand,
|
|
||||||
MarginRightOverride = 3,
|
|
||||||
MarginLeftOverride = 3,
|
|
||||||
MarginTopOverride = 3,
|
|
||||||
MarginBottomOverride = 3,
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new HBoxContainer
|
|
||||||
{
|
|
||||||
SizeFlagsHorizontal = SizeFlags.FillExpand,
|
|
||||||
CustomMinimumSize = (50,50),
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
(OnlinePlayerList = new LobbyPlayerList
|
|
||||||
{
|
|
||||||
SizeFlagsVertical = SizeFlags.FillExpand,
|
|
||||||
SizeFlagsHorizontal = SizeFlags.FillExpand,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new NanoHeading
|
|
||||||
{
|
|
||||||
Text = Loc.GetString("Server Info"),
|
|
||||||
},
|
|
||||||
new MarginContainer
|
|
||||||
{
|
|
||||||
SizeFlagsVertical = SizeFlags.FillExpand,
|
|
||||||
MarginRightOverride = 3,
|
|
||||||
MarginLeftOverride = 3,
|
|
||||||
MarginTopOverride = 3,
|
|
||||||
MarginBottomOverride = 2,
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
(ServerInfo = new ServerInfo())
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class LobbyPlayerList : Control
|
|
||||||
{
|
|
||||||
private readonly ScrollContainer _scroll;
|
|
||||||
private readonly VBoxContainer _vBox;
|
|
||||||
|
|
||||||
public LobbyPlayerList()
|
|
||||||
{
|
|
||||||
var panel = new PanelContainer()
|
|
||||||
{
|
|
||||||
PanelOverride = new StyleBoxFlat { BackgroundColor = Color.FromHex("#202028") },
|
|
||||||
};
|
|
||||||
_vBox = new VBoxContainer();
|
|
||||||
_scroll = new ScrollContainer();
|
|
||||||
_scroll.AddChild(_vBox);
|
|
||||||
panel.AddChild(_scroll);
|
|
||||||
AddChild(panel);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adds a row
|
|
||||||
public void AddItem(string name, string status)
|
|
||||||
{
|
|
||||||
var hbox = new HBoxContainer
|
|
||||||
{
|
|
||||||
SizeFlagsHorizontal = SizeFlags.FillExpand,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Player Name
|
|
||||||
hbox.AddChild(new PanelContainer()
|
|
||||||
{
|
|
||||||
PanelOverride = new StyleBoxFlat
|
|
||||||
{
|
|
||||||
BackgroundColor = Color.FromHex("#373744"),
|
|
||||||
ContentMarginBottomOverride = 2,
|
|
||||||
ContentMarginLeftOverride = 4,
|
|
||||||
ContentMarginRightOverride = 4,
|
|
||||||
ContentMarginTopOverride = 2
|
|
||||||
},
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new Label
|
|
||||||
{
|
|
||||||
Text = name
|
|
||||||
}
|
|
||||||
},
|
|
||||||
SizeFlagsHorizontal = SizeFlags.FillExpand
|
|
||||||
});
|
|
||||||
// Status
|
|
||||||
hbox.AddChild(new PanelContainer()
|
|
||||||
{
|
|
||||||
PanelOverride = new StyleBoxFlat
|
|
||||||
{
|
|
||||||
BackgroundColor = Color.FromHex("#373744"),
|
|
||||||
ContentMarginBottomOverride = 2,
|
|
||||||
ContentMarginLeftOverride = 4,
|
|
||||||
ContentMarginRightOverride = 4,
|
|
||||||
ContentMarginTopOverride = 2
|
|
||||||
},
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new Label
|
|
||||||
{
|
|
||||||
Text = status
|
|
||||||
}
|
|
||||||
},
|
|
||||||
SizeFlagsHorizontal = SizeFlags.FillExpand,
|
|
||||||
SizeFlagsStretchRatio = 0.2f,
|
|
||||||
});
|
|
||||||
|
|
||||||
_vBox.AddChild(hbox);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deletes all rows
|
|
||||||
public void Clear()
|
|
||||||
{
|
|
||||||
_vBox.RemoveAllChildren();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
88
Content.Client/UserInterface/LobbyGui.xaml
Normal file
88
Content.Client/UserInterface/LobbyGui.xaml
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<Control xmlns="https://spacestation14.io"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||||
|
xmlns:style="clr-namespace:Content.Client.UserInterface.Stylesheets"
|
||||||
|
xmlns:cui="clr-namespace:Content.Client.UserInterface"
|
||||||
|
xmlns:chat="clr-namespace:Content.Client.Chat"
|
||||||
|
xmlns:maths="clr-namespace:Robust.Shared.Maths;assembly=Robust.Shared.Maths"
|
||||||
|
xmlns:voting="clr-namespace:Content.Client.Voting">
|
||||||
|
|
||||||
|
<!-- One day I'll code a Margin property for controls. -->
|
||||||
|
<MarginContainer MarginBottomOverride="20" MarginLeftOverride="20" MarginRightOverride="20"
|
||||||
|
MarginTopOverride="20">
|
||||||
|
<PanelContainer StyleClasses="AngleRect" />
|
||||||
|
<VBoxContainer>
|
||||||
|
<!-- Top row -->
|
||||||
|
<HBoxContainer CustomMinimumSize="0 40">
|
||||||
|
<MarginContainer MarginLeftOverride="8">
|
||||||
|
<Label StyleClasses="LabelHeadingBigger" VAlign="Center" Text="{Loc 'Lobby'}" />
|
||||||
|
</MarginContainer>
|
||||||
|
<Label Name="CServerName" StyleClasses="LabelHeadingBigger" VAlign="Center" />
|
||||||
|
<voting:VoteCallMenuButton Name="CCallVoteButton" StyleClasses="ButtonBig" />
|
||||||
|
<Button Name="COptionsButton" StyleClasses="ButtonBig" Text="{Loc 'Options'}" />
|
||||||
|
<Button Name="CLeaveButton" StyleClasses="ButtonBig" Text="{Loc 'Leave'}" />
|
||||||
|
</HBoxContainer>
|
||||||
|
<!-- Gold line -->
|
||||||
|
<PanelContainer>
|
||||||
|
<PanelContainer.PanelOverride>
|
||||||
|
<gfx:StyleBoxFlat BackgroundColor="{x:Static style:StyleNano.NanoGold}"
|
||||||
|
ContentMarginTopOverride="2" />
|
||||||
|
</PanelContainer.PanelOverride>
|
||||||
|
</PanelContainer>
|
||||||
|
<!-- Middle section with the two vertical panels -->
|
||||||
|
<HBoxContainer SizeFlagsVertical="FillExpand">
|
||||||
|
<!-- Left panel -->
|
||||||
|
<VBoxContainer Name="CLeftPanelContainer" SizeFlagsHorizontal="FillExpand">
|
||||||
|
<cui:StripeBack>
|
||||||
|
<MarginContainer MarginLeftOverride="3" MarginRightOverride="3" MarginBottomOverride="3"
|
||||||
|
MarginTopOverride="3">
|
||||||
|
<HBoxContainer SeparationOverride="6">
|
||||||
|
<Button Name="CObserveButton" Text="{Loc 'Observe'}" StyleClasses="ButtonBig" />
|
||||||
|
<Label Name="CStartTime" Align="Right"
|
||||||
|
FontColorOverride="{x:Static maths:Color.DarkGray}"
|
||||||
|
StyleClasses="LabelBig" SizeFlagsHorizontal="FillExpand" />
|
||||||
|
<Button Name="CReadyButton" ToggleMode="True" Text="{Loc 'Ready Up'}"
|
||||||
|
StyleClasses="ButtonBig" />
|
||||||
|
</HBoxContainer>
|
||||||
|
</MarginContainer>
|
||||||
|
</cui:StripeBack>
|
||||||
|
<MarginContainer SizeFlagsVertical="FillExpand" MarginLeftOverride="3" MarginRightOverride="3"
|
||||||
|
MarginBottomOverride="3"
|
||||||
|
MarginTopOverride="3">
|
||||||
|
<chat:ChatBox Name="CChat" />
|
||||||
|
</MarginContainer>
|
||||||
|
</VBoxContainer>
|
||||||
|
<!-- Gold line -->
|
||||||
|
<PanelContainer CustomMinimumSize="2 0">
|
||||||
|
<PanelContainer.PanelOverride>
|
||||||
|
<gfx:StyleBoxFlat BackgroundColor="{x:Static style:StyleNano.NanoGold}" />
|
||||||
|
</PanelContainer.PanelOverride>
|
||||||
|
</PanelContainer>
|
||||||
|
<!-- Right panel -->
|
||||||
|
<Control SizeFlagsHorizontal="FillExpand">
|
||||||
|
<VBoxContainer>
|
||||||
|
<!-- Player list -->
|
||||||
|
<cui:NanoHeading Text="{Loc 'Online Players'}" />
|
||||||
|
<MarginContainer SizeFlagsVertical="FillExpand"
|
||||||
|
MarginRightOverride="3" MarginLeftOverride="3"
|
||||||
|
MarginBottomOverride="3" MarginTopOverride="3">
|
||||||
|
<cui:LobbyPlayerList Name="COnlinePlayerList"
|
||||||
|
SizeFlagsHorizontal="FillExpand"
|
||||||
|
SizeFlagsVertical="FillExpand" />
|
||||||
|
</MarginContainer>
|
||||||
|
<!-- Server info -->
|
||||||
|
<cui:NanoHeading Text="{Loc 'Server Info'}" />
|
||||||
|
<MarginContainer SizeFlagsVertical="FillExpand"
|
||||||
|
MarginRightOverride="3" MarginLeftOverride="3"
|
||||||
|
MarginBottomOverride="2" MarginTopOverride="3">
|
||||||
|
<cui:ServerInfo Name="CServerInfo" />
|
||||||
|
</MarginContainer>
|
||||||
|
</VBoxContainer>
|
||||||
|
<MarginContainer SizeFlagsHorizontal="ShrinkEnd" MarginTopOverride="8" MarginRightOverride="8">
|
||||||
|
<VBoxContainer Name="CVoteContainer" />
|
||||||
|
</MarginContainer>
|
||||||
|
</Control>
|
||||||
|
</HBoxContainer>
|
||||||
|
</VBoxContainer>
|
||||||
|
</MarginContainer>
|
||||||
|
</Control>
|
||||||
124
Content.Client/UserInterface/LobbyGui.xaml.cs
Normal file
124
Content.Client/UserInterface/LobbyGui.xaml.cs
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
using Content.Client.Chat;
|
||||||
|
using Content.Client.Interfaces;
|
||||||
|
using Robust.Client.AutoGenerated;
|
||||||
|
using Robust.Client.Graphics;
|
||||||
|
using Robust.Client.UserInterface;
|
||||||
|
using Robust.Client.UserInterface.Controls;
|
||||||
|
using Robust.Client.UserInterface.XAML;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.Maths;
|
||||||
|
|
||||||
|
namespace Content.Client.UserInterface
|
||||||
|
{
|
||||||
|
[GenerateTypedNameReferences]
|
||||||
|
internal sealed partial class LobbyGui : Control
|
||||||
|
{
|
||||||
|
public Label ServerName => CServerName;
|
||||||
|
public Label StartTime => CStartTime;
|
||||||
|
public Button ReadyButton => CReadyButton;
|
||||||
|
public Button ObserveButton => CObserveButton;
|
||||||
|
public Button OptionsButton => COptionsButton;
|
||||||
|
public Button LeaveButton => CLeaveButton;
|
||||||
|
public ChatBox Chat => CChat;
|
||||||
|
public VBoxContainer VoteContainer => CVoteContainer;
|
||||||
|
public LobbyPlayerList OnlinePlayerList => COnlinePlayerList;
|
||||||
|
public ServerInfo ServerInfo => CServerInfo;
|
||||||
|
public LobbyCharacterPreviewPanel CharacterPreview { get; }
|
||||||
|
|
||||||
|
public LobbyGui(IEntityManager entityManager,
|
||||||
|
IClientPreferencesManager preferencesManager)
|
||||||
|
{
|
||||||
|
RobustXamlLoader.Load(this);
|
||||||
|
|
||||||
|
ServerName.SizeFlagsHorizontal = SizeFlags.Expand | SizeFlags.ShrinkCenter;
|
||||||
|
|
||||||
|
CharacterPreview = new LobbyCharacterPreviewPanel(
|
||||||
|
entityManager,
|
||||||
|
preferencesManager)
|
||||||
|
{
|
||||||
|
SizeFlagsHorizontal = SizeFlags.None
|
||||||
|
};
|
||||||
|
|
||||||
|
CLeftPanelContainer.AddChild(CharacterPreview);
|
||||||
|
CharacterPreview.SetPositionFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LobbyPlayerList : Control
|
||||||
|
{
|
||||||
|
private readonly ScrollContainer _scroll;
|
||||||
|
private readonly VBoxContainer _vBox;
|
||||||
|
|
||||||
|
public LobbyPlayerList()
|
||||||
|
{
|
||||||
|
var panel = new PanelContainer()
|
||||||
|
{
|
||||||
|
PanelOverride = new StyleBoxFlat {BackgroundColor = Color.FromHex("#202028")},
|
||||||
|
};
|
||||||
|
_vBox = new VBoxContainer();
|
||||||
|
_scroll = new ScrollContainer();
|
||||||
|
_scroll.AddChild(_vBox);
|
||||||
|
panel.AddChild(_scroll);
|
||||||
|
AddChild(panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds a row
|
||||||
|
public void AddItem(string name, string status)
|
||||||
|
{
|
||||||
|
var hbox = new HBoxContainer
|
||||||
|
{
|
||||||
|
SizeFlagsHorizontal = SizeFlags.FillExpand,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Player Name
|
||||||
|
hbox.AddChild(new PanelContainer()
|
||||||
|
{
|
||||||
|
PanelOverride = new StyleBoxFlat
|
||||||
|
{
|
||||||
|
BackgroundColor = Color.FromHex("#373744"),
|
||||||
|
ContentMarginBottomOverride = 2,
|
||||||
|
ContentMarginLeftOverride = 4,
|
||||||
|
ContentMarginRightOverride = 4,
|
||||||
|
ContentMarginTopOverride = 2
|
||||||
|
},
|
||||||
|
Children =
|
||||||
|
{
|
||||||
|
new Label
|
||||||
|
{
|
||||||
|
Text = name
|
||||||
|
}
|
||||||
|
},
|
||||||
|
SizeFlagsHorizontal = SizeFlags.FillExpand
|
||||||
|
});
|
||||||
|
// Status
|
||||||
|
hbox.AddChild(new PanelContainer()
|
||||||
|
{
|
||||||
|
PanelOverride = new StyleBoxFlat
|
||||||
|
{
|
||||||
|
BackgroundColor = Color.FromHex("#373744"),
|
||||||
|
ContentMarginBottomOverride = 2,
|
||||||
|
ContentMarginLeftOverride = 4,
|
||||||
|
ContentMarginRightOverride = 4,
|
||||||
|
ContentMarginTopOverride = 2
|
||||||
|
},
|
||||||
|
Children =
|
||||||
|
{
|
||||||
|
new Label
|
||||||
|
{
|
||||||
|
Text = status
|
||||||
|
}
|
||||||
|
},
|
||||||
|
SizeFlagsHorizontal = SizeFlags.FillExpand,
|
||||||
|
SizeFlagsStretchRatio = 0.2f,
|
||||||
|
});
|
||||||
|
|
||||||
|
_vBox.AddChild(hbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deletes all rows
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
_vBox.RemoveAllChildren();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ using Content.Client.Utility;
|
|||||||
using Robust.Client.Graphics;
|
using Robust.Client.Graphics;
|
||||||
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.CustomControls;
|
||||||
using Robust.Shared.Maths;
|
using Robust.Shared.Maths;
|
||||||
|
|
||||||
namespace Content.Client.UserInterface.Stylesheets
|
namespace Content.Client.UserInterface.Stylesheets
|
||||||
@@ -9,10 +11,13 @@ namespace Content.Client.UserInterface.Stylesheets
|
|||||||
public abstract class StyleBase
|
public abstract class StyleBase
|
||||||
{
|
{
|
||||||
public const string ClassHighDivider = "HighDivider";
|
public const string ClassHighDivider = "HighDivider";
|
||||||
|
public const string ClassLowDivider = "LowDivider";
|
||||||
public const string StyleClassLabelHeading = "LabelHeading";
|
public const string StyleClassLabelHeading = "LabelHeading";
|
||||||
public const string StyleClassLabelSubText = "LabelSubText";
|
public const string StyleClassLabelSubText = "LabelSubText";
|
||||||
public const string StyleClassItalic = "Italic";
|
public const string StyleClassItalic = "Italic";
|
||||||
|
|
||||||
|
public const string ClassAngleRect = "AngleRect";
|
||||||
|
|
||||||
public const string ButtonOpenRight = "OpenRight";
|
public const string ButtonOpenRight = "OpenRight";
|
||||||
public const string ButtonOpenLeft = "OpenLeft";
|
public const string ButtonOpenLeft = "OpenLeft";
|
||||||
public const string ButtonOpenBoth = "OpenBoth";
|
public const string ButtonOpenBoth = "OpenBoth";
|
||||||
@@ -30,10 +35,13 @@ namespace Content.Client.UserInterface.Stylesheets
|
|||||||
protected StyleBoxTexture BaseButtonOpenBoth { get; }
|
protected StyleBoxTexture BaseButtonOpenBoth { get; }
|
||||||
protected StyleBoxTexture BaseButtonSquare { get; }
|
protected StyleBoxTexture BaseButtonSquare { get; }
|
||||||
|
|
||||||
|
protected StyleBoxTexture BaseAngleRect { get; }
|
||||||
|
|
||||||
protected StyleBase(IResourceCache resCache)
|
protected StyleBase(IResourceCache resCache)
|
||||||
{
|
{
|
||||||
var notoSans12 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Regular.ttf", 12);
|
var notoSans12 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Regular.ttf", 12);
|
||||||
var notoSans12Italic = resCache.GetFont("/Fonts/NotoSans/NotoSans-Italic.ttf", 12);
|
var notoSans12Italic = resCache.GetFont("/Fonts/NotoSans/NotoSans-Italic.ttf", 12);
|
||||||
|
var textureCloseButton = resCache.GetTexture("/Textures/Interface/Nano/cross.svg.png");
|
||||||
|
|
||||||
// Button styles.
|
// Button styles.
|
||||||
var buttonTex = resCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png");
|
var buttonTex = resCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png");
|
||||||
@@ -80,6 +88,12 @@ namespace Content.Client.UserInterface.Stylesheets
|
|||||||
BaseButtonSquare.SetPadding(StyleBox.Margin.Right, 2);
|
BaseButtonSquare.SetPadding(StyleBox.Margin.Right, 2);
|
||||||
BaseButtonSquare.SetPadding(StyleBox.Margin.Left, 1);
|
BaseButtonSquare.SetPadding(StyleBox.Margin.Left, 1);
|
||||||
|
|
||||||
|
BaseAngleRect = new StyleBoxTexture
|
||||||
|
{
|
||||||
|
Texture = buttonTex,
|
||||||
|
};
|
||||||
|
BaseAngleRect.SetPatchMargin(StyleBox.Margin.All, 10);
|
||||||
|
|
||||||
BaseRules = new[]
|
BaseRules = new[]
|
||||||
{
|
{
|
||||||
// Default font.
|
// Default font.
|
||||||
@@ -97,6 +111,33 @@ namespace Content.Client.UserInterface.Stylesheets
|
|||||||
{
|
{
|
||||||
new StyleProperty("font", notoSans12Italic),
|
new StyleProperty("font", notoSans12Italic),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// Window close button base texture.
|
||||||
|
new StyleRule(
|
||||||
|
new SelectorElement(typeof(TextureButton), new[] {SS14Window.StyleClassWindowCloseButton}, null,
|
||||||
|
null),
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
new StyleProperty(TextureButton.StylePropertyTexture, textureCloseButton),
|
||||||
|
new StyleProperty(Control.StylePropertyModulateSelf, Color.FromHex("#4B596A")),
|
||||||
|
}),
|
||||||
|
// Window close button hover.
|
||||||
|
new StyleRule(
|
||||||
|
new SelectorElement(typeof(TextureButton), new[] {SS14Window.StyleClassWindowCloseButton}, null,
|
||||||
|
new[] {TextureButton.StylePseudoClassHover}),
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
new StyleProperty(Control.StylePropertyModulateSelf, Color.FromHex("#7F3636")),
|
||||||
|
}),
|
||||||
|
// Window close button pressed.
|
||||||
|
new StyleRule(
|
||||||
|
new SelectorElement(typeof(TextureButton), new[] {SS14Window.StyleClassWindowCloseButton}, null,
|
||||||
|
new[] {TextureButton.StylePseudoClassPressed}),
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
new StyleProperty(Control.StylePropertyModulateSelf, Color.FromHex("#753131")),
|
||||||
|
}),
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ namespace Content.Client.UserInterface.Stylesheets
|
|||||||
var notoSansBold16 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Bold.ttf", 16);
|
var notoSansBold16 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Bold.ttf", 16);
|
||||||
var notoSansBold18 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Bold.ttf", 18);
|
var notoSansBold18 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Bold.ttf", 18);
|
||||||
var notoSansBold20 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Bold.ttf", 20);
|
var notoSansBold20 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Bold.ttf", 20);
|
||||||
var textureCloseButton = resCache.GetTexture("/Textures/Interface/Nano/cross.svg.png");
|
|
||||||
var windowHeaderTex = resCache.GetTexture("/Textures/Interface/Nano/window_header.png");
|
var windowHeaderTex = resCache.GetTexture("/Textures/Interface/Nano/window_header.png");
|
||||||
var windowHeader = new StyleBoxTexture
|
var windowHeader = new StyleBoxTexture
|
||||||
{
|
{
|
||||||
@@ -428,31 +427,6 @@ namespace Content.Client.UserInterface.Stylesheets
|
|||||||
{
|
{
|
||||||
new StyleProperty(PanelContainer.StylePropertyPanel, windowHeader),
|
new StyleProperty(PanelContainer.StylePropertyPanel, windowHeader),
|
||||||
}),
|
}),
|
||||||
// Window close button base texture.
|
|
||||||
new StyleRule(
|
|
||||||
new SelectorElement(typeof(TextureButton), new[] {SS14Window.StyleClassWindowCloseButton}, null,
|
|
||||||
null),
|
|
||||||
new[]
|
|
||||||
{
|
|
||||||
new StyleProperty(TextureButton.StylePropertyTexture, textureCloseButton),
|
|
||||||
new StyleProperty(Control.StylePropertyModulateSelf, Color.FromHex("#4B596A")),
|
|
||||||
}),
|
|
||||||
// Window close button hover.
|
|
||||||
new StyleRule(
|
|
||||||
new SelectorElement(typeof(TextureButton), new[] {SS14Window.StyleClassWindowCloseButton}, null,
|
|
||||||
new[] {TextureButton.StylePseudoClassHover}),
|
|
||||||
new[]
|
|
||||||
{
|
|
||||||
new StyleProperty(Control.StylePropertyModulateSelf, Color.FromHex("#7F3636")),
|
|
||||||
}),
|
|
||||||
// Window close button pressed.
|
|
||||||
new StyleRule(
|
|
||||||
new SelectorElement(typeof(TextureButton), new[] {SS14Window.StyleClassWindowCloseButton}, null,
|
|
||||||
new[] {TextureButton.StylePseudoClassPressed}),
|
|
||||||
new[]
|
|
||||||
{
|
|
||||||
new StyleProperty(Control.StylePropertyModulateSelf, Color.FromHex("#753131")),
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Shapes for the buttons.
|
// Shapes for the buttons.
|
||||||
Element<ContainerButton>().Class(ContainerButton.StyleClassButton)
|
Element<ContainerButton>().Class(ContainerButton.StyleClassButton)
|
||||||
@@ -1061,7 +1035,12 @@ namespace Content.Client.UserInterface.Stylesheets
|
|||||||
new StyleRule(new SelectorElement(typeof(PanelContainer), new []{ ClassHighDivider}, null, null), new []
|
new StyleRule(new SelectorElement(typeof(PanelContainer), new []{ ClassHighDivider}, null, null), new []
|
||||||
{
|
{
|
||||||
new StyleProperty(PanelContainer.StylePropertyPanel, new StyleBoxFlat { BackgroundColor = NanoGold, ContentMarginBottomOverride = 2, ContentMarginLeftOverride = 2}),
|
new StyleProperty(PanelContainer.StylePropertyPanel, new StyleBoxFlat { BackgroundColor = NanoGold, ContentMarginBottomOverride = 2, ContentMarginLeftOverride = 2}),
|
||||||
})
|
}),
|
||||||
|
|
||||||
|
Element<PanelContainer>().Class(ClassAngleRect)
|
||||||
|
.Prop(PanelContainer.StylePropertyPanel, BaseAngleRect)
|
||||||
|
.Prop(Control.StylePropertyModulateSelf, Color.FromHex("#25252A")),
|
||||||
|
|
||||||
}).ToList());
|
}).ToList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,20 @@ namespace Content.Client.UserInterface.Stylesheets
|
|||||||
var notoSans10 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Regular.ttf", 10);
|
var notoSans10 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Regular.ttf", 10);
|
||||||
var notoSansBold16 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Bold.ttf", 16);
|
var notoSansBold16 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Bold.ttf", 16);
|
||||||
|
|
||||||
|
var progressBarBackground = new StyleBoxFlat
|
||||||
|
{
|
||||||
|
BackgroundColor = new Color(0.25f, 0.25f, 0.25f)
|
||||||
|
};
|
||||||
|
progressBarBackground.SetContentMarginOverride(StyleBox.Margin.Vertical, 5);
|
||||||
|
|
||||||
|
var progressBarForeground = new StyleBoxFlat
|
||||||
|
{
|
||||||
|
BackgroundColor = new Color(0.25f, 0.50f, 0.25f)
|
||||||
|
};
|
||||||
|
progressBarForeground.SetContentMarginOverride(StyleBox.Margin.Vertical, 5);
|
||||||
|
|
||||||
|
var textureInvertedTriangle = resCache.GetTexture("/Textures/Interface/Nano/inverted_triangle.svg.png");
|
||||||
|
|
||||||
Stylesheet = new Stylesheet(BaseRules.Concat(new StyleRule[]
|
Stylesheet = new Stylesheet(BaseRules.Concat(new StyleRule[]
|
||||||
{
|
{
|
||||||
Element<Label>().Class(StyleClassLabelHeading)
|
Element<Label>().Class(StyleClassLabelHeading)
|
||||||
@@ -45,6 +59,15 @@ namespace Content.Client.UserInterface.Stylesheets
|
|||||||
{
|
{
|
||||||
BackgroundColor = SpaceRed, ContentMarginBottomOverride = 2, ContentMarginLeftOverride = 2
|
BackgroundColor = SpaceRed, ContentMarginBottomOverride = 2, ContentMarginLeftOverride = 2
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
Element<PanelContainer>().Class(ClassLowDivider)
|
||||||
|
.Prop(PanelContainer.StylePropertyPanel, new StyleBoxFlat
|
||||||
|
{
|
||||||
|
BackgroundColor = Color.FromHex("#444"),
|
||||||
|
ContentMarginLeftOverride = 2,
|
||||||
|
ContentMarginBottomOverride = 2
|
||||||
|
}),
|
||||||
|
|
||||||
// Shapes for the buttons.
|
// Shapes for the buttons.
|
||||||
Element<ContainerButton>().Class(ContainerButton.StyleClassButton)
|
Element<ContainerButton>().Class(ContainerButton.StyleClassButton)
|
||||||
.Prop(ContainerButton.StylePropertyStyleBox, BaseButton),
|
.Prop(ContainerButton.StylePropertyStyleBox, BaseButton),
|
||||||
@@ -103,11 +126,42 @@ namespace Content.Client.UserInterface.Stylesheets
|
|||||||
Element<Label>().Class(ContainerButton.StyleClassButton)
|
Element<Label>().Class(ContainerButton.StyleClassButton)
|
||||||
.Prop(Label.StylePropertyAlignMode, Label.AlignMode.Center),
|
.Prop(Label.StylePropertyAlignMode, Label.AlignMode.Center),
|
||||||
|
|
||||||
|
Element<PanelContainer>().Class(ClassAngleRect)
|
||||||
|
.Prop(PanelContainer.StylePropertyPanel, BaseAngleRect)
|
||||||
|
.Prop(Control.StylePropertyModulateSelf, Color.FromHex("#202030")),
|
||||||
|
|
||||||
Child()
|
Child()
|
||||||
.Parent(Element<Button>().Class(ContainerButton.StylePseudoClassDisabled))
|
.Parent(Element<Button>().Class(ContainerButton.StylePseudoClassDisabled))
|
||||||
.Child(Element<Label>())
|
.Child(Element<Label>())
|
||||||
.Prop("font-color", Color.FromHex("#E5E5E581")),
|
.Prop("font-color", Color.FromHex("#E5E5E581")),
|
||||||
|
|
||||||
|
Element<ProgressBar>()
|
||||||
|
.Prop(ProgressBar.StylePropertyBackground, progressBarBackground)
|
||||||
|
.Prop(ProgressBar.StylePropertyForeground, progressBarForeground),
|
||||||
|
|
||||||
|
// OptionButton
|
||||||
|
Element<OptionButton>()
|
||||||
|
.Prop(ContainerButton.StylePropertyStyleBox, BaseButton),
|
||||||
|
|
||||||
|
Element<OptionButton>().Pseudo(ContainerButton.StylePseudoClassNormal)
|
||||||
|
.Prop(Control.StylePropertyModulateSelf, ButtonColorDefault),
|
||||||
|
|
||||||
|
Element<OptionButton>().Pseudo(ContainerButton.StylePseudoClassHover)
|
||||||
|
.Prop(Control.StylePropertyModulateSelf, ButtonColorHovered),
|
||||||
|
|
||||||
|
Element<OptionButton>().Pseudo(ContainerButton.StylePseudoClassPressed)
|
||||||
|
.Prop(Control.StylePropertyModulateSelf, ButtonColorPressed),
|
||||||
|
|
||||||
|
Element<OptionButton>().Pseudo(ContainerButton.StylePseudoClassDisabled)
|
||||||
|
.Prop(Control.StylePropertyModulateSelf, ButtonColorDisabled),
|
||||||
|
|
||||||
|
Element<TextureRect>().Class(OptionButton.StyleClassOptionTriangle)
|
||||||
|
.Prop(TextureRect.StylePropertyTexture, textureInvertedTriangle),
|
||||||
|
|
||||||
|
Element<Label>().Class(OptionButton.StyleClassOptionButton)
|
||||||
|
.Prop(Label.StylePropertyAlignMode, Label.AlignMode.Center),
|
||||||
|
|
||||||
|
|
||||||
}).ToList());
|
}).ToList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
9
Content.Client/UserInterface/TopNotification.cs
Normal file
9
Content.Client/UserInterface/TopNotification.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using Robust.Client.UserInterface;
|
||||||
|
|
||||||
|
namespace Content.Client.UserInterface
|
||||||
|
{
|
||||||
|
public class TopNotification : Control
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
39
Content.Client/Voting/VoteCallMenu.xaml
Normal file
39
Content.Client/Voting/VoteCallMenu.xaml
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<v:VoteCallMenu xmlns="https://spacestation14.io"
|
||||||
|
xmlns:cuic="clr-namespace:Content.Client.UserInterface.Controls"
|
||||||
|
xmlns:v="clr-namespace:Content.Client.Voting"
|
||||||
|
MouseFilter="Stop" CustomMinimumSize="350 150">
|
||||||
|
<PanelContainer StyleClasses="AngleRect" />
|
||||||
|
<VBoxContainer>
|
||||||
|
<HBoxContainer>
|
||||||
|
<MarginContainer MarginLeftOverride="8" SizeFlagsHorizontal="FillExpand">
|
||||||
|
<Label Text="{Loc 'Call Vote'}" VAlign="Center" StyleClasses="LabelHeading" />
|
||||||
|
</MarginContainer>
|
||||||
|
<MarginContainer MarginRightOverride="8">
|
||||||
|
<TextureButton Name="CloseButton" StyleClasses="windowCloseButton"
|
||||||
|
SizeFlagsVertical="ShrinkCenter" />
|
||||||
|
</MarginContainer>
|
||||||
|
</HBoxContainer>
|
||||||
|
<cuic:HighDivider />
|
||||||
|
|
||||||
|
<MarginContainer SizeFlagsHorizontal="Fill" SizeFlagsVertical="Expand"
|
||||||
|
MarginLeftOverride="8" MarginRightOverride="8" MarginTopOverride="2">
|
||||||
|
<HBoxContainer>
|
||||||
|
<OptionButton Name="VoteTypeButton" SizeFlagsHorizontal="FillExpand" />
|
||||||
|
<Control SizeFlagsHorizontal="FillExpand">
|
||||||
|
<OptionButton Name="VoteSecondButton" Visible="False" />
|
||||||
|
</Control>
|
||||||
|
</HBoxContainer>
|
||||||
|
</MarginContainer>
|
||||||
|
|
||||||
|
<MarginContainer SizeFlagsHorizontal="Fill"
|
||||||
|
MarginLeftOverride="8" MarginRightOverride="8" MarginBottomOverride="2">
|
||||||
|
<Button Name="CreateButton" Text="{Loc 'Call Vote'}" />
|
||||||
|
</MarginContainer>
|
||||||
|
|
||||||
|
<PanelContainer StyleClasses="LowDivider" />
|
||||||
|
<MarginContainer MarginLeftOverride="12">
|
||||||
|
<Label StyleClasses="LabelSubText" Text="{Loc 'Powered by Robust™ Anti-Tamper Technology'}" />
|
||||||
|
</MarginContainer>
|
||||||
|
|
||||||
|
</VBoxContainer>
|
||||||
|
</v:VoteCallMenu>
|
||||||
111
Content.Client/Voting/VoteCallMenu.xaml.cs
Normal file
111
Content.Client/Voting/VoteCallMenu.xaml.cs
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
#nullable enable
|
||||||
|
using Content.Client.UserInterface.Stylesheets;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Robust.Client.AutoGenerated;
|
||||||
|
using Robust.Client.Console;
|
||||||
|
using Robust.Client.UserInterface.Controls;
|
||||||
|
using Robust.Client.UserInterface.CustomControls;
|
||||||
|
using Robust.Client.UserInterface.XAML;
|
||||||
|
using Robust.Shared.Console;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
using Robust.Shared.Localization;
|
||||||
|
using Robust.Shared.Maths;
|
||||||
|
|
||||||
|
namespace Content.Client.Voting
|
||||||
|
{
|
||||||
|
[GenerateTypedNameReferences]
|
||||||
|
public partial class VoteCallMenu : BaseWindow
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
|
||||||
|
|
||||||
|
public static readonly (string name, string id, (string name, string id)[]? secondaries)[] AvailableVoteTypes =
|
||||||
|
{
|
||||||
|
("Restart round", "restart", null),
|
||||||
|
("Next gamemode", "preset", null)
|
||||||
|
};
|
||||||
|
|
||||||
|
public VoteCallMenu()
|
||||||
|
{
|
||||||
|
IoCManager.InjectDependencies(this);
|
||||||
|
RobustXamlLoader.Load(this);
|
||||||
|
|
||||||
|
Stylesheet = IoCManager.Resolve<IStylesheetManager>().SheetSpace;
|
||||||
|
CloseButton.OnPressed += _ => Close();
|
||||||
|
|
||||||
|
for (var i = 0; i < AvailableVoteTypes.Length; i++)
|
||||||
|
{
|
||||||
|
var (text, _, _) = AvailableVoteTypes[i];
|
||||||
|
VoteTypeButton.AddItem(Loc.GetString(text), i);
|
||||||
|
}
|
||||||
|
|
||||||
|
VoteTypeButton.OnItemSelected += VoteTypeSelected;
|
||||||
|
VoteSecondButton.OnItemSelected += VoteSecondSelected;
|
||||||
|
CreateButton.OnPressed += CreatePressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreatePressed(BaseButton.ButtonEventArgs obj)
|
||||||
|
{
|
||||||
|
var typeId = VoteTypeButton.SelectedId;
|
||||||
|
var (_, typeKey, secondaries) = AvailableVoteTypes[typeId];
|
||||||
|
|
||||||
|
if (secondaries != null)
|
||||||
|
{
|
||||||
|
var secondaryId = VoteSecondButton.SelectedId;
|
||||||
|
var (_, secondKey) = secondaries[secondaryId];
|
||||||
|
|
||||||
|
_consoleHost.LocalShell.RemoteExecuteCommand($"createvote {typeKey} {secondKey}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_consoleHost.LocalShell.RemoteExecuteCommand($"createvote {typeKey}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void VoteSecondSelected(OptionButton.ItemSelectedEventArgs obj)
|
||||||
|
{
|
||||||
|
obj.Button.SelectId(obj.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void VoteTypeSelected(OptionButton.ItemSelectedEventArgs obj)
|
||||||
|
{
|
||||||
|
VoteTypeButton.SelectId(obj.Id);
|
||||||
|
|
||||||
|
var (_, _, options) = AvailableVoteTypes[obj.Id];
|
||||||
|
if (options == null)
|
||||||
|
{
|
||||||
|
VoteSecondButton.Visible = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
VoteSecondButton.Visible = true;
|
||||||
|
VoteSecondButton.Clear();
|
||||||
|
|
||||||
|
for (var i = 0; i < options.Length; i++)
|
||||||
|
{
|
||||||
|
var (text, _) = options[i];
|
||||||
|
VoteSecondButton.AddItem(Loc.GetString(text), i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override DragMode GetDragModeFor(Vector2 relativeMousePos)
|
||||||
|
{
|
||||||
|
return DragMode.Move;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
public sealed class VoteMenuCommand : IConsoleCommand
|
||||||
|
{
|
||||||
|
public string Command => "votemenu";
|
||||||
|
public string Description => "Opens the voting menu";
|
||||||
|
public string Help => "Usage: votemenu";
|
||||||
|
|
||||||
|
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||||
|
{
|
||||||
|
new VoteCallMenu().OpenCentered();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
49
Content.Client/Voting/VoteCallMenuButton.cs
Normal file
49
Content.Client/Voting/VoteCallMenuButton.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
using Robust.Client.UserInterface.Controls;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
using Robust.Shared.Localization;
|
||||||
|
|
||||||
|
namespace Content.Client.Voting
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// LITERALLY just a button that opens the vote call menu.
|
||||||
|
/// Automatically disables itself if the client cannot call votes.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class VoteCallMenuButton : Button
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IVoteManager _voteManager = default!;
|
||||||
|
|
||||||
|
public VoteCallMenuButton()
|
||||||
|
{
|
||||||
|
IoCManager.InjectDependencies(this);
|
||||||
|
|
||||||
|
Text = Loc.GetString("Call vote");
|
||||||
|
OnPressed += OnOnPressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnOnPressed(ButtonEventArgs obj)
|
||||||
|
{
|
||||||
|
var menu = new VoteCallMenu();
|
||||||
|
menu.OpenCentered();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void EnteredTree()
|
||||||
|
{
|
||||||
|
base.EnteredTree();
|
||||||
|
|
||||||
|
UpdateCanCall(_voteManager.CanCallVote);
|
||||||
|
_voteManager.CanCallVoteChanged += UpdateCanCall;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ExitedTree()
|
||||||
|
{
|
||||||
|
base.ExitedTree();
|
||||||
|
|
||||||
|
_voteManager.CanCallVoteChanged += UpdateCanCall;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateCanCall(bool canCall)
|
||||||
|
{
|
||||||
|
Disabled = !canCall;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
196
Content.Client/Voting/VoteManager.cs
Normal file
196
Content.Client/Voting/VoteManager.cs
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Content.Shared.Network.NetMessages;
|
||||||
|
using Robust.Client.Console;
|
||||||
|
using Robust.Client.UserInterface;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
using Robust.Shared.Network;
|
||||||
|
using Robust.Shared.Timing;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace Content.Client.Voting
|
||||||
|
{
|
||||||
|
public interface IVoteManager
|
||||||
|
{
|
||||||
|
void Initialize();
|
||||||
|
void SendCastVote(int voteId, int option);
|
||||||
|
void ClearPopupContainer();
|
||||||
|
void SetPopupContainer(Control container);
|
||||||
|
bool CanCallVote { get; }
|
||||||
|
event Action<bool> CanCallVoteChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class VoteManager : IVoteManager
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IClientNetManager _netManager = default!;
|
||||||
|
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||||
|
[Dependency] private readonly IClientConsoleHost _console = default!;
|
||||||
|
|
||||||
|
private readonly Dictionary<int, ActiveVote> _votes = new();
|
||||||
|
private readonly Dictionary<int, VotePopup> _votePopups = new();
|
||||||
|
private Control? _popupContainer;
|
||||||
|
|
||||||
|
public bool CanCallVote { get; private set; }
|
||||||
|
public event Action<bool>? CanCallVoteChanged;
|
||||||
|
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
_netManager.RegisterNetMessage<MsgVoteData>(MsgVoteData.NAME, ReceiveVoteData);
|
||||||
|
_netManager.RegisterNetMessage<MsgVoteCanCall>(MsgVoteCanCall.NAME, ReceiveVoteCanCall);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearPopupContainer()
|
||||||
|
{
|
||||||
|
if (_popupContainer == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!_popupContainer.Disposed)
|
||||||
|
{
|
||||||
|
foreach (var popup in _votePopups.Values)
|
||||||
|
{
|
||||||
|
popup.Orphan();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_votePopups.Clear();
|
||||||
|
_popupContainer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetPopupContainer(Control container)
|
||||||
|
{
|
||||||
|
if (_popupContainer != null)
|
||||||
|
{
|
||||||
|
ClearPopupContainer();
|
||||||
|
}
|
||||||
|
|
||||||
|
_popupContainer = container;
|
||||||
|
|
||||||
|
foreach (var (vId, vote) in _votes)
|
||||||
|
{
|
||||||
|
var popup = new VotePopup(vote);
|
||||||
|
|
||||||
|
_votePopups.Add(vId, popup);
|
||||||
|
_popupContainer.AddChild(popup);
|
||||||
|
popup.UpdateData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReceiveVoteData(MsgVoteData message)
|
||||||
|
{
|
||||||
|
var @new = false;
|
||||||
|
var voteId = message.VoteId;
|
||||||
|
if (!_votes.TryGetValue(voteId, out var existingVote))
|
||||||
|
{
|
||||||
|
if (!message.VoteActive)
|
||||||
|
{
|
||||||
|
// Got "vote inactive" for nonexistent vote???
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
@new = true;
|
||||||
|
|
||||||
|
// New vote from the server.
|
||||||
|
var vote = new ActiveVote(voteId)
|
||||||
|
{
|
||||||
|
Entries = message.Options
|
||||||
|
.Select(c => new VoteEntry(c.name))
|
||||||
|
.ToArray()
|
||||||
|
};
|
||||||
|
|
||||||
|
existingVote = vote;
|
||||||
|
_votes.Add(voteId, vote);
|
||||||
|
}
|
||||||
|
else if (!message.VoteActive)
|
||||||
|
{
|
||||||
|
// Remove gone vote.
|
||||||
|
_votes.Remove(voteId);
|
||||||
|
if (_votePopups.TryGetValue(voteId, out var toRemove))
|
||||||
|
{
|
||||||
|
toRemove.Orphan();
|
||||||
|
_votePopups.Remove(voteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update vote data from incoming.
|
||||||
|
if (message.IsYourVoteDirty)
|
||||||
|
existingVote.OurVote = message.YourVote;
|
||||||
|
// On the server, most of these params can't change.
|
||||||
|
// It can't hurt to just re-set this stuff since I'm lazy and the server is sending it anyways, so...
|
||||||
|
existingVote.Initiator = message.VoteInitiator;
|
||||||
|
existingVote.Title = message.VoteTitle;
|
||||||
|
existingVote.StartTime = _gameTiming.RealServerToLocal(message.StartTime);
|
||||||
|
existingVote.EndTime = _gameTiming.RealServerToLocal(message.EndTime);
|
||||||
|
|
||||||
|
// Logger.Debug($"{existingVote.StartTime}, {existingVote.EndTime}, {_gameTiming.RealTime}");
|
||||||
|
|
||||||
|
for (var i = 0; i < message.Options.Length; i++)
|
||||||
|
{
|
||||||
|
existingVote.Entries[i].Votes = message.Options[i].votes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@new && _popupContainer != null)
|
||||||
|
{
|
||||||
|
var popup = new VotePopup(existingVote);
|
||||||
|
|
||||||
|
_votePopups.Add(voteId, popup);
|
||||||
|
_popupContainer.AddChild(popup);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_votePopups.TryGetValue(voteId, out var ePopup))
|
||||||
|
{
|
||||||
|
ePopup.UpdateData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReceiveVoteCanCall(MsgVoteCanCall message)
|
||||||
|
{
|
||||||
|
if (CanCallVote == message.CanCall)
|
||||||
|
return;
|
||||||
|
|
||||||
|
CanCallVote = message.CanCall;
|
||||||
|
CanCallVoteChanged?.Invoke(CanCallVote);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendCastVote(int voteId, int option)
|
||||||
|
{
|
||||||
|
var data = _votes[voteId];
|
||||||
|
// Update immediately to avoid any funny reconciliation bugs.
|
||||||
|
// See also code in server side to avoid bulldozing this.
|
||||||
|
data.OurVote = option;
|
||||||
|
_console.LocalShell.RemoteExecuteCommand($"vote {voteId} {option}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ActiveVote
|
||||||
|
{
|
||||||
|
public VoteEntry[] Entries = default!;
|
||||||
|
|
||||||
|
// Both of these are local RealTime (converted at NetMsg receive).
|
||||||
|
public TimeSpan StartTime;
|
||||||
|
public TimeSpan EndTime;
|
||||||
|
public string Title = "";
|
||||||
|
public string Initiator = "";
|
||||||
|
public int? OurVote;
|
||||||
|
public int Id;
|
||||||
|
|
||||||
|
public ActiveVote(int voteId)
|
||||||
|
{
|
||||||
|
Id = voteId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VoteEntry
|
||||||
|
{
|
||||||
|
public string Text { get; }
|
||||||
|
public int Votes { get; set; }
|
||||||
|
|
||||||
|
public VoteEntry(string text)
|
||||||
|
{
|
||||||
|
Text = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
Content.Client/Voting/VotePopup.xaml
Normal file
19
Content.Client/Voting/VotePopup.xaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<ui:Control xmlns:ui="clr-namespace:Robust.Client.UserInterface;assembly=Robust.Client"
|
||||||
|
xmlns:uic="clr-namespace:Robust.Client.UserInterface.Controls;assembly=Robust.Client">
|
||||||
|
<uic:PanelContainer StyleClasses="AngleRect" />
|
||||||
|
<uic:MarginContainer MarginLeftOverride="4" MarginRightOverride="4" MarginTopOverride="4" MarginBottomOverride="4">
|
||||||
|
<uic:VBoxContainer>
|
||||||
|
<uic:Label Name="VoteCaller" />
|
||||||
|
<uic:Label Name="VoteTitle" />
|
||||||
|
|
||||||
|
<uic:GridContainer Columns="3" Name="VoteOptionsContainer" />
|
||||||
|
<uic:HBoxContainer>
|
||||||
|
<uic:MarginContainer SizeFlagsHorizontal="FillExpand" MarginLeftOverride="2" MarginRightOverride="2"
|
||||||
|
MarginTopOverride="2" MarginBottomOverride="2">
|
||||||
|
<uic:ProgressBar Name="TimeLeftBar" MinValue="0" MaxValue="1" />
|
||||||
|
</uic:MarginContainer>
|
||||||
|
<uic:Label Name="TimeLeftText" />
|
||||||
|
</uic:HBoxContainer>
|
||||||
|
</uic:VBoxContainer>
|
||||||
|
</uic:MarginContainer>
|
||||||
|
</ui:Control>
|
||||||
83
Content.Client/Voting/VotePopup.xaml.cs
Normal file
83
Content.Client/Voting/VotePopup.xaml.cs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
using System;
|
||||||
|
using Content.Client.UserInterface.Stylesheets;
|
||||||
|
using Robust.Client.AutoGenerated;
|
||||||
|
using Robust.Client.UserInterface;
|
||||||
|
using Robust.Client.UserInterface.Controls;
|
||||||
|
using Robust.Client.UserInterface.XAML;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
using Robust.Shared.Localization;
|
||||||
|
using Robust.Shared.Log;
|
||||||
|
using Robust.Shared.Maths;
|
||||||
|
using Robust.Shared.Timing;
|
||||||
|
|
||||||
|
namespace Content.Client.Voting
|
||||||
|
{
|
||||||
|
[GenerateTypedNameReferences]
|
||||||
|
public partial class VotePopup : Control
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||||
|
[Dependency] private readonly IVoteManager _voteManager = default!;
|
||||||
|
|
||||||
|
private readonly VoteManager.ActiveVote _vote;
|
||||||
|
private readonly Button[] _voteButtons;
|
||||||
|
|
||||||
|
public VotePopup(VoteManager.ActiveVote vote)
|
||||||
|
{
|
||||||
|
_vote = vote;
|
||||||
|
IoCManager.InjectDependencies(this);
|
||||||
|
RobustXamlLoader.Load(this);
|
||||||
|
|
||||||
|
Stylesheet = IoCManager.Resolve<IStylesheetManager>().SheetSpace;
|
||||||
|
|
||||||
|
Modulate = Color.White.WithAlpha(0.75f);
|
||||||
|
_voteButtons = new Button[vote.Entries.Length];
|
||||||
|
var group = new ButtonGroup();
|
||||||
|
|
||||||
|
for (var i = 0; i < _voteButtons.Length; i++)
|
||||||
|
{
|
||||||
|
var button = new Button
|
||||||
|
{
|
||||||
|
ToggleMode = true,
|
||||||
|
Group = group
|
||||||
|
};
|
||||||
|
_voteButtons[i] = button;
|
||||||
|
VoteOptionsContainer.AddChild(button);
|
||||||
|
var i1 = i;
|
||||||
|
button.OnPressed += _ => _voteManager.SendCastVote(vote.Id, i1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateData()
|
||||||
|
{
|
||||||
|
VoteTitle.Text = _vote.Title;
|
||||||
|
VoteCaller.Text = Loc.GetString("{0} called a vote:", _vote.Initiator);
|
||||||
|
|
||||||
|
for (var i = 0; i < _voteButtons.Length; i++)
|
||||||
|
{
|
||||||
|
var entry = _vote.Entries[i];
|
||||||
|
_voteButtons[i].Text = Loc.GetString("{0} ({1})", entry.Text, entry.Votes);
|
||||||
|
|
||||||
|
if (_vote.OurVote == i)
|
||||||
|
_voteButtons[i].Pressed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void FrameUpdate(FrameEventArgs args)
|
||||||
|
{
|
||||||
|
// Logger.Debug($"{_gameTiming.ServerTime}, {_vote.StartTime}, {_vote.EndTime}");
|
||||||
|
|
||||||
|
var curTime = _gameTiming.RealTime;
|
||||||
|
var timeLeft = _vote.EndTime - curTime;
|
||||||
|
if (timeLeft < TimeSpan.Zero)
|
||||||
|
timeLeft = TimeSpan.Zero;
|
||||||
|
|
||||||
|
// Round up a second.
|
||||||
|
timeLeft = TimeSpan.FromSeconds(Math.Ceiling(timeLeft.TotalSeconds));
|
||||||
|
|
||||||
|
TimeLeftBar.Value = Math.Min(1, (float) ((curTime.TotalSeconds - _vote.StartTime.TotalSeconds) /
|
||||||
|
(_vote.EndTime.TotalSeconds - _vote.StartTime.TotalSeconds)));
|
||||||
|
|
||||||
|
TimeLeftText.Text = $"{timeLeft:m\\:ss}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ using Content.Server.Interfaces.Chat;
|
|||||||
using Content.Server.Interfaces.GameTicking;
|
using Content.Server.Interfaces.GameTicking;
|
||||||
using Content.Server.Interfaces.PDA;
|
using Content.Server.Interfaces.PDA;
|
||||||
using Content.Server.Sandbox;
|
using Content.Server.Sandbox;
|
||||||
|
using Content.Server.Voting;
|
||||||
using Content.Shared.Actions;
|
using Content.Shared.Actions;
|
||||||
using Content.Shared.Kitchen;
|
using Content.Shared.Kitchen;
|
||||||
using Content.Shared.Alert;
|
using Content.Shared.Alert;
|
||||||
@@ -28,6 +29,7 @@ namespace Content.Server
|
|||||||
private IGameTicker _gameTicker;
|
private IGameTicker _gameTicker;
|
||||||
private EuiManager _euiManager;
|
private EuiManager _euiManager;
|
||||||
private StatusShell _statusShell;
|
private StatusShell _statusShell;
|
||||||
|
private IVoteManager _voteManager;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Init()
|
public override void Init()
|
||||||
@@ -55,6 +57,7 @@ namespace Content.Server
|
|||||||
|
|
||||||
_gameTicker = IoCManager.Resolve<IGameTicker>();
|
_gameTicker = IoCManager.Resolve<IGameTicker>();
|
||||||
_euiManager = IoCManager.Resolve<EuiManager>();
|
_euiManager = IoCManager.Resolve<EuiManager>();
|
||||||
|
_voteManager = IoCManager.Resolve<IVoteManager>();
|
||||||
|
|
||||||
IoCManager.Resolve<IServerNotifyManager>().Initialize();
|
IoCManager.Resolve<IServerNotifyManager>().Initialize();
|
||||||
IoCManager.Resolve<IChatManager>().Initialize();
|
IoCManager.Resolve<IChatManager>().Initialize();
|
||||||
@@ -73,6 +76,7 @@ namespace Content.Server
|
|||||||
IoCManager.Resolve<INodeGroupFactory>().Initialize();
|
IoCManager.Resolve<INodeGroupFactory>().Initialize();
|
||||||
IoCManager.Resolve<ISandboxManager>().Initialize();
|
IoCManager.Resolve<ISandboxManager>().Initialize();
|
||||||
IoCManager.Resolve<IAccentManager>().Initialize();
|
IoCManager.Resolve<IAccentManager>().Initialize();
|
||||||
|
_voteManager.Initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void PostInit()
|
public override void PostInit()
|
||||||
@@ -105,6 +109,7 @@ namespace Content.Server
|
|||||||
case ModUpdateLevel.PostEngine:
|
case ModUpdateLevel.PostEngine:
|
||||||
{
|
{
|
||||||
_euiManager.SendUpdates();
|
_euiManager.SendUpdates();
|
||||||
|
_voteManager.Update();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ using Content.Server.PDA;
|
|||||||
using Content.Server.Preferences;
|
using Content.Server.Preferences;
|
||||||
using Content.Server.Sandbox;
|
using Content.Server.Sandbox;
|
||||||
using Content.Server.Utility;
|
using Content.Server.Utility;
|
||||||
|
using Content.Server.Voting;
|
||||||
using Content.Shared.Actions;
|
using Content.Shared.Actions;
|
||||||
using Content.Shared.Interfaces;
|
using Content.Shared.Interfaces;
|
||||||
using Content.Shared.Kitchen;
|
using Content.Shared.Kitchen;
|
||||||
@@ -58,6 +59,7 @@ namespace Content.Server
|
|||||||
IoCManager.Register<IDeviceNetwork, DeviceNetwork>();
|
IoCManager.Register<IDeviceNetwork, DeviceNetwork>();
|
||||||
IoCManager.Register<EuiManager, EuiManager>();
|
IoCManager.Register<EuiManager, EuiManager>();
|
||||||
IoCManager.Register<IHolidayManager, HolidayManager>();
|
IoCManager.Register<IHolidayManager, HolidayManager>();
|
||||||
|
IoCManager.Register<IVoteManager, VoteManager>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
Content.Server/Voting/IVoteHandle.cs
Normal file
18
Content.Server/Voting/IVoteHandle.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using Robust.Server.Player;
|
||||||
|
|
||||||
|
namespace Content.Server.Voting
|
||||||
|
{
|
||||||
|
public interface IVoteHandle
|
||||||
|
{
|
||||||
|
int Id { get; }
|
||||||
|
string Title { get; }
|
||||||
|
string InitiatorText { get; }
|
||||||
|
bool Finished { get; }
|
||||||
|
bool Cancelled { get; }
|
||||||
|
|
||||||
|
event VoteFinishedEventHandler OnFinished;
|
||||||
|
bool IsValidOption(int optionId);
|
||||||
|
void CastVote(IPlayerSession session, int? optionId);
|
||||||
|
void Cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
22
Content.Server/Voting/IVoteManager.cs
Normal file
22
Content.Server/Voting/IVoteManager.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Robust.Server.Player;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace Content.Server.Voting
|
||||||
|
{
|
||||||
|
public interface IVoteManager
|
||||||
|
{
|
||||||
|
IEnumerable<IVoteHandle> ActiveVotes { get; }
|
||||||
|
bool TryGetVote(int voteId, [NotNullWhen(true)] out IVoteHandle? vote);
|
||||||
|
|
||||||
|
bool CanCallVote(IPlayerSession initiator);
|
||||||
|
void CreateRestartVote(IPlayerSession? initiator);
|
||||||
|
void CreatePresetVote(IPlayerSession? initiator);
|
||||||
|
IVoteHandle CreateVote(VoteOptions options);
|
||||||
|
|
||||||
|
void Initialize();
|
||||||
|
void Update();
|
||||||
|
}
|
||||||
|
}
|
||||||
210
Content.Server/Voting/VoteCommands.cs
Normal file
210
Content.Server/Voting/VoteCommands.cs
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using Content.Server.Administration;
|
||||||
|
using Content.Server.Interfaces.Chat;
|
||||||
|
using Content.Shared.Administration;
|
||||||
|
using Robust.Server.Player;
|
||||||
|
using Robust.Shared.Console;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
using Robust.Shared.Localization;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace Content.Server.Voting
|
||||||
|
{
|
||||||
|
[AnyCommand]
|
||||||
|
public sealed class CreateVoteCommand : IConsoleCommand
|
||||||
|
{
|
||||||
|
public string Command => "createvote";
|
||||||
|
public string Description => "Creates a vote";
|
||||||
|
public string Help => "Usage: createvote <'restart'|'preset'>";
|
||||||
|
|
||||||
|
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||||
|
{
|
||||||
|
if (args.Length != 1)
|
||||||
|
{
|
||||||
|
shell.WriteError("Need exactly one argument!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var type = args[0];
|
||||||
|
|
||||||
|
var mgr = IoCManager.Resolve<IVoteManager>();
|
||||||
|
|
||||||
|
if (shell.Player != null && !mgr.CanCallVote((IPlayerSession) shell.Player))
|
||||||
|
{
|
||||||
|
shell.WriteError("You can't call a vote right now!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case "restart":
|
||||||
|
mgr.CreateRestartVote((IPlayerSession?) shell.Player);
|
||||||
|
break;
|
||||||
|
case "preset":
|
||||||
|
mgr.CreatePresetVote((IPlayerSession?) shell.Player);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
shell.WriteError("Invalid vote type!");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[AdminCommand(AdminFlags.Fun)]
|
||||||
|
public sealed class CreateCustomCommand : IConsoleCommand
|
||||||
|
{
|
||||||
|
public string Command => "customvote";
|
||||||
|
public string Description => "Creates a custom vote";
|
||||||
|
public string Help => "customvote <title> <option1> <option2> [option3...]";
|
||||||
|
|
||||||
|
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||||
|
{
|
||||||
|
if (args.Length < 3 || args.Length > 10)
|
||||||
|
{
|
||||||
|
shell.WriteError("Need 3 to 10 arguments!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var title = args[0];
|
||||||
|
|
||||||
|
var mgr = IoCManager.Resolve<IVoteManager>();
|
||||||
|
|
||||||
|
var options = new VoteOptions
|
||||||
|
{
|
||||||
|
Title = title,
|
||||||
|
Duration = TimeSpan.FromSeconds(30),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (var i = 1; i < args.Length; i++)
|
||||||
|
{
|
||||||
|
options.Options.Add((args[i], i));
|
||||||
|
}
|
||||||
|
|
||||||
|
options.SetInitiatorOrServer((IPlayerSession?) shell.Player);
|
||||||
|
|
||||||
|
var vote = mgr.CreateVote(options);
|
||||||
|
|
||||||
|
vote.OnFinished += (_, eventArgs) =>
|
||||||
|
{
|
||||||
|
var chatMgr = IoCManager.Resolve<IChatManager>();
|
||||||
|
if (eventArgs.Winner == null)
|
||||||
|
{
|
||||||
|
var ties = string.Join(", ", eventArgs.Winners.Select(c => args[(int) c]));
|
||||||
|
chatMgr.DispatchServerAnnouncement(Loc.GetString("Tie between {0}!", ties));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
chatMgr.DispatchServerAnnouncement(Loc.GetString("{0} wins!", args[(int) eventArgs.Winner]));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[AnyCommand]
|
||||||
|
public sealed class VoteCommand : IConsoleCommand
|
||||||
|
{
|
||||||
|
public string Command => "vote";
|
||||||
|
public string Description => "Votes on an active vote";
|
||||||
|
public string Help => "vote <voteId> <option>";
|
||||||
|
|
||||||
|
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||||
|
{
|
||||||
|
if (shell.Player == null)
|
||||||
|
{
|
||||||
|
shell.WriteError("Must be a player");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.Length != 2)
|
||||||
|
{
|
||||||
|
shell.WriteError("Expected two arguments.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!int.TryParse(args[0], out var voteId))
|
||||||
|
{
|
||||||
|
shell.WriteError("Invalid vote ID");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!int.TryParse(args[1], out var voteOption))
|
||||||
|
{
|
||||||
|
shell.WriteError("Invalid vote options");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mgr = IoCManager.Resolve<IVoteManager>();
|
||||||
|
if (!mgr.TryGetVote(voteId, out var vote))
|
||||||
|
{
|
||||||
|
shell.WriteError("Invalid vote");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int? optionN;
|
||||||
|
if (voteOption == -1)
|
||||||
|
{
|
||||||
|
optionN = null;
|
||||||
|
}
|
||||||
|
else if (vote.IsValidOption(voteOption))
|
||||||
|
{
|
||||||
|
optionN = voteOption;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
shell.WriteError("Invalid option");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
vote.CastVote((IPlayerSession) shell.Player!, optionN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[AnyCommand]
|
||||||
|
public sealed class ListVotesCommand : IConsoleCommand
|
||||||
|
{
|
||||||
|
public string Command => "listvotes";
|
||||||
|
public string Description => "Lists currently active votes";
|
||||||
|
public string Help => "Usage: listvotes";
|
||||||
|
|
||||||
|
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||||
|
{
|
||||||
|
var mgr = IoCManager.Resolve<IVoteManager>();
|
||||||
|
|
||||||
|
foreach (var vote in mgr.ActiveVotes)
|
||||||
|
{
|
||||||
|
shell.WriteLine($"[{vote.Id}] {vote.InitiatorText}: {vote.Title}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[AdminCommand(AdminFlags.Admin)]
|
||||||
|
public sealed class CancelVoteCommand : IConsoleCommand
|
||||||
|
{
|
||||||
|
public string Command => "cancelvote";
|
||||||
|
public string Description => "Cancels an active vote";
|
||||||
|
public string Help => "Usage: cancelvote <id>\nYou can get the ID from the listvotes command.";
|
||||||
|
|
||||||
|
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||||
|
{
|
||||||
|
var mgr = IoCManager.Resolve<IVoteManager>();
|
||||||
|
|
||||||
|
if (args.Length < 1)
|
||||||
|
{
|
||||||
|
shell.WriteError("Missing ID");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!int.TryParse(args[0], out var id) || !mgr.TryGetVote(id, out var vote))
|
||||||
|
{
|
||||||
|
shell.WriteError("Invalid vote ID");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
vote.Cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
Content.Server/Voting/VoteFinishedEventArgs.cs
Normal file
26
Content.Server/Voting/VoteFinishedEventArgs.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace Content.Server.Voting
|
||||||
|
{
|
||||||
|
public sealed class VoteFinishedEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Null if stalemate.
|
||||||
|
/// </summary>
|
||||||
|
public readonly object? Winner;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Winners. More than one if there was a stalemate.
|
||||||
|
/// </summary>
|
||||||
|
public readonly ImmutableArray<object> Winners;
|
||||||
|
|
||||||
|
public VoteFinishedEventArgs(object? winner, ImmutableArray<object> winners)
|
||||||
|
{
|
||||||
|
Winner = winner;
|
||||||
|
Winners = winners;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
Content.Server/Voting/VoteFinishedEventHandler.cs
Normal file
7
Content.Server/Voting/VoteFinishedEventHandler.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace Content.Server.Voting
|
||||||
|
{
|
||||||
|
public delegate void VoteFinishedEventHandler(IVoteHandle sender, VoteFinishedEventArgs args);
|
||||||
|
public delegate void VoteCancelledEventHandler(IVoteHandle sender);
|
||||||
|
}
|
||||||
114
Content.Server/Voting/VoteManager.DefaultVotes.cs
Normal file
114
Content.Server/Voting/VoteManager.DefaultVotes.cs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
#nullable enable
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Robust.Server.Player;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
using Robust.Shared.Localization;
|
||||||
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
|
namespace Content.Server.Voting
|
||||||
|
{
|
||||||
|
public sealed partial class VoteManager
|
||||||
|
{
|
||||||
|
public void CreateRestartVote(IPlayerSession? initiator)
|
||||||
|
{
|
||||||
|
var alone = _playerManager.PlayerCount == 1 && initiator != null;
|
||||||
|
var options = new VoteOptions
|
||||||
|
{
|
||||||
|
Title = Loc.GetString("Restart round"),
|
||||||
|
Options =
|
||||||
|
{
|
||||||
|
(Loc.GetString("Yes"), true),
|
||||||
|
(Loc.GetString("No"), false)
|
||||||
|
},
|
||||||
|
Duration = alone
|
||||||
|
? TimeSpan.FromSeconds(10)
|
||||||
|
: TimeSpan.FromSeconds(30)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (alone)
|
||||||
|
options.InitiatorTimeout = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
|
WirePresetVoteInitiator(options, initiator);
|
||||||
|
|
||||||
|
var vote = CreateVote(options);
|
||||||
|
|
||||||
|
vote.OnFinished += (_, args) =>
|
||||||
|
{
|
||||||
|
if (args.Winner == null)
|
||||||
|
{
|
||||||
|
_chatManager.DispatchServerAnnouncement(Loc.GetString("Restart vote failed due to tie."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var win = (bool) args.Winner;
|
||||||
|
if (win)
|
||||||
|
{
|
||||||
|
_chatManager.DispatchServerAnnouncement(Loc.GetString("Restart vote succeeded."));
|
||||||
|
_ticker.RestartRound();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_chatManager.DispatchServerAnnouncement(Loc.GetString("Restart vote failed."));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (initiator != null)
|
||||||
|
{
|
||||||
|
// Cast yes vote if created the vote yourself.
|
||||||
|
vote.CastVote(initiator, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CreatePresetVote(IPlayerSession? initiator)
|
||||||
|
{
|
||||||
|
var presets = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["traitor"] = "Traitor",
|
||||||
|
["extended"] = "Extended",
|
||||||
|
["sandbox"] = "Sandbox",
|
||||||
|
["suspicion"] = "Suspicion"
|
||||||
|
};
|
||||||
|
|
||||||
|
var alone = _playerManager.PlayerCount == 1 && initiator != null;
|
||||||
|
var options = new VoteOptions
|
||||||
|
{
|
||||||
|
Title = Loc.GetString("Next gamemode"),
|
||||||
|
Duration = alone
|
||||||
|
? TimeSpan.FromSeconds(10)
|
||||||
|
: TimeSpan.FromSeconds(30)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (alone)
|
||||||
|
options.InitiatorTimeout = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
|
foreach (var (k, v) in presets)
|
||||||
|
{
|
||||||
|
options.Options.Add((Loc.GetString(v), k));
|
||||||
|
}
|
||||||
|
|
||||||
|
WirePresetVoteInitiator(options, initiator);
|
||||||
|
|
||||||
|
var vote = CreateVote(options);
|
||||||
|
|
||||||
|
vote.OnFinished += (_, args) =>
|
||||||
|
{
|
||||||
|
string picked;
|
||||||
|
if (args.Winner == null)
|
||||||
|
{
|
||||||
|
picked = (string) IoCManager.Resolve<IRobustRandom>().Pick(args.Winners);
|
||||||
|
_chatManager.DispatchServerAnnouncement(
|
||||||
|
Loc.GetString("Tie for gamemode vote! Picking... {0}", Loc.GetString(presets[picked])));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
picked = (string) args.Winner;
|
||||||
|
_chatManager.DispatchServerAnnouncement(
|
||||||
|
Loc.GetString("{0} won the gamemode vote!", Loc.GetString(presets[picked])));
|
||||||
|
}
|
||||||
|
|
||||||
|
_ticker.SetStartPreset(picked);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
428
Content.Server/Voting/VoteManager.cs
Normal file
428
Content.Server/Voting/VoteManager.cs
Normal file
@@ -0,0 +1,428 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Linq;
|
||||||
|
using Content.Server.Administration;
|
||||||
|
using Content.Server.Interfaces.Chat;
|
||||||
|
using Content.Server.Interfaces.GameTicking;
|
||||||
|
using Content.Shared.Administration;
|
||||||
|
using Content.Shared.Network.NetMessages;
|
||||||
|
using Content.Shared.Utility;
|
||||||
|
using Robust.Server.Player;
|
||||||
|
using Robust.Shared.Enums;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
using Robust.Shared.Localization;
|
||||||
|
using Robust.Shared.Network;
|
||||||
|
using Robust.Shared.Timing;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace Content.Server.Voting
|
||||||
|
{
|
||||||
|
public sealed partial class VoteManager : IVoteManager
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IServerNetManager _netManager = default!;
|
||||||
|
[Dependency] private readonly IGameTiming _timing = default!;
|
||||||
|
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||||
|
[Dependency] private readonly IChatManager _chatManager = default!;
|
||||||
|
[Dependency] private readonly IGameTicker _ticker = default!;
|
||||||
|
[Dependency] private readonly IAdminManager _adminMgr = default!;
|
||||||
|
|
||||||
|
private int _nextVoteId = 1;
|
||||||
|
|
||||||
|
private readonly Dictionary<int, VoteReg> _votes = new();
|
||||||
|
private readonly Dictionary<int, VoteHandle> _voteHandles = new();
|
||||||
|
|
||||||
|
private readonly Dictionary<NetUserId, TimeSpan> _voteTimeout = new();
|
||||||
|
private readonly HashSet<IPlayerSession> _playerCanCallVoteDirty = new();
|
||||||
|
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
_netManager.RegisterNetMessage<MsgVoteData>(MsgVoteData.NAME);
|
||||||
|
_netManager.RegisterNetMessage<MsgVoteCanCall>(MsgVoteCanCall.NAME);
|
||||||
|
|
||||||
|
_playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged;
|
||||||
|
_adminMgr.OnPermsChanged += AdminPermsChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AdminPermsChanged(AdminPermsChangedEventArgs obj)
|
||||||
|
{
|
||||||
|
DirtyCanCallVote(obj.Player);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PlayerManagerOnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.NewStatus == SessionStatus.InGame)
|
||||||
|
{
|
||||||
|
// Send current votes to newly connected players.
|
||||||
|
foreach (var voteReg in _votes.Values)
|
||||||
|
{
|
||||||
|
SendSingleUpdate(voteReg, e.Session);
|
||||||
|
}
|
||||||
|
|
||||||
|
DirtyCanCallVote(e.Session);
|
||||||
|
}
|
||||||
|
else if (e.NewStatus == SessionStatus.Disconnected)
|
||||||
|
{
|
||||||
|
// Clear votes from disconnected players.
|
||||||
|
foreach (var voteReg in _votes.Values)
|
||||||
|
{
|
||||||
|
CastVote(voteReg, e.Session, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CastVote(VoteReg v, IPlayerSession player, int? option)
|
||||||
|
{
|
||||||
|
if (!IsValidOption(v, option))
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(option), "Invalid vote option ID");
|
||||||
|
|
||||||
|
if (v.CastVotes.TryGetValue(player, out var existingOption))
|
||||||
|
{
|
||||||
|
v.Entries[existingOption].Votes -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option != null)
|
||||||
|
{
|
||||||
|
v.Entries[option.Value].Votes += 1;
|
||||||
|
v.CastVotes[player] = option.Value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
v.CastVotes.Remove(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
v.VotesDirty.Add(player);
|
||||||
|
v.Dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsValidOption(VoteReg voteReg, int? option)
|
||||||
|
{
|
||||||
|
return option == null || option >= 0 && option < voteReg.Entries.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Update()
|
||||||
|
{
|
||||||
|
// Handle active votes.
|
||||||
|
var remQueue = new RemQueue<int>();
|
||||||
|
foreach (var v in _votes.Values)
|
||||||
|
{
|
||||||
|
// Logger.Debug($"{_timing.ServerTime}");
|
||||||
|
if (_timing.RealTime >= v.EndTime)
|
||||||
|
EndVote(v);
|
||||||
|
|
||||||
|
if (v.Finished)
|
||||||
|
remQueue.Add(v.Id);
|
||||||
|
|
||||||
|
if (v.Dirty)
|
||||||
|
SendUpdates(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var id in remQueue)
|
||||||
|
{
|
||||||
|
_votes.Remove(id);
|
||||||
|
_voteHandles.Remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle player timeouts.
|
||||||
|
var timeoutRemQueue = new RemQueue<NetUserId>();
|
||||||
|
foreach (var (userId, timeout) in _voteTimeout)
|
||||||
|
{
|
||||||
|
if (timeout < _timing.RealTime)
|
||||||
|
timeoutRemQueue.Add(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var userId in timeoutRemQueue)
|
||||||
|
{
|
||||||
|
_voteTimeout.Remove(userId);
|
||||||
|
|
||||||
|
if (_playerManager.TryGetSessionById(userId, out var session))
|
||||||
|
DirtyCanCallVote(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle dirty canCallVotes.
|
||||||
|
foreach (var dirtyPlayer in _playerCanCallVoteDirty)
|
||||||
|
{
|
||||||
|
if (dirtyPlayer.Status != SessionStatus.Disconnected)
|
||||||
|
SendUpdateCanCallVote(dirtyPlayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
_playerCanCallVoteDirty.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IVoteHandle CreateVote(VoteOptions options)
|
||||||
|
{
|
||||||
|
var id = _nextVoteId++;
|
||||||
|
|
||||||
|
var entries = options.Options.Select(o => new VoteEntry(o.data, o.text)).ToArray();
|
||||||
|
|
||||||
|
var start = _timing.RealTime;
|
||||||
|
var end = start + options.Duration;
|
||||||
|
var reg = new VoteReg(id, entries, options.Title, options.InitiatorText,
|
||||||
|
options.InitiatorPlayer, start, end);
|
||||||
|
|
||||||
|
var handle = new VoteHandle(this, reg);
|
||||||
|
|
||||||
|
_votes.Add(id, reg);
|
||||||
|
_voteHandles.Add(id, handle);
|
||||||
|
|
||||||
|
if (options.InitiatorPlayer != null)
|
||||||
|
{
|
||||||
|
var timeout = options.InitiatorTimeout ?? options.Duration * 2;
|
||||||
|
_voteTimeout[options.InitiatorPlayer.UserId] = _timing.RealTime + timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
DirtyCanCallVoteAll();
|
||||||
|
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendUpdates(VoteReg v)
|
||||||
|
{
|
||||||
|
foreach (var player in _playerManager.GetAllPlayers())
|
||||||
|
{
|
||||||
|
SendSingleUpdate(v, player);
|
||||||
|
}
|
||||||
|
|
||||||
|
v.VotesDirty.Clear();
|
||||||
|
v.Dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendSingleUpdate(VoteReg v, IPlayerSession player)
|
||||||
|
{
|
||||||
|
var msg = _netManager.CreateNetMessage<MsgVoteData>();
|
||||||
|
|
||||||
|
msg.VoteId = v.Id;
|
||||||
|
msg.VoteActive = !v.Finished;
|
||||||
|
|
||||||
|
if (!v.Finished)
|
||||||
|
{
|
||||||
|
msg.VoteTitle = v.Title;
|
||||||
|
msg.VoteInitiator = v.InitiatorText;
|
||||||
|
msg.StartTime = v.StartTime;
|
||||||
|
msg.EndTime = v.EndTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v.CastVotes.TryGetValue(player, out var cast))
|
||||||
|
{
|
||||||
|
// Only send info for your vote IF IT CHANGED.
|
||||||
|
// Otherwise there would be a reconciliation b*g causing the UI to jump back and forth.
|
||||||
|
// (votes are not in simulation so can't use normal prediction/reconciliation sadly).
|
||||||
|
var dirty = v.VotesDirty.Contains(player);
|
||||||
|
msg.IsYourVoteDirty = dirty;
|
||||||
|
if (dirty)
|
||||||
|
{
|
||||||
|
msg.YourVote = (byte) cast;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.Options = new (ushort votes, string name)[v.Entries.Length];
|
||||||
|
for (var i = 0; i < msg.Options.Length; i++)
|
||||||
|
{
|
||||||
|
ref var entry = ref v.Entries[i];
|
||||||
|
msg.Options[i] = ((ushort) entry.Votes, entry.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
player.ConnectedClient.SendMessage(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DirtyCanCallVoteAll()
|
||||||
|
{
|
||||||
|
_playerCanCallVoteDirty.UnionWith(_playerManager.GetAllPlayers());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendUpdateCanCallVote(IPlayerSession player)
|
||||||
|
{
|
||||||
|
var msg = _netManager.CreateNetMessage<MsgVoteCanCall>();
|
||||||
|
msg.CanCall = CanCallVote(player);
|
||||||
|
|
||||||
|
_netManager.ServerSendMessage(msg, player.ConnectedClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanCallVote(IPlayerSession player)
|
||||||
|
{
|
||||||
|
// Admins can always call votes.
|
||||||
|
if (_adminMgr.HasAdminFlag(player, AdminFlags.Admin))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cannot start vote if vote is already active (as non-admin).
|
||||||
|
if (_votes.Count != 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !_voteTimeout.ContainsKey(player.UserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EndVote(VoteReg v)
|
||||||
|
{
|
||||||
|
if (v.Finished)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find winner or stalemate.
|
||||||
|
var winners = v.Entries
|
||||||
|
.GroupBy(e => e.Votes)
|
||||||
|
.OrderByDescending(g => g.Key)
|
||||||
|
.First()
|
||||||
|
.Select(e => e.Data)
|
||||||
|
.ToImmutableArray();
|
||||||
|
|
||||||
|
v.Finished = true;
|
||||||
|
v.Dirty = true;
|
||||||
|
var args = new VoteFinishedEventArgs(winners.Length == 1 ? winners[0] : null, winners);
|
||||||
|
v.OnFinished?.Invoke(_voteHandles[v.Id], args);
|
||||||
|
DirtyCanCallVoteAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelVote(VoteReg v)
|
||||||
|
{
|
||||||
|
if (v.Cancelled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
v.Cancelled = true;
|
||||||
|
v.Finished = true;
|
||||||
|
v.Dirty = true;
|
||||||
|
v.OnCancelled?.Invoke(_voteHandles[v.Id]);
|
||||||
|
DirtyCanCallVoteAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<IVoteHandle> ActiveVotes => _voteHandles.Values;
|
||||||
|
|
||||||
|
public bool TryGetVote(int voteId, [NotNullWhen(true)] out IVoteHandle? vote)
|
||||||
|
{
|
||||||
|
if (_voteHandles.TryGetValue(voteId, out var vHandle))
|
||||||
|
{
|
||||||
|
vote = vHandle;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
vote = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DirtyCanCallVote(IPlayerSession player)
|
||||||
|
{
|
||||||
|
_playerCanCallVoteDirty.Add(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Preset Votes
|
||||||
|
|
||||||
|
private void WirePresetVoteInitiator(VoteOptions options, IPlayerSession? player)
|
||||||
|
{
|
||||||
|
if (player != null)
|
||||||
|
{
|
||||||
|
options.SetInitiator(player);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
options.InitiatorText = Loc.GetString("The server");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Vote Data
|
||||||
|
|
||||||
|
private sealed class VoteReg
|
||||||
|
{
|
||||||
|
public readonly int Id;
|
||||||
|
public readonly Dictionary<IPlayerSession, int> CastVotes = new();
|
||||||
|
public readonly VoteEntry[] Entries;
|
||||||
|
public readonly string Title;
|
||||||
|
public readonly string InitiatorText;
|
||||||
|
public readonly TimeSpan StartTime;
|
||||||
|
public readonly TimeSpan EndTime;
|
||||||
|
public readonly HashSet<IPlayerSession> VotesDirty = new();
|
||||||
|
|
||||||
|
public bool Cancelled;
|
||||||
|
public bool Finished;
|
||||||
|
public bool Dirty = true;
|
||||||
|
|
||||||
|
public VoteFinishedEventHandler? OnFinished;
|
||||||
|
public VoteCancelledEventHandler? OnCancelled;
|
||||||
|
public IPlayerSession? Initiator { get; }
|
||||||
|
|
||||||
|
public VoteReg(int id, VoteEntry[] entries, string title, string initiatorText,
|
||||||
|
IPlayerSession? initiator, TimeSpan start, TimeSpan end)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
Entries = entries;
|
||||||
|
Title = title;
|
||||||
|
InitiatorText = initiatorText;
|
||||||
|
Initiator = initiator;
|
||||||
|
StartTime = start;
|
||||||
|
EndTime = end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct VoteEntry
|
||||||
|
{
|
||||||
|
public object Data;
|
||||||
|
public string Text;
|
||||||
|
public int Votes;
|
||||||
|
|
||||||
|
public VoteEntry(object data, string text)
|
||||||
|
{
|
||||||
|
Data = data;
|
||||||
|
Text = text;
|
||||||
|
Votes = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region IVoteHandle API surface
|
||||||
|
|
||||||
|
private sealed class VoteHandle : IVoteHandle
|
||||||
|
{
|
||||||
|
private readonly VoteManager _mgr;
|
||||||
|
private readonly VoteReg _reg;
|
||||||
|
|
||||||
|
public int Id => _reg.Id;
|
||||||
|
public string Title => _reg.Title;
|
||||||
|
public string InitiatorText => _reg.InitiatorText;
|
||||||
|
public bool Finished => _reg.Finished;
|
||||||
|
public bool Cancelled => _reg.Cancelled;
|
||||||
|
|
||||||
|
public event VoteFinishedEventHandler? OnFinished
|
||||||
|
{
|
||||||
|
add => _reg.OnFinished += value;
|
||||||
|
remove => _reg.OnFinished -= value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public event VoteCancelledEventHandler? OnCancelled
|
||||||
|
{
|
||||||
|
add => _reg.OnCancelled += value;
|
||||||
|
remove => _reg.OnCancelled -= value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public VoteHandle(VoteManager mgr, VoteReg reg)
|
||||||
|
{
|
||||||
|
_mgr = mgr;
|
||||||
|
_reg = reg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsValidOption(int optionId)
|
||||||
|
{
|
||||||
|
return _mgr.IsValidOption(_reg, optionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CastVote(IPlayerSession session, int? optionId)
|
||||||
|
{
|
||||||
|
_mgr.CastVote(_reg, session, optionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Cancel()
|
||||||
|
{
|
||||||
|
_mgr.CancelVote(_reg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
68
Content.Server/Voting/VoteOptions.cs
Normal file
68
Content.Server/Voting/VoteOptions.cs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Robust.Server.Player;
|
||||||
|
using Robust.Shared.Localization;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace Content.Server.Voting
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Options for creating a vote.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class VoteOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The text that is shown for "who called the vote".
|
||||||
|
/// </summary>
|
||||||
|
public string InitiatorText { get; set; } = "<placeholder>";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The player that started the vote. Used to keep track of player cooldowns to avoid vote spam.
|
||||||
|
/// </summary>
|
||||||
|
public IPlayerSession? InitiatorPlayer { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The shown title of the vote.
|
||||||
|
/// </summary>
|
||||||
|
public string Title { get; set; } = "<somebody forgot to fill this in lol>";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How long the vote lasts.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan Duration { get; set; } = TimeSpan.FromMinutes(1);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How long the initiator should be timed out from calling votes. Defaults to duration * 2;
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan? InitiatorTimeout { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The options of the vote. Each entry is a tuple of the player-shown text,
|
||||||
|
/// and a data object that can be used to keep track of options later.
|
||||||
|
/// </summary>
|
||||||
|
public List<(string text, object data)> Options { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets <see cref="InitiatorPlayer"/> and <see cref="InitiatorText"/>
|
||||||
|
/// by setting the latter to the player's name.
|
||||||
|
/// </summary>
|
||||||
|
public void SetInitiator(IPlayerSession player)
|
||||||
|
{
|
||||||
|
InitiatorPlayer = player;
|
||||||
|
InitiatorText = player.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetInitiatorOrServer(IPlayerSession? player)
|
||||||
|
{
|
||||||
|
if (player != null)
|
||||||
|
{
|
||||||
|
SetInitiator(player);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
InitiatorText = Loc.GetString("The server");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,5 +61,15 @@ namespace Content.Shared.Input
|
|||||||
public static readonly BoundKeyFunction Loadout7 = "Loadout7";
|
public static readonly BoundKeyFunction Loadout7 = "Loadout7";
|
||||||
public static readonly BoundKeyFunction Loadout8 = "Loadout8";
|
public static readonly BoundKeyFunction Loadout8 = "Loadout8";
|
||||||
public static readonly BoundKeyFunction Loadout9 = "Loadout9";
|
public static readonly BoundKeyFunction Loadout9 = "Loadout9";
|
||||||
|
public static readonly BoundKeyFunction Vote0 = "Vote0";
|
||||||
|
public static readonly BoundKeyFunction Vote1 = "Vote1";
|
||||||
|
public static readonly BoundKeyFunction Vote2 = "Vote2";
|
||||||
|
public static readonly BoundKeyFunction Vote3 = "Vote3";
|
||||||
|
public static readonly BoundKeyFunction Vote4 = "Vote4";
|
||||||
|
public static readonly BoundKeyFunction Vote5 = "Vote5";
|
||||||
|
public static readonly BoundKeyFunction Vote6 = "Vote6";
|
||||||
|
public static readonly BoundKeyFunction Vote7 = "Vote7";
|
||||||
|
public static readonly BoundKeyFunction Vote8 = "Vote8";
|
||||||
|
public static readonly BoundKeyFunction Vote9 = "Vote9";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
32
Content.Shared/Network/NetMessages/MsgVoteCanCall.cs
Normal file
32
Content.Shared/Network/NetMessages/MsgVoteCanCall.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using Lidgren.Network;
|
||||||
|
using Robust.Shared.Network;
|
||||||
|
|
||||||
|
namespace Content.Shared.Network.NetMessages
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Used to tell clients whether they are able to currently call votes.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MsgVoteCanCall : NetMessage
|
||||||
|
{
|
||||||
|
#region REQUIRED
|
||||||
|
|
||||||
|
public const MsgGroups GROUP = MsgGroups.Command;
|
||||||
|
public const string NAME = nameof(MsgVoteCanCall);
|
||||||
|
|
||||||
|
public MsgVoteCanCall(INetChannel channel) : base(NAME, GROUP) { }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public bool CanCall;
|
||||||
|
|
||||||
|
public override void ReadFromBuffer(NetIncomingMessage buffer)
|
||||||
|
{
|
||||||
|
CanCall = buffer.ReadBoolean();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void WriteToBuffer(NetOutgoingMessage buffer)
|
||||||
|
{
|
||||||
|
buffer.Write(CanCall);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
89
Content.Shared/Network/NetMessages/MsgVoteData.cs
Normal file
89
Content.Shared/Network/NetMessages/MsgVoteData.cs
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
using System;
|
||||||
|
using Lidgren.Network;
|
||||||
|
using Robust.Shared.Network;
|
||||||
|
|
||||||
|
namespace Content.Shared.Network.NetMessages
|
||||||
|
{
|
||||||
|
public sealed class MsgVoteData : NetMessage
|
||||||
|
{
|
||||||
|
#region REQUIRED
|
||||||
|
|
||||||
|
public const MsgGroups GROUP = MsgGroups.Command;
|
||||||
|
public const string NAME = nameof(MsgVoteData);
|
||||||
|
|
||||||
|
public MsgVoteData(INetChannel channel) : base(NAME, GROUP) { }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public int VoteId;
|
||||||
|
public bool VoteActive;
|
||||||
|
public string VoteTitle;
|
||||||
|
public string VoteInitiator;
|
||||||
|
public TimeSpan StartTime; // Server RealTime.
|
||||||
|
public TimeSpan EndTime; // Server RealTime.
|
||||||
|
public (ushort votes, string name)[] Options;
|
||||||
|
public bool IsYourVoteDirty;
|
||||||
|
public byte? YourVote;
|
||||||
|
|
||||||
|
public override void ReadFromBuffer(NetIncomingMessage buffer)
|
||||||
|
{
|
||||||
|
VoteId = buffer.ReadVariableInt32();
|
||||||
|
VoteActive = buffer.ReadBoolean();
|
||||||
|
buffer.ReadPadBits();
|
||||||
|
|
||||||
|
if (!VoteActive)
|
||||||
|
return;
|
||||||
|
|
||||||
|
VoteTitle = buffer.ReadString();
|
||||||
|
VoteInitiator = buffer.ReadString();
|
||||||
|
StartTime = TimeSpan.FromTicks(buffer.ReadInt64());
|
||||||
|
EndTime = TimeSpan.FromTicks(buffer.ReadInt64());
|
||||||
|
|
||||||
|
Options = new (ushort votes, string name)[buffer.ReadByte()];
|
||||||
|
for (var i = 0; i < Options.Length; i++)
|
||||||
|
{
|
||||||
|
Options[i] = (buffer.ReadUInt16(), buffer.ReadString());
|
||||||
|
}
|
||||||
|
|
||||||
|
IsYourVoteDirty = buffer.ReadBoolean();
|
||||||
|
if (IsYourVoteDirty)
|
||||||
|
{
|
||||||
|
YourVote = buffer.ReadBoolean() ? buffer.ReadByte() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void WriteToBuffer(NetOutgoingMessage buffer)
|
||||||
|
{
|
||||||
|
buffer.WriteVariableInt32(VoteId);
|
||||||
|
buffer.Write(VoteActive);
|
||||||
|
buffer.WritePadBits();
|
||||||
|
|
||||||
|
if (!VoteActive)
|
||||||
|
return;
|
||||||
|
|
||||||
|
buffer.Write(VoteTitle);
|
||||||
|
buffer.Write(VoteInitiator);
|
||||||
|
buffer.Write(StartTime.Ticks);
|
||||||
|
buffer.Write(EndTime.Ticks);
|
||||||
|
|
||||||
|
buffer.Write((byte) Options.Length);
|
||||||
|
foreach (var (votes, name) in Options)
|
||||||
|
{
|
||||||
|
buffer.Write(votes);
|
||||||
|
buffer.Write(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.Write(IsYourVoteDirty);
|
||||||
|
if (IsYourVoteDirty)
|
||||||
|
{
|
||||||
|
buffer.Write(YourVote.HasValue);
|
||||||
|
if (YourVote.HasValue)
|
||||||
|
{
|
||||||
|
buffer.Write(YourVote.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override NetDeliveryMethod DeliveryMethod => NetDeliveryMethod.ReliableOrdered;
|
||||||
|
}
|
||||||
|
}
|
||||||
76
Content.Shared/Utility/RemList.cs
Normal file
76
Content.Shared/Utility/RemList.cs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace Content.Shared.Utility
|
||||||
|
{
|
||||||
|
// It's a Remie Queue now.
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Simple helper struct for "iterate collection and have a queue of things to remove when you're done",
|
||||||
|
/// to avoid concurrent iteration/modification.
|
||||||
|
/// </summary>
|
||||||
|
public struct RemQueue<T>
|
||||||
|
{
|
||||||
|
public List<T>? List;
|
||||||
|
|
||||||
|
public void Add(T t)
|
||||||
|
{
|
||||||
|
List ??= new();
|
||||||
|
List.Add(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Enumerator GetEnumerator()
|
||||||
|
{
|
||||||
|
return new(List);
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Enumerator : IEnumerator<T>
|
||||||
|
{
|
||||||
|
private readonly bool _filled;
|
||||||
|
private List<T>.Enumerator _enumerator;
|
||||||
|
|
||||||
|
public Enumerator(List<T>? list)
|
||||||
|
{
|
||||||
|
if (list == null)
|
||||||
|
{
|
||||||
|
_filled = false;
|
||||||
|
_enumerator = default;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_filled = true;
|
||||||
|
_enumerator = list.GetEnumerator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool MoveNext()
|
||||||
|
{
|
||||||
|
if (!_filled)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _enumerator.MoveNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
void IEnumerator.Reset()
|
||||||
|
{
|
||||||
|
if (_filled)
|
||||||
|
{
|
||||||
|
((IEnumerator) _enumerator).Reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public T Current => _enumerator.Current;
|
||||||
|
|
||||||
|
object? IEnumerator.Current => Current;
|
||||||
|
|
||||||
|
void IDisposable.Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user