Compare commits

...

10 Commits

Author SHA1 Message Date
hacker
d40f5292b6 Updated version
Some checks failed
Continuous Integration / Linux (.NET 6.0) (push) Has been cancelled
Continuous Integration / Linux (mono) (push) Has been cancelled
Continuous Integration / Windows (.NET 6.0) (push) Has been cancelled
2024-07-10 00:55:06 +01:00
hacker
3a75add1ba Added Dockerfile 2024-07-10 00:55:04 +01:00
hacker
b3bcdeb4ec Log chat messages on the server 2024-07-10 00:33:09 +01:00
abcdefg30
dde39344a0 Add NotBefore<SpawnStartingUnitsInfo> to LuaScriptInfo
(cherry picked from commit 9f96d0c772)
2023-10-06 15:04:53 +03:00
Pavel Penev
386f691c2e Added a new helper method - a temporary fix
(cherry picked from commit d83e579dfe)
2023-09-27 11:09:34 +03:00
Gustas
24623eaa65 Close the ingame menu upon voting
(cherry picked from commit d5c940ba4c)
2023-09-27 10:47:20 +03:00
Gustas
dc89341634 Add vote kick
(cherry picked from commit 144e716cdf)
2023-09-27 10:47:17 +03:00
JovialFeline
8961b4986f Disable flak truck in Soviet-13, others 2023-09-22 12:26:50 +03:00
Gustas
cbf4207d22 Add backup ExplicitSequenceFilenames to update rules
(cherry picked from commit 29eaab59be)
2023-09-18 11:07:24 +03:00
penev92
c27bf85631 Bumped Eluant NuGet version
The new version fixes the windows 32-bit build not working.
2023-09-16 20:08:05 +02:00
33 changed files with 625 additions and 61 deletions

29
Dockerfile Normal file
View File

@@ -0,0 +1,29 @@
FROM mcr.microsoft.com/dotnet/sdk:6.0
RUN \
apt-get update; \
apt-get -y upgrade; \
apt-get install -y --no-install-recommends \
curl \
wget \
make \
python3 \
unzip \
mono-complete
RUN useradd -d /home/openra -m -s /sbin/nologin openra
WORKDIR /home/openra
COPY . .
RUN chown -R openra:openra .
USER openra
RUN make
EXPOSE 1234
ENTRYPOINT ["./launch-dedicated.sh"]

View File

@@ -562,6 +562,11 @@ namespace OpenRA
{
return new LineSplitEnumerator(str.AsSpan(), separator);
}
public static bool TryParseInt32Invariant(string s, out int i)
{
return int.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
}
}
public ref struct LineSplitEnumerator

View File

@@ -86,8 +86,9 @@ namespace OpenRA
static void JoinInner(OrderManager om)
{
// Refresh TextNotificationsManager before the game starts.
// Refresh static classes before the game starts.
TextNotificationsManager.Clear();
UnitOrders.Clear();
// HACK: The shellmap World and OrderManager are owned by the main menu's WorldRenderer instead of Game.
// This allows us to switch Game.OrderManager from the shellmap to the new network connection when joining

View File

@@ -20,11 +20,15 @@ namespace OpenRA.Network
{
public const int ChatMessageMaxLength = 2500;
public static int? KickVoteTarget { get; internal set; }
static Player FindPlayerByClient(this World world, Session.Client c)
{
return world.Players.FirstOrDefault(p => p.ClientIndex == c.Index && p.PlayerReference.Playable);
}
static bool OrderNotFromServerOrWorldIsReplay(int clientId, World world) => clientId != 0 || (world != null && world.IsReplay);
internal static void ProcessOrder(OrderManager orderManager, World world, int clientId, Order order)
{
switch (order.OrderString)
@@ -52,9 +56,7 @@ namespace OpenRA.Network
case "DisableChatEntry":
{
// Order must originate from the server
// Don't disable chat in replays
if (clientId != 0 || (world != null && world.IsReplay))
if (OrderNotFromServerOrWorldIsReplay(clientId, world))
break;
// Server may send MaxValue to indicate that it is disabled until further notice
@@ -66,6 +68,26 @@ namespace OpenRA.Network
break;
}
case "StartKickVote":
{
if (OrderNotFromServerOrWorldIsReplay(clientId, world))
break;
KickVoteTarget = (int)order.ExtraData;
break;
}
case "EndKickVote":
{
if (OrderNotFromServerOrWorldIsReplay(clientId, world))
break;
if (KickVoteTarget == (int)order.ExtraData)
KickVoteTarget = null;
break;
}
case "Chat":
{
var client = orderManager.LobbyInfo.ClientWithIndex(clientId);
@@ -365,5 +387,10 @@ namespace OpenRA.Network
if (world.OrderValidators.All(vo => vo.OrderValidation(orderManager, world, clientId, order)))
order.Subject.ResolveOrder(order);
}
public static void Clear()
{
KickVoteTarget = null;
}
}
}

View File

@@ -11,7 +11,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Linguini.Bundle" Version="0.5.0" />
<PackageReference Include="OpenRA-Eluant" Version="1.0.21" />
<PackageReference Include="OpenRA-Eluant" Version="1.0.22" />
<PackageReference Include="Mono.NAT" Version="3.0.4" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />

View File

@@ -148,6 +148,8 @@ namespace OpenRA.Server
GameInformation gameInfo;
readonly List<GameInformation.Player> worldPlayers = new();
readonly Stopwatch pingUpdated = Stopwatch.StartNew();
public readonly VoteKickTracker VoteKickTracker;
readonly PlayerMessageTracker playerMessageTracker;
public ServerState State
@@ -318,6 +320,7 @@ namespace OpenRA.Server
MapStatusCache = new MapStatusCache(modData, MapStatusChanged, type == ServerType.Dedicated && settings.EnableLintChecks);
playerMessageTracker = new PlayerMessageTracker(this, DispatchOrdersToClient, SendLocalizedMessageTo);
VoteKickTracker = new VoteKickTracker(this);
LobbyInfo = new Session
{
@@ -1009,6 +1012,9 @@ namespace OpenRA.Server
case "Chat":
{
var client = GetClient(conn);
Console.WriteLine($"[{DateTime.Now.ToString(Settings.TimestampFormat)}] {client.Name}: {o.TargetString}");
if (Type == ServerType.Local || !playerMessageTracker.IsPlayerAtFloodLimit(conn))
DispatchOrdersToClients(conn, 0, o.Serialize());
@@ -1163,15 +1169,8 @@ namespace OpenRA.Server
return LobbyInfo.ClientWithIndex(conn.PlayerIndex);
}
/// <summary>Does not check if client is admin.</summary>
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 bool HasClientWonOrLost(Session.Client client) =>
worldPlayers.FirstOrDefault(p => p?.ClientIndex == client.Index)?.Outcome != WinState.Undefined;
public void DropClient(Connection toDrop)
{

View File

@@ -0,0 +1,223 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System.Collections.Generic;
using System.Diagnostics;
using OpenRA.Network;
namespace OpenRA.Server
{
public sealed class VoteKickTracker
{
[TranslationReference("kickee")]
const string InsufficientVotes = "notification-insufficient-votes-to-kick";
[TranslationReference]
const string AlreadyVoted = "notification-kick-already-voted";
[TranslationReference("kicker", "kickee")]
const string VoteKickStarted = "notification-vote-kick-started";
[TranslationReference]
const string UnableToStartAVote = "notification-unable-to-start-a-vote";
[TranslationReference("kickee", "percentage")]
const string VoteKickProgress = "notification-vote-kick-in-progress";
[TranslationReference("kickee")]
const string VoteKickEnded = "notification-vote-kick-ended";
readonly Dictionary<int, bool> voteTracker = new();
readonly Dictionary<Session.Client, long> failedVoteKickers = new();
readonly Server server;
Stopwatch voteKickTimer;
(Session.Client Client, Connection Conn) kickee;
(Session.Client Client, Connection Conn) voteKickerStarter;
public VoteKickTracker(Server server)
{
this.server = server;
}
// Only admins and alive players can participate in a vote kick.
bool ClientHasPower(Session.Client client) => client.IsAdmin || (!client.IsObserver && !server.HasClientWonOrLost(client));
public void Tick()
{
if (voteKickTimer == null)
return;
if (!server.Conns.Contains(kickee.Conn))
{
EndKickVote();
return;
}
if (voteKickTimer.ElapsedMilliseconds > server.Settings.VoteKickTimer)
EndKickVoteAndBlockKicker();
}
public bool VoteKick(Connection conn, Session.Client kicker, Connection kickeeConn, Session.Client kickee, int kickeeID, bool vote)
{
var voteInProgress = voteKickTimer != null;
if (server.State != ServerState.GameStarted
|| (kickee.IsAdmin && server.Type != ServerType.Dedicated)
|| (!voteInProgress && !vote) // Disallow starting a vote with a downvote
|| (voteInProgress && this.kickee.Client != kickee) // Disallow starting new votes when one is already ongoing.
|| !ClientHasPower(kicker))
{
server.SendLocalizedMessageTo(conn, UnableToStartAVote);
return false;
}
short eligiblePlayers = 0;
var isKickeeOnline = false;
var adminIsDeadButOnline = false;
foreach (var c in server.Conns)
{
var client = server.GetClient(c);
if (client != kickee && ClientHasPower(client))
eligiblePlayers++;
if (c == kickeeConn)
isKickeeOnline = true;
if (client.IsAdmin && (client.IsObserver || server.HasClientWonOrLost(client)))
adminIsDeadButOnline = true;
}
if (!isKickeeOnline)
{
EndKickVote();
return false;
}
if (eligiblePlayers < 2 || (adminIsDeadButOnline && !kickee.IsAdmin && eligiblePlayers < 3))
{
if (!kickee.IsObserver && !server.HasClientWonOrLost(kickee))
{
// Vote kick cannot be the sole deciding factor for a game.
server.SendLocalizedMessageTo(conn, InsufficientVotes, Translation.Arguments("kickee", kickee.Name));
EndKickVote();
return false;
}
else if (vote)
{
// If only a single player is playing, allow him to kick observers.
EndKickVote(false);
return true;
}
}
if (!voteInProgress)
{
// Prevent vote kick spam abuse.
if (failedVoteKickers.TryGetValue(kicker, out var time))
{
if (time + server.Settings.VoteKickerCooldown > kickeeConn.ConnectionTimer.ElapsedMilliseconds)
{
server.SendLocalizedMessageTo(conn, UnableToStartAVote);
return false;
}
else
failedVoteKickers.Remove(kicker);
}
Log.Write("server", $"Vote kick started on {kickeeID}.");
voteKickTimer = Stopwatch.StartNew();
server.SendLocalizedMessage(VoteKickStarted, Translation.Arguments("kicker", kicker.Name, "kickee", kickee.Name));
server.DispatchServerOrdersToClients(new Order("StartKickVote", null, false) { ExtraData = (uint)kickeeID }.Serialize());
this.kickee = (kickee, kickeeConn);
voteKickerStarter = (kicker, conn);
}
if (!voteTracker.ContainsKey(conn.PlayerIndex))
voteTracker[conn.PlayerIndex] = vote;
else
{
server.SendLocalizedMessageTo(conn, AlreadyVoted, null);
return false;
}
short votesFor = 0;
short votesAgainst = 0;
foreach (var c in voteTracker)
{
if (c.Value)
votesFor++;
else
votesAgainst++;
}
// Include the kickee in eligeablePlayers, so that in a 2v2 or any other even team
// matchup one team could not vote out the other team's player.
if (ClientHasPower(kickee))
{
eligiblePlayers++;
votesAgainst++;
}
var votesNeeded = eligiblePlayers / 2 + 1;
server.SendLocalizedMessage(VoteKickProgress, Translation.Arguments(
"kickee", kickee.Name,
"percentage", votesFor * 100 / eligiblePlayers));
// If a player or players during a vote lose or disconnect, it is possible that a downvote will
// kick a client. Guard against that situation.
if (vote && (votesFor >= votesNeeded))
{
EndKickVote(false);
return true;
}
// End vote if it can never succeed.
if (eligiblePlayers - votesAgainst < votesNeeded)
{
EndKickVoteAndBlockKicker();
return false;
}
voteKickTimer.Restart();
return false;
}
void EndKickVoteAndBlockKicker()
{
// Make sure vote kick is in progress.
if (voteKickTimer == null)
return;
if (server.Conns.Contains(voteKickerStarter.Conn))
failedVoteKickers[voteKickerStarter.Client] = voteKickerStarter.Conn.ConnectionTimer.ElapsedMilliseconds;
EndKickVote();
}
void EndKickVote(bool sendMessage = true)
{
// Make sure vote kick is in progress.
if (voteKickTimer == null)
return;
if (sendMessage)
server.SendLocalizedMessage(VoteKickEnded, Translation.Arguments("kickee", kickee.Client.Name));
server.DispatchServerOrdersToClients(new Order("EndKickVote", null, false) { ExtraData = (uint)kickee.Client.Index }.Serialize());
voteKickTimer = null;
voteKickerStarter = (null, null);
kickee = (null, null);
voteTracker.Clear();
}
}
}

View File

@@ -114,6 +114,15 @@ namespace OpenRA
[Desc("Delay in milliseconds before players can send chat messages after flood was detected.")]
public int FloodLimitCooldown = 15000;
[Desc("Can players vote to kick other players?")]
public bool EnableVoteKick = true;
[Desc("After how much time in miliseconds should the vote kick fail after idling?")]
public int VoteKickTimer = 30000;
[Desc("If a vote kick was unsuccessful for how long should the player who started the vote not be able to start new votes?")]
public int VoteKickerCooldown = 120000;
public ServerSettings Clone()
{
return (ServerSettings)MemberwiseClone();

View File

@@ -20,7 +20,7 @@ namespace OpenRA.Mods.Common.Scripting
{
[TraitLocation(SystemActors.World)]
[Desc("Part of the new Lua API.")]
public class LuaScriptInfo : TraitInfo, Requires<SpawnMapActorsInfo>
public class LuaScriptInfo : TraitInfo, Requires<SpawnMapActorsInfo>, NotBefore<SpawnStartingUnitsInfo>
{
[Desc("File names with location relative to the map.")]
public readonly HashSet<string> Scripts = new();

View File

@@ -22,7 +22,7 @@ using S = OpenRA.Server.Server;
namespace OpenRA.Mods.Common.Server
{
public class LobbyCommands : ServerTrait, IInterpretCommand, INotifyServerStart, INotifyServerEmpty, IClientJoined
public class LobbyCommands : ServerTrait, IInterpretCommand, INotifyServerStart, INotifyServerEmpty, IClientJoined, OpenRA.Server.ITick
{
[TranslationReference]
const string CustomRules = "notification-custom-rules";
@@ -55,6 +55,9 @@ namespace OpenRA.Mods.Common.Server
const string NoKickGameStarted = "notification-no-kick-game-started";
[TranslationReference("admin", "player")]
const string AdminKicked = "notification-admin-kicked";
[TranslationReference("player")]
const string Kicked = "notification-kicked";
[TranslationReference("admin", "player")]
@@ -156,6 +159,9 @@ namespace OpenRA.Mods.Common.Server
[TranslationReference]
const string YouWereKicked = "notification-you-were-kicked";
[TranslationReference]
const string VoteKickDisabled = "notification-vote-kick-disabled";
readonly IDictionary<string, Func<S, Connection, Session.Client, string, bool>> commandHandlers = new Dictionary<string, Func<S, Connection, Session.Client, string, bool>>
{
{ "state", State },
@@ -170,6 +176,7 @@ namespace OpenRA.Mods.Common.Server
{ "option", Option },
{ "assignteams", AssignTeams },
{ "kick", Kick },
{ "vote_kick", VoteKick },
{ "make_admin", MakeAdmin },
{ "make_spectator", MakeSpectator },
{ "name", Name },
@@ -207,7 +214,7 @@ namespace OpenRA.Mods.Common.Server
lock (server.LobbyInfo)
{
// Kick command is always valid for the host
if (command.StartsWith("kick "))
if (command.StartsWith("kick ", StringComparison.Ordinal) || command.StartsWith("vote_kick ", StringComparison.Ordinal))
return true;
if (server.State == ServerState.GameStarted)
@@ -804,14 +811,14 @@ namespace OpenRA.Mods.Common.Server
return true;
}
if (!server.CanKickClient(kickClient))
if (server.State == ServerState.GameStarted && !kickClient.IsObserver && !server.HasClientWonOrLost(kickClient))
{
server.SendLocalizedMessageTo(conn, NoKickGameStarted);
return true;
}
Log.Write("server", $"Kicking client {kickClientID}.");
server.SendLocalizedMessage(Kicked, Translation.Arguments("admin", client.Name, "player", kickClient.Name));
server.SendLocalizedMessage(AdminKicked, Translation.Arguments("admin", client.Name, "player", kickClient.Name));
server.SendOrderTo(kickConn, "ServerError", YouWereKicked);
server.DropClient(kickConn);
@@ -829,6 +836,62 @@ namespace OpenRA.Mods.Common.Server
}
}
static bool VoteKick(S server, Connection conn, Session.Client client, string s)
{
lock (server.LobbyInfo)
{
var split = s.Split(' ');
if (split.Length != 2)
{
server.SendLocalizedMessageTo(conn, MalformedCommand, Translation.Arguments("command", "vote_kick"));
return true;
}
if (!server.Settings.EnableVoteKick)
{
server.SendLocalizedMessageTo(conn, VoteKickDisabled);
return true;
}
var kickConn = Exts.TryParseInt32Invariant(split[0], out var kickClientID)
? server.Conns.SingleOrDefault(c => server.GetClient(c)?.Index == kickClientID) : null;
if (kickConn == null)
{
server.SendLocalizedMessageTo(conn, KickNone);
return true;
}
var kickClient = server.GetClient(kickConn);
if (client == kickClient)
{
server.SendLocalizedMessageTo(conn, NoKickSelf);
return true;
}
if (!bool.TryParse(split[1], out var vote))
{
server.SendLocalizedMessageTo(conn, MalformedCommand, Translation.Arguments("command", "vote_kick"));
return true;
}
if (server.VoteKickTracker.VoteKick(conn, client, kickConn, kickClient, kickClientID, vote))
{
Log.Write("server", $"Kicking client {kickClientID}.");
server.SendLocalizedMessage(Kicked, Translation.Arguments("player", kickClient.Name));
server.SendOrderTo(kickConn, "ServerError", YouWereKicked);
server.DropClient(kickConn);
server.SyncLobbyClients();
server.SyncLobbySlots();
}
return true;
}
}
void OpenRA.Server.ITick.Tick(S server) => server.VoteKickTracker.Tick();
static bool MakeAdmin(S server, Connection conn, Session.Client client, string s)
{
lock (server.LobbyInfo)

View File

@@ -92,19 +92,73 @@ namespace OpenRA.Mods.Common.UpdateRules.Rules
defaultSpriteExtension = defaultSpriteExtensionNode.Value.Value;
}
var tilesetExtensionsNode = spriteSequenceFormatNode.LastChildMatching("TilesetExtensions");
var fromBackup = false;
var tilesetExtensionsNode = spriteSequenceFormatNode.LastChildMatching("TilesetExtensions")?.Value?.Nodes;
if (tilesetExtensionsNode == null)
{
switch (modData.Manifest.Id)
{
case "cnc":
fromBackup = true;
tilesetExtensionsNode = new List<MiniYamlNode>()
{
new MiniYamlNode("TEMPERAT", ".tem"),
new MiniYamlNode("SNOW", ".sno"),
new MiniYamlNode("INTERIOR", ".int"),
new MiniYamlNode("DESERT", ".des"),
new MiniYamlNode("JUNGLE", ".jun"),
};
break;
case "ra":
fromBackup = true;
tilesetExtensionsNode = new List<MiniYamlNode>()
{
new MiniYamlNode("TEMPERAT", ".tem"),
new MiniYamlNode("SNOW", ".sno"),
new MiniYamlNode("INTERIOR", ".int"),
new MiniYamlNode("DESERT", ".des"),
};
break;
case "ts":
fromBackup = true;
tilesetExtensionsNode = new List<MiniYamlNode>()
{
new MiniYamlNode("TEMPERATE", ".tem"),
new MiniYamlNode("SNOW", ".sno"),
};
break;
}
}
if (tilesetExtensionsNode != null)
{
reportModYamlChanges = true;
foreach (var n in tilesetExtensionsNode.Value.Nodes)
if (!fromBackup)
reportModYamlChanges = true;
foreach (var n in tilesetExtensionsNode)
tilesetExtensions[n.Key] = n.Value.Value;
}
var tilesetCodesNode = spriteSequenceFormatNode.LastChildMatching("TilesetCodes");
fromBackup = false;
var tilesetCodesNode = spriteSequenceFormatNode.LastChildMatching("TilesetCodes")?.Value?.Nodes;
if (tilesetCodesNode == null && modData.Manifest.Id == "ts")
{
fromBackup = true;
tilesetCodesNode = new List<MiniYamlNode>()
{
new MiniYamlNode("TEMPERATE", "t"),
new MiniYamlNode("SNOW", "a"),
};
}
if (tilesetCodesNode != null)
{
reportModYamlChanges = true;
foreach (var n in tilesetCodesNode.Value.Nodes)
if (!fromBackup)
reportModYamlChanges = true;
foreach (var n in tilesetCodesNode)
tilesetCodes[n.Key] = n.Value.Value;
}
}

View File

@@ -40,12 +40,13 @@ namespace OpenRA.Mods.Common.Widgets.Logic
readonly World world;
readonly ModData modData;
readonly Action<bool> hideMenu;
readonly Action closeMenu;
readonly IObjectivesPanel iop;
IngameInfoPanel activePanel;
readonly bool hasError;
[ObjectCreator.UseCtor]
public GameInfoLogic(Widget widget, ModData modData, World world, IngameInfoPanel initialPanel, Action<bool> hideMenu)
public GameInfoLogic(Widget widget, ModData modData, World world, IngameInfoPanel initialPanel, Action<bool> hideMenu, Action closeMenu)
{
var panels = new Dictionary<IngameInfoPanel, (string Panel, string Label, Action<ButtonWidget, Widget> Setup)>()
{
@@ -59,6 +60,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
this.world = world;
this.modData = modData;
this.hideMenu = hideMenu;
this.closeMenu = closeMenu;
activePanel = initialPanel;
var visiblePanels = new List<IngameInfoPanel>();
@@ -140,7 +142,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic
var panel = hasError ? "SCRIPT_ERROR_PANEL" : iop.PanelName;
Game.LoadWidget(world, panel, objectivesPanelContainer, new WidgetArgs()
{
{ "hideMenu", hideMenu }
{ "hideMenu", hideMenu },
{ "closeMenu", closeMenu },
});
}

View File

@@ -49,6 +49,9 @@ namespace OpenRA.Mods.Common.Widgets.Logic
[TranslationReference]
const string Gone = "label-client-state-disconnected";
[TranslationReference]
const string KickTooltip = "button-kick-player";
[TranslationReference("player")]
const string KickTitle = "dialog-kick.title";
@@ -58,8 +61,32 @@ namespace OpenRA.Mods.Common.Widgets.Logic
[TranslationReference]
const string KickAccept = "dialog-kick.confirm";
[TranslationReference]
const string KickVoteTooltip = "button-vote-kick-player";
[TranslationReference("player")]
const string VoteKickTitle = "dialog-vote-kick.title";
[TranslationReference]
const string VoteKickPrompt = "dialog-vote-kick.prompt";
[TranslationReference("bots")]
const string VoteKickPromptBreakBots = "dialog-vote-kick.prompt-break-bots";
[TranslationReference]
const string VoteKickVoteStart = "dialog-vote-kick.vote-start";
[TranslationReference]
const string VoteKickVoteFor = "dialog-vote-kick.vote-for";
[TranslationReference]
const string VoteKickVoteAgainst = "dialog-vote-kick.vote-against";
[TranslationReference]
const string VoteKickVoteCancel = "dialog-vote-kick.vote-cancel";
[ObjectCreator.UseCtor]
public GameInfoStatsLogic(Widget widget, ModData modData, World world, OrderManager orderManager, WorldRenderer worldRenderer, Action<bool> hideMenu)
public GameInfoStatsLogic(Widget widget, ModData modData, World world, OrderManager orderManager, WorldRenderer worldRenderer, Action<bool> hideMenu, Action closeMenu)
{
var player = world.LocalPlayer;
var playerPanel = widget.Get<ScrollPanelWidget>("PLAYER_LIST");
@@ -106,6 +133,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic
var spectatorTemplate = playerPanel.Get("SPECTATOR_TEMPLATE");
var unmuteTooltip = TranslationProvider.GetString(Unmute);
var muteTooltip = TranslationProvider.GetString(Mute);
var kickTooltip = TranslationProvider.GetString(KickTooltip);
var voteKickTooltip = TranslationProvider.GetString(KickVoteTooltip);
playerPanel.RemoveChildren();
var teams = world.Players.Where(p => !p.NonCombatant && p.Playable)
@@ -114,22 +143,81 @@ 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)
void KickAction(Session.Client client, Func<bool> isVoteKick)
{
hideMenu(true);
ConfirmationDialogs.ButtonPrompt(modData,
title: KickTitle,
titleArguments: Translation.Arguments("player", client.Name),
text: KickPrompt,
onConfirm: () =>
if (isVoteKick())
{
var botsCount = 0;
if (client.IsAdmin)
botsCount = world.Players.Count(p => p.IsBot && p.WinState == WinState.Undefined);
if (UnitOrders.KickVoteTarget == null)
{
orderManager.IssueOrder(Order.Command($"kick {client.Index} {false}"));
hideMenu(false);
},
onCancel: () => hideMenu(false),
confirmText: KickAccept);
ConfirmationDialogs.ButtonPrompt(modData,
title: VoteKickTitle,
titleArguments: Translation.Arguments("player", client.Name),
text: botsCount > 0 ? VoteKickPromptBreakBots : VoteKickPrompt,
textArguments: Translation.Arguments("bots", botsCount),
onConfirm: () =>
{
orderManager.IssueOrder(Order.Command($"vote_kick {client.Index} {true}"));
hideMenu(false);
closeMenu();
},
confirmText: VoteKickVoteStart,
onCancel: () => hideMenu(false));
return;
}
ConfirmationDialogs.ButtonPrompt(modData,
title: VoteKickTitle,
titleArguments: Translation.Arguments("player", client.Name),
text: botsCount > 0 ? VoteKickPromptBreakBots : VoteKickPrompt,
textArguments: Translation.Arguments("bots", botsCount),
onConfirm: () =>
{
orderManager.IssueOrder(Order.Command($"vote_kick {client.Index} {true}"));
hideMenu(false);
closeMenu();
},
confirmText: VoteKickVoteFor,
onOther: () =>
{
Ui.CloseWindow();
orderManager.IssueOrder(Order.Command($"vote_kick {client.Index} {false}"));
hideMenu(false);
closeMenu();
},
otherText: VoteKickVoteAgainst,
onCancel: () => hideMenu(false),
cancelText: VoteKickVoteCancel);
}
else
{
ConfirmationDialogs.ButtonPrompt(modData,
title: KickTitle,
titleArguments: Translation.Arguments("player", client.Name),
text: KickPrompt,
onConfirm: () =>
{
orderManager.IssueOrder(Order.Command($"kick {client.Index} {false}"));
hideMenu(false);
},
confirmText: KickAccept,
onCancel: () => hideMenu(false));
}
}
var localClient = orderManager.LocalClient;
var localPlayer = localClient == null ? null : world.Players.FirstOrDefault(player => player.ClientIndex == localClient.Index);
bool LocalPlayerCanKick() => localClient != null
&& (Game.IsHost || ((!orderManager.LocalClient.IsObserver) && localPlayer.WinState == WinState.Undefined));
bool CanClientBeKicked(Session.Client client, Func<bool> isVoteKick) =>
client.Index != localClient.Index && client.State != Session.ClientState.Disconnected
&& (!client.IsAdmin || orderManager.LobbyInfo.GlobalSettings.Dedicated)
&& (!isVoteKick() || UnitOrders.KickVoteTarget == null || UnitOrders.KickVoteTarget == client.Index);
foreach (var t in teams)
{
if (teams.Count() > 1)
@@ -182,8 +270,10 @@ namespace OpenRA.Mods.Common.Widgets.Logic
muteCheckbox.GetTooltipText = () => muteCheckbox.IsChecked() ? unmuteTooltip : muteTooltip;
var kickButton = item.Get<ButtonWidget>("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);
bool IsVoteKick() => !Game.IsHost || pp.WinState == WinState.Undefined;
kickButton.IsVisible = () => !pp.IsBot && LocalPlayerCanKick() && CanClientBeKicked(client, IsVoteKick);
kickButton.OnClick = () => KickAction(client, IsVoteKick);
kickButton.GetTooltipText = () => IsVoteKick() ? voteKickTooltip : kickTooltip;
playerPanel.AddChild(item);
}
@@ -217,8 +307,10 @@ namespace OpenRA.Mods.Common.Widgets.Logic
};
var kickButton = item.Get<ButtonWidget>("KICK");
kickButton.IsVisible = () => Game.IsHost && client.Index != orderManager.LocalClient?.Index && client.State != Session.ClientState.Disconnected;
kickButton.OnClick = () => KickAction(client);
bool IsVoteKick() => !Game.IsHost;
kickButton.IsVisible = () => LocalPlayerCanKick() && CanClientBeKicked(client, IsVoteKick);
kickButton.OnClick = () => KickAction(client, IsVoteKick);
kickButton.GetTooltipText = () => IsVoteKick() ? voteKickTooltip : kickTooltip;
var muteCheckbox = item.Get<CheckboxWidget>("MUTE");
muteCheckbox.IsChecked = () => TextNotificationsManager.MutedPlayers[client.Index];

View File

@@ -200,7 +200,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic
var gameInfoPanel = Game.LoadWidget(world, "GAME_INFO_PANEL", panelRoot, new WidgetArgs()
{
{ "initialPanel", initialPanel },
{ "hideMenu", requestHideMenu }
{ "hideMenu", requestHideMenu },
{ "closeMenu", CloseMenu },
});
gameInfoPanel.IsVisible = () => !hideMenu;

View File

@@ -1 +1 @@
{DEV_VERSION}
release-20231010

View File

@@ -1,6 +1,6 @@
Metadata:
Title: All mods
Version: {DEV_VERSION}
Version: release-20231010
Hidden: true
Packages:

View File

@@ -139,7 +139,6 @@ Container@SKIRMISH_STATS:
Height: 25
Background: checkbox-toggle
TooltipContainer: TOOLTIP_CONTAINER
TooltipText: Kick this player
Children:
Image:
ImageCollection: lobby-bits
@@ -182,7 +181,6 @@ Container@SKIRMISH_STATS:
Height: 25
Background: checkbox-toggle
TooltipContainer: TOOLTIP_CONTAINER
TooltipText: Kick this player
Children:
Image:
ImageCollection: lobby-bits

View File

@@ -1,6 +1,6 @@
Metadata:
Title: Tiberian Dawn
Version: {DEV_VERSION}
Version: release-20231010
Website: https://www.openra.net
WebIcon32: https://www.openra.net/images/icons/cnc_32x32.png
WindowTitle: OpenRA - Tiberian Dawn
@@ -37,7 +37,7 @@ Packages:
MapFolders:
cnc|maps: System
~^SupportDir|maps/cnc/{DEV_VERSION}: User
~^SupportDir|maps/cnc/release-20231010: User
Rules:
cnc|rules/misc.yaml

View File

@@ -136,7 +136,6 @@ Container@SKIRMISH_STATS:
Height: 25
Background: checkbox-toggle
TooltipContainer: TOOLTIP_CONTAINER
TooltipText: Kick this player
Children:
Image:
ImageCollection: lobby-bits
@@ -179,7 +178,6 @@ Container@SKIRMISH_STATS:
Height: 25
Background: checkbox-toggle
TooltipContainer: TOOLTIP_CONTAINER
TooltipText: Kick this player
Children:
Image:
ImageCollection: lobby-bits

View File

@@ -33,7 +33,8 @@ notification-admin-change-configuration = Only the host can change the configura
notification-changed-map = { $player } changed the map to { $map }
notification-option-changed = { $player } changed { $name } to { $value }.
notification-you-were-kicked = You have been kicked from the server.
notification-kicked = { $admin } kicked { $player } from the server.
notification-admin-kicked = { $admin } kicked { $player } from the server.
notification-kicked = { $player } was kicked from the server.
notification-temp-ban = { $admin } temporarily banned { $player } from the server.
notification-admin-transfer-admin = Only admins can transfer admin to another player.
notification-admin-move-spectators = Only the host can move players to spectators.
@@ -95,6 +96,14 @@ notification-chat-temp-disabled =
*[other] Chat is disabled. Please try again in { $remaining } seconds.
}
## VoteKickTracker
notification-unable-to-start-a-vote = Unable to start a vote.
notification-insufficient-votes-to-kick = Insufficient votes to kick player { $kickee }.
notification-kick-already-voted = You have already voted.
notification-vote-kick-started = Player { $kicker } has started a vote to kick player { $kickee }.
notification-vote-kick-in-progress = { $percentage }% of players have voted to kick player { $kickee }.
notification-vote-kick-ended = Vote to kick player { $kickee } has failed.
## ActorEditLogic
label-duplicate-actor-id = Duplicate Actor ID
label-actor-id = Enter an Actor ID
@@ -149,12 +158,29 @@ label-mission-failed = Failed
label-client-state-disconnected = Gone
label-mute-player = Mute this player
label-unmute-player = Unmute this player
button-kick-player = Kick this player
button-vote-kick-player = Vote to kick this player
dialog-kick =
.title = Kick { $player }?
.prompt = They will not be able to rejoin this game.
.prompt = This player will not be able to rejoin the game.
.confirm = Kick
dialog-vote-kick =
.title = Vote to kick { $player }?
.prompt = This player will not be able to rejoin the game.
.prompt-break-bots =
{ $bots ->
[one] Kicking the game admin will also kick 1 bot.
*[other] Kicking the game admin will also kick { $bots } bots.
}
.vote-start = Start Vote
.vote-for = Vote For
.vote-against = Vote Against
.vote-cancel = Abstain
notification-vote-kick-disabled = Vote kick is disabled on this server.
## GameTimerLogic
label-paused = Paused
label-max-speed = Max Speed

View File

@@ -1,6 +1,6 @@
Metadata:
Title: Dune 2000
Version: {DEV_VERSION}
Version: release-20231010
Website: https://www.openra.net
WebIcon32: https://www.openra.net/images/icons/d2k_32x32.png
WindowTitle: OpenRA - Dune 2000
@@ -25,7 +25,7 @@ Packages:
MapFolders:
d2k|maps: System
~^SupportDir|maps/d2k/{DEV_VERSION}: User
~^SupportDir|maps/d2k/release-20231010: User
Rules:
d2k|rules/misc.yaml

View File

@@ -1,6 +1,6 @@
Metadata:
Title: Mod Content Manager
Version: {DEV_VERSION}
Version: release-20231010
Hidden: true
Packages:

View File

@@ -39,6 +39,10 @@ ARTY:
Buildable:
Prerequisites: ~disabled
FTRK:
Buildable:
Prerequisites: ~disabled
MCV:
Buildable:
Prerequisites: ~disabled

View File

@@ -35,6 +35,10 @@ ARTY:
Buildable:
Prerequisites: ~disabled
FTRK:
Buildable:
Prerequisites: ~disabled
MCV:
Buildable:
Prerequisites: ~disabled

View File

@@ -33,6 +33,10 @@ powerproxy.paratroopers:
ParatroopersPower:
DropItems: E1,E1,E1,E2,E2
FTRK:
Buildable:
Prerequisites: ~disabled
MCV:
Buildable:
Prerequisites: ~disabled

View File

@@ -41,6 +41,10 @@ powerproxy.paratroopers:
ParatroopersPower:
DropItems: E1,E1,E1,E2,E2
FTRK:
Buildable:
Prerequisites: ~disabled
MCV:
Buildable:
Prerequisites: ~disabled

View File

@@ -42,6 +42,10 @@ MSLO:
Capturable:
Types: ~disabled
FTRK:
Buildable:
Prerequisites: ~disabled
MCV:
Buildable:
Prerequisites: ~disabled

View File

@@ -75,6 +75,10 @@ MSLO:
Buildable:
Prerequisites: ~disabled
FTRK:
Buildable:
Prerequisites: ~disabled
MCV:
Buildable:
Prerequisites: ~disabled

View File

@@ -282,6 +282,10 @@ STEK:
Buildable:
Prerequisites: ~disabled
FTRK:
Buildable:
Prerequisites: ~disabled
MCV:
Buildable:
Prerequisites: ~disabled

View File

@@ -42,6 +42,10 @@ MSLO:
Buildable:
Prerequisites: ~disabled
FTRK:
Buildable:
Prerequisites: ~disabled
MCV:
Buildable:
Prerequisites: ~disabled

View File

@@ -42,6 +42,10 @@ MSLO:
Buildable:
Prerequisites: ~disabled
FTRK:
Buildable:
Prerequisites: ~disabled
MCV:
Buildable:
Prerequisites: ~disabled

View File

@@ -1,6 +1,6 @@
Metadata:
Title: Red Alert
Version: {DEV_VERSION}
Version: release-20231010
Website: https://www.openra.net
WebIcon32: https://www.openra.net/images/icons/ra_32x32.png
WindowTitle: OpenRA - Red Alert
@@ -40,7 +40,7 @@ Packages:
MapFolders:
ra|maps: System
~^SupportDir|maps/ra/{DEV_VERSION}: User
~^SupportDir|maps/ra/release-20231010: User
Rules:
ra|rules/misc.yaml

View File

@@ -1,6 +1,6 @@
Metadata:
Title: Tiberian Sun
Version: {DEV_VERSION}
Version: release-20231010
Website: https://www.openra.net
WebIcon32: https://www.openra.net/images/icons/ts_32x32.png
WindowTitle: OpenRA - Tiberian Sun
@@ -52,7 +52,7 @@ Packages:
MapFolders:
ts|maps: System
~^SupportDir|maps/ts/{DEV_VERSION}: User
~^SupportDir|maps/ts/release-20231010: User
Rules:
ts|rules/ai.yaml