* 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:
Pieter-Jan Briers
2021-02-16 15:07:17 +01:00
committed by GitHub
parent db290fd91e
commit cea87d6985
35 changed files with 2001 additions and 413 deletions

View File

@@ -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>();
} }
} }
} }

View File

@@ -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) =>
{ {

View File

@@ -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");

View File

@@ -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,

View File

@@ -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;

View 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>

View File

@@ -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)

View File

@@ -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";

View File

@@ -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();
}
}
}

View 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>

View 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();
}
}
}

View File

@@ -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")),
}),
}; };
} }
} }

View File

@@ -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());
} }
} }

View File

@@ -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());
} }
} }

View File

@@ -0,0 +1,9 @@
using Robust.Client.UserInterface;
namespace Content.Client.UserInterface
{
public class TopNotification : Control
{
}
}

View 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>

View 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();
}
}
}

View 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;
}
}
}

View 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;
}
}
}
}

View 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>

View 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}";
}
}
}

View File

@@ -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;
} }
} }

View File

@@ -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>();
} }
} }
} }

View 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();
}
}

View 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();
}
}

View 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();
}
}
}

View 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;
}
}
}

View 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);
}

View 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);
};
}
}
}

View 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
}
}

View 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");
}
}
}
}

View File

@@ -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";
} }
} }

View 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);
}
}
}

View 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;
}
}

View 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()
{
}
}
}
}