diff --git a/OpenRA.Game/Server/Server.cs b/OpenRA.Game/Server/Server.cs
index 12eaa25b39..6fe2655b29 100644
--- a/OpenRA.Game/Server/Server.cs
+++ b/OpenRA.Game/Server/Server.cs
@@ -791,9 +791,12 @@ namespace OpenRA.Server
// Make sure the written file is not valid
// TODO: storing a serverside replay on desync would be extremely useful
- recorder.Metadata = null;
+ if (recorder != null)
+ {
+ recorder.Metadata = null;
- recorder.Dispose();
+ recorder.Dispose();
+ }
// Stop the recording
recorder = null;
@@ -856,17 +859,14 @@ namespace OpenRA.Server
void RecordOrder(int frame, byte[] data, int from)
{
- if (recorder != null)
- {
- recorder.ReceiveFrame(from, frame, data);
+ recorder?.ReceiveFrame(from, frame, data);
- if (data.Length > 0 && data[0] == (byte)OrderType.SyncHash)
- {
- if (data.Length == Order.SyncHashOrderLength)
- HandleSyncOrder(frame, data);
- else
- Log.Write("server", $"Dropped sync order with length {data.Length} from client {from}. Expected length {Order.SyncHashOrderLength}.");
- }
+ if (data.Length > 0 && data[0] == (byte)OrderType.SyncHash)
+ {
+ if (data.Length == Order.SyncHashOrderLength)
+ HandleSyncOrder(frame, data);
+ else
+ Log.Write("server", $"Dropped sync order with length {data.Length} from client {from}. Expected length {Order.SyncHashOrderLength}.");
}
}
@@ -1163,6 +1163,16 @@ namespace OpenRA.Server
return LobbyInfo.ClientWithIndex(conn.PlayerIndex);
}
+ /// Does not check if client is admin
+ public bool CanKickClient(Session.Client kickee)
+ {
+ if (State != ServerState.GameStarted || kickee.IsObserver)
+ return true;
+
+ var player = worldPlayers.FirstOrDefault(p => p?.ClientIndex == kickee.Index);
+ return player != null && player.Outcome != WinState.Undefined;
+ }
+
public void DropClient(Connection toDrop)
{
lock (LobbyInfo)
@@ -1323,24 +1333,22 @@ namespace OpenRA.Server
foreach (var cmpi in Map.WorldActorInfo.TraitInfos())
cmpi.CreateServerPlayers(Map, LobbyInfo, worldPlayers, playerRandom);
- if (recorder != null)
+ gameInfo = new GameInformation
{
- gameInfo = new GameInformation
- {
- Mod = Game.ModData.Manifest.Id,
- Version = Game.ModData.Manifest.Metadata.Version,
- MapUid = Map.Uid,
- MapTitle = Map.Title,
- StartTimeUtc = DateTime.UtcNow,
- };
+ Mod = Game.ModData.Manifest.Id,
+ Version = Game.ModData.Manifest.Metadata.Version,
+ MapUid = Map.Uid,
+ MapTitle = Map.Title,
+ StartTimeUtc = DateTime.UtcNow,
+ };
- // Replay metadata should only include the playable players
- foreach (var p in worldPlayers)
- if (p != null)
- gameInfo.Players.Add(p);
+ // Replay metadata should only include the playable players
+ foreach (var p in worldPlayers)
+ if (p != null)
+ gameInfo.Players.Add(p);
+ if (recorder != null)
recorder.Metadata = new ReplayMetadata(gameInfo);
- }
SyncLobbyInfo();
diff --git a/OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs b/OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs
index dc71e0aba9..d21330d948 100644
--- a/OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs
+++ b/OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs
@@ -48,6 +48,9 @@ namespace OpenRA.Mods.Common.Server
[TranslationReference]
const string KickNone = "notification-kick-none";
+ [TranslationReference]
+ const string NoKickSelf = "notification-kick-self";
+
[TranslationReference]
const string NoKickGameStarted = "notification-no-kick-game-started";
@@ -794,7 +797,13 @@ namespace OpenRA.Mods.Common.Server
}
var kickClient = server.GetClient(kickConn);
- if (server.State == ServerState.GameStarted && !kickClient.IsObserver)
+ if (client == kickClient)
+ {
+ server.SendLocalizedMessageTo(conn, NoKickSelf);
+ return true;
+ }
+
+ if (!server.CanKickClient(kickClient))
{
server.SendLocalizedMessageTo(conn, NoKickGameStarted);
return true;
diff --git a/OpenRA.Mods.Common/Widgets/Logic/Ingame/GameInfoStatsLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Ingame/GameInfoStatsLogic.cs
index 64179c2735..e045464f4b 100644
--- a/OpenRA.Mods.Common/Widgets/Logic/Ingame/GameInfoStatsLogic.cs
+++ b/OpenRA.Mods.Common/Widgets/Logic/Ingame/GameInfoStatsLogic.cs
@@ -114,6 +114,22 @@ namespace OpenRA.Mods.Common.Widgets.Logic
.GroupBy(p => (world.LobbyInfo.ClientWithIndex(p.Player.ClientIndex) ?? new Session.Client()).Team)
.OrderByDescending(g => g.Sum(gg => gg.PlayerStatistics?.Experience ?? 0));
+ void KickAction(Session.Client client)
+ {
+ hideMenu(true);
+ ConfirmationDialogs.ButtonPrompt(modData,
+ title: KickTitle,
+ titleArguments: Translation.Arguments("player", client.Name),
+ text: KickPrompt,
+ onConfirm: () =>
+ {
+ orderManager.IssueOrder(Order.Command($"kick {client.Index} {false}"));
+ hideMenu(false);
+ },
+ onCancel: () => hideMenu(false),
+ confirmText: KickAccept);
+ }
+
foreach (var t in teams)
{
if (teams.Count() > 1)
@@ -165,6 +181,10 @@ namespace OpenRA.Mods.Common.Widgets.Logic
muteCheckbox.IsVisible = () => !pp.IsBot && client.State != Session.ClientState.Disconnected && pp.ClientIndex != orderManager.LocalClient?.Index;
muteCheckbox.GetTooltipText = () => muteCheckbox.IsChecked() ? unmuteTooltip : muteTooltip;
+ var kickButton = item.Get("KICK");
+ kickButton.IsVisible = () => Game.IsHost && client.Index != orderManager.LocalClient?.Index && client.State != Session.ClientState.Disconnected && pp.WinState != WinState.Undefined && !pp.IsBot;
+ kickButton.OnClick = () => KickAction(client);
+
playerPanel.AddChild(item);
}
}
@@ -198,21 +218,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
var kickButton = item.Get("KICK");
kickButton.IsVisible = () => Game.IsHost && client.Index != orderManager.LocalClient?.Index && client.State != Session.ClientState.Disconnected;
- kickButton.OnClick = () =>
- {
- hideMenu(true);
- ConfirmationDialogs.ButtonPrompt(modData,
- title: KickTitle,
- titleArguments: Translation.Arguments("player", client.Name),
- text: KickPrompt,
- onConfirm: () =>
- {
- orderManager.IssueOrder(Order.Command($"kick {client.Index} {false}"));
- hideMenu(false);
- },
- onCancel: () => hideMenu(false),
- confirmText: KickAccept);
- };
+ kickButton.OnClick = () => KickAction(client);
var muteCheckbox = item.Get("MUTE");
muteCheckbox.IsChecked = () => TextNotificationsManager.MutedPlayers[client.Index];
diff --git a/mods/cnc/chrome/ingame-infostats.yaml b/mods/cnc/chrome/ingame-infostats.yaml
index 0c444d41f9..9b103d4203 100644
--- a/mods/cnc/chrome/ingame-infostats.yaml
+++ b/mods/cnc/chrome/ingame-infostats.yaml
@@ -133,6 +133,19 @@ Container@SKIRMISH_STATS:
Checkmark: mute
Background: checkbox-toggle
TooltipContainer: TOOLTIP_CONTAINER
+ Button@KICK:
+ X: 485
+ Width: 25
+ Height: 25
+ Background: checkbox-toggle
+ TooltipContainer: TOOLTIP_CONTAINER
+ TooltipText: Kick this player
+ Children:
+ Image:
+ ImageCollection: lobby-bits
+ ImageName: kick
+ X: 7
+ Y: 7
Container@SPECTATOR_TEMPLATE:
Width: PARENT_RIGHT - 27
Height: 25
diff --git a/mods/common/chrome/ingame-infostats.yaml b/mods/common/chrome/ingame-infostats.yaml
index 6769210296..eb911296b8 100644
--- a/mods/common/chrome/ingame-infostats.yaml
+++ b/mods/common/chrome/ingame-infostats.yaml
@@ -130,6 +130,19 @@ Container@SKIRMISH_STATS:
Checkmark: mute
Background: checkbox-toggle
TooltipContainer: TOOLTIP_CONTAINER
+ Button@KICK:
+ X: 485
+ Width: 25
+ Height: 25
+ Background: checkbox-toggle
+ TooltipContainer: TOOLTIP_CONTAINER
+ TooltipText: Kick this player
+ Children:
+ Image:
+ ImageCollection: lobby-bits
+ ImageName: kick
+ X: 7
+ Y: 7
Container@SPECTATOR_TEMPLATE:
Width: PARENT_RIGHT - 26
Height: 25
diff --git a/mods/common/languages/en.ftl b/mods/common/languages/en.ftl
index 05043f5d6b..1d52a62864 100644
--- a/mods/common/languages/en.ftl
+++ b/mods/common/languages/en.ftl
@@ -64,8 +64,9 @@ notification-invalid-configuration-command = Invalid configuration command.
notification-admin-option = Only the host can set that option.
notification-error-number-teams = Number of teams could not be parsed: { $raw }
notification-admin-kick = Only the host can kick players.
+notification-kick-self = The host is not allowed to kick themselves.
notification-kick-none = No-one in that slot.
-notification-no-kick-game-started = Only spectators can be kicked after the game has started.
+notification-no-kick-game-started = Only spectators and defeated players can be kicked after the game has started.
notification-admin-clear-spawn = Only admins can clear spawn points.
notification-spawn-occupied = You cannot occupy the same spawn point as another player.
notification-spawn-locked = The spawn point is locked to another player slot.
diff --git a/mods/d2k/chrome/ingame-infostats.yaml b/mods/d2k/chrome/ingame-infostats.yaml
index 1043d9bb76..1f5310011d 100644
--- a/mods/d2k/chrome/ingame-infostats.yaml
+++ b/mods/d2k/chrome/ingame-infostats.yaml
@@ -132,6 +132,19 @@ Container@SKIRMISH_STATS:
Checkmark: mute
Background: checkbox-toggle
TooltipContainer: TOOLTIP_CONTAINER
+ Button@KICK:
+ X: 485
+ Width: 25
+ Height: 25
+ Background: checkbox-toggle
+ TooltipContainer: TOOLTIP_CONTAINER
+ TooltipText: Kick this player
+ Children:
+ Image:
+ ImageCollection: lobby-bits
+ ImageName: kick
+ X: 7
+ Y: 7
Container@SPECTATOR_TEMPLATE:
Width: PARENT_RIGHT - 27
Height: 25