Added translation support for server orders.
This commit is contained in:
committed by
teinarss
parent
ee95d2591f
commit
0260884369
136
OpenRA.Game/Network/LocalizedMessage.cs
Normal file
136
OpenRA.Game/Network/LocalizedMessage.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright 2007-2022 The OpenRA Developers (see AUTHORS)
|
||||
* 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Fluent.Net;
|
||||
using OpenRA.Widgets;
|
||||
|
||||
namespace OpenRA.Network
|
||||
{
|
||||
public class FluentArgument
|
||||
{
|
||||
[Flags]
|
||||
public enum FluentArgumentType
|
||||
{
|
||||
String = 0,
|
||||
Number = 1,
|
||||
}
|
||||
|
||||
public readonly string Key;
|
||||
public readonly string Value;
|
||||
public readonly FluentArgumentType Type;
|
||||
|
||||
public FluentArgument() { }
|
||||
|
||||
public FluentArgument(string key, object value)
|
||||
{
|
||||
Key = key;
|
||||
Value = value.ToString();
|
||||
Type = GetFluentArgumentType(value);
|
||||
}
|
||||
|
||||
static FluentArgumentType GetFluentArgumentType(object value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case byte _:
|
||||
case sbyte _:
|
||||
case short _:
|
||||
case uint _:
|
||||
case int _:
|
||||
case long _:
|
||||
case ulong _:
|
||||
case float _:
|
||||
case double _:
|
||||
case decimal _:
|
||||
return FluentArgumentType.Number;
|
||||
default:
|
||||
return FluentArgumentType.String;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class LocalizedMessage
|
||||
{
|
||||
public const int ProtocolVersion = 1;
|
||||
|
||||
public readonly string Key;
|
||||
|
||||
[FieldLoader.LoadUsing(nameof(LoadArguments))]
|
||||
public readonly FluentArgument[] Arguments;
|
||||
|
||||
static object LoadArguments(MiniYaml yaml)
|
||||
{
|
||||
var arguments = new List<FluentArgument>();
|
||||
var argumentsNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Arguments");
|
||||
if (argumentsNode != null)
|
||||
{
|
||||
var regex = new Regex(@"Argument@\d+");
|
||||
foreach (var argument in argumentsNode.Value.Nodes)
|
||||
if (regex.IsMatch(argument.Key))
|
||||
arguments.Add(FieldLoader.Load<FluentArgument>(argument.Value));
|
||||
}
|
||||
|
||||
return arguments.ToArray();
|
||||
}
|
||||
|
||||
static readonly string[] SerializeFields = { "Key" };
|
||||
|
||||
public LocalizedMessage(MiniYaml yaml)
|
||||
{
|
||||
FieldLoader.Load(this, yaml);
|
||||
}
|
||||
|
||||
public LocalizedMessage(string key, Dictionary<string, object> arguments = null)
|
||||
{
|
||||
Key = key;
|
||||
Arguments = arguments?.Select(a => new FluentArgument(a.Key, a.Value)).ToArray();
|
||||
}
|
||||
|
||||
public string Serialize()
|
||||
{
|
||||
var root = new List<MiniYamlNode>() { new MiniYamlNode("Protocol", ProtocolVersion.ToString()) };
|
||||
foreach (var field in SerializeFields)
|
||||
root.Add(FieldSaver.SaveField(this, field));
|
||||
|
||||
if (Arguments != null)
|
||||
{
|
||||
var argumentsNode = new MiniYaml("");
|
||||
var i = 0;
|
||||
foreach (var argument in Arguments)
|
||||
argumentsNode.Nodes.Add(new MiniYamlNode("Argument@" + i++, FieldSaver.Save(argument)));
|
||||
|
||||
root.Add(new MiniYamlNode("Arguments", argumentsNode));
|
||||
}
|
||||
|
||||
return new MiniYaml("", root)
|
||||
.ToLines("LocalizedMessage")
|
||||
.JoinWith("\n");
|
||||
}
|
||||
|
||||
public string Translate()
|
||||
{
|
||||
var argumentDictionary = new Dictionary<string, object>();
|
||||
foreach (var argument in Arguments)
|
||||
{
|
||||
if (argument.Type == FluentArgument.FluentArgumentType.Number)
|
||||
argumentDictionary.Add(argument.Key, new FluentNumber(argument.Value));
|
||||
else
|
||||
argumentDictionary.Add(argument.Key, new FluentString(argument.Value));
|
||||
}
|
||||
|
||||
return Ui.Translate(Key, argumentDictionary);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,22 @@ namespace OpenRA.Network
|
||||
TextNotificationsManager.AddSystemLine(order.TargetString);
|
||||
break;
|
||||
|
||||
// Client side translated server message
|
||||
case "LocalizedMessage":
|
||||
{
|
||||
if (string.IsNullOrEmpty(order.TargetString))
|
||||
break;
|
||||
|
||||
var yaml = MiniYaml.FromString(order.TargetString);
|
||||
foreach (var node in yaml)
|
||||
{
|
||||
var localizedMessage = new LocalizedMessage(node.Value);
|
||||
TextNotificationsManager.AddSystemLine(localizedMessage.Translate());
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "DisableChatEntry":
|
||||
{
|
||||
// Order must originate from the server
|
||||
|
||||
@@ -171,7 +171,7 @@ namespace OpenRA.Server
|
||||
var sent = socket.Send(data, start, length - start, SocketFlags.None, out var error);
|
||||
if (error == SocketError.WouldBlock)
|
||||
{
|
||||
Log.Write("server", "Non-blocking send of {0} bytes failed. Falling back to blocking send.", length - start);
|
||||
Log.Write("server", $"Non-blocking send of {length - start} bytes failed. Falling back to blocking send.");
|
||||
socket.Blocking = true;
|
||||
sent = socket.Send(data, start, length - start, SocketFlags.None);
|
||||
socket.Blocking = false;
|
||||
|
||||
@@ -77,6 +77,6 @@ namespace OpenRA.Server
|
||||
// The protocol for server and world orders
|
||||
// This applies after the handshake has completed, and is provided to support
|
||||
// alternative server implementations that wish to support multiple versions in parallel
|
||||
public const int Orders = 18;
|
||||
public const int Orders = 19;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,11 +21,13 @@ using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using OpenRA;
|
||||
using OpenRA.FileFormats;
|
||||
using OpenRA.Network;
|
||||
using OpenRA.Primitives;
|
||||
using OpenRA.Support;
|
||||
using OpenRA.Traits;
|
||||
using OpenRA.Widgets;
|
||||
|
||||
namespace OpenRA.Server
|
||||
{
|
||||
@@ -45,8 +47,6 @@ namespace OpenRA.Server
|
||||
|
||||
public sealed class Server
|
||||
{
|
||||
public readonly string TwoHumansRequiredText = "This server requires at least two human players to start a match.";
|
||||
|
||||
public readonly MersenneTwister Random = new MersenneTwister();
|
||||
public readonly ServerType Type;
|
||||
|
||||
@@ -79,6 +79,72 @@ namespace OpenRA.Server
|
||||
readonly List<GameInformation.Player> worldPlayers = new List<GameInformation.Player>();
|
||||
readonly Stopwatch pingUpdated = Stopwatch.StartNew();
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string CustomRules = "custom-rules";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string BotsDisabled = "bots-disabled";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string TwoHumansRequired = "two-humans-required";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string ErrorGameStarted = "error-game-started";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string RequiresPassword = "requires-password";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string IncorrectPassword = "incorrect-password";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string IncompatibleMod = "incompatible-mod";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string IncompatibleVersion = "incompatible-version";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string IncompatibleProtocol = "incompatible-protocol";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string Banned = "banned";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string TempBanned = "temp-banned";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string Full = "full";
|
||||
|
||||
[TranslationReference("player")]
|
||||
static readonly string Joined = "joined";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string RequiresForumAccount = "requires-forum-account";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string NoPermission = "no-permission";
|
||||
|
||||
[TranslationReference("command")]
|
||||
static readonly string UnknownServerCommand = "unknown-server-command";
|
||||
|
||||
[TranslationReference("remaining")]
|
||||
static readonly string ChatDisabled = "chat-disabled";
|
||||
|
||||
[TranslationReference("player")]
|
||||
static readonly string LobbyDisconnected = "lobby-disconnected";
|
||||
|
||||
[TranslationReference("player", "team")]
|
||||
static readonly string PlayerDisconnected = "player-disconnected";
|
||||
|
||||
[TranslationReference("player")]
|
||||
static readonly string ObserverDisconnected = "observer-disconnected";
|
||||
|
||||
[TranslationReference("player")]
|
||||
public static readonly string NewAdmin = "new-admin";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string YouWereKicked = "you-were-kicked";
|
||||
|
||||
public ServerState State
|
||||
{
|
||||
get => internalState;
|
||||
@@ -177,7 +243,7 @@ namespace OpenRA.Server
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ex is SocketException || ex is ArgumentException)
|
||||
Log.Write("server", "Failed to set socket option on {0}: {1}", endpoint.ToString(), ex.Message);
|
||||
Log.Write("server", $"Failed to set socket option on {endpoint}: {ex.Message}");
|
||||
else
|
||||
throw;
|
||||
}
|
||||
@@ -214,7 +280,7 @@ namespace OpenRA.Server
|
||||
catch (SocketException ex)
|
||||
{
|
||||
lastException = ex;
|
||||
Log.Write("server", "Failed to listen on {0}: {1}", endpoint.ToString(), ex.Message);
|
||||
Log.Write("server", $"Failed to listen on {endpoint}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,8 +343,8 @@ namespace OpenRA.Server
|
||||
foreach (var t in serverTraits.WithInterface<INotifyServerStart>())
|
||||
t.ServerStarted(this);
|
||||
|
||||
Log.Write("server", "Initial mod: {0}", ModData.Manifest.Id);
|
||||
Log.Write("server", "Initial map: {0}", LobbyInfo.GlobalSettings.Map);
|
||||
Log.Write("server", $"Initial mod: {ModData.Manifest.Id}");
|
||||
Log.Write("server", $"Initial map: {LobbyInfo.GlobalSettings.Map}");
|
||||
|
||||
while (true)
|
||||
{
|
||||
@@ -380,9 +446,9 @@ namespace OpenRA.Server
|
||||
{
|
||||
if (State == ServerState.GameStarted)
|
||||
{
|
||||
Log.Write("server", "Rejected connection from {0}; game is already started.", newConn.EndPoint);
|
||||
Log.Write("server", $"Rejected connection from {newConn.EndPoint}; game is already started.");
|
||||
|
||||
SendOrderTo(newConn, "ServerError", "The game has already started");
|
||||
SendOrderTo(newConn, "ServerError", ErrorGameStarted);
|
||||
DropClient(newConn);
|
||||
return;
|
||||
}
|
||||
@@ -391,7 +457,7 @@ namespace OpenRA.Server
|
||||
|
||||
if (!string.IsNullOrEmpty(Settings.Password) && handshake.Password != Settings.Password)
|
||||
{
|
||||
var message = string.IsNullOrEmpty(handshake.Password) ? "Server requires a password" : "Incorrect password";
|
||||
var message = string.IsNullOrEmpty(handshake.Password) ? RequiresPassword : IncorrectPassword;
|
||||
SendOrderTo(newConn, "AuthenticationError", message);
|
||||
DropClient(newConn);
|
||||
return;
|
||||
@@ -416,29 +482,27 @@ namespace OpenRA.Server
|
||||
|
||||
if (ModData.Manifest.Id != handshake.Mod)
|
||||
{
|
||||
Log.Write("server", "Rejected connection from {0}; mods do not match.",
|
||||
newConn.EndPoint);
|
||||
Log.Write("server", $"Rejected connection from {newConn.EndPoint}; mods do not match.");
|
||||
|
||||
SendOrderTo(newConn, "ServerError", "Server is running an incompatible mod");
|
||||
SendOrderTo(newConn, "ServerError", IncompatibleMod);
|
||||
DropClient(newConn);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ModData.Manifest.Metadata.Version != handshake.Version)
|
||||
{
|
||||
Log.Write("server", "Rejected connection from {0}; Not running the same version.", newConn.EndPoint);
|
||||
Log.Write("server", $"Rejected connection from {newConn.EndPoint}; Not running the same version.");
|
||||
|
||||
SendOrderTo(newConn, "ServerError", "Server is running an incompatible version");
|
||||
SendOrderTo(newConn, "ServerError", IncompatibleVersion);
|
||||
DropClient(newConn);
|
||||
return;
|
||||
}
|
||||
|
||||
if (handshake.OrdersProtocol != ProtocolVersion.Orders)
|
||||
{
|
||||
Log.Write("server", "Rejected connection from {0}; incompatible Orders protocol version {1}.",
|
||||
newConn.EndPoint, handshake.OrdersProtocol);
|
||||
Log.Write("server", $"Rejected connection from {newConn.EndPoint}; incompatible Orders protocol version {handshake.OrdersProtocol}.");
|
||||
|
||||
SendOrderTo(newConn, "ServerError", "Server is running an incompatible protocol");
|
||||
SendOrderTo(newConn, "ServerError", IncompatibleProtocol);
|
||||
DropClient(newConn);
|
||||
return;
|
||||
}
|
||||
@@ -447,8 +511,9 @@ namespace OpenRA.Server
|
||||
var bans = Settings.Ban.Union(TempBans);
|
||||
if (bans.Contains(client.IPAddress))
|
||||
{
|
||||
Log.Write("server", "Rejected connection from {0}; Banned.", newConn.EndPoint);
|
||||
SendOrderTo(newConn, "ServerError", $"You have been {(Settings.Ban.Contains(client.IPAddress) ? "banned" : "temporarily banned")} from the server");
|
||||
Log.Write("server", $"Rejected connection from {newConn.EndPoint}; Banned.");
|
||||
var message = Settings.Ban.Contains(client.IPAddress) ? Banned : TempBanned;
|
||||
SendOrderTo(newConn, "ServerError", message);
|
||||
DropClient(newConn);
|
||||
return;
|
||||
}
|
||||
@@ -458,11 +523,11 @@ namespace OpenRA.Server
|
||||
lock (LobbyInfo)
|
||||
{
|
||||
client.Slot = LobbyInfo.FirstEmptySlot();
|
||||
client.IsAdmin = !LobbyInfo.Clients.Any(c1 => c1.IsAdmin);
|
||||
client.IsAdmin = !LobbyInfo.Clients.Any(c => c.IsAdmin);
|
||||
|
||||
if (client.IsObserver && !LobbyInfo.GlobalSettings.AllowSpectators)
|
||||
{
|
||||
SendOrderTo(newConn, "ServerError", "The game is full");
|
||||
SendOrderTo(newConn, "ServerError", Full);
|
||||
DropClient(newConn);
|
||||
return;
|
||||
}
|
||||
@@ -480,20 +545,20 @@ namespace OpenRA.Server
|
||||
if (!client.IsAdmin && Settings.JoinChatDelay > 0)
|
||||
DispatchOrdersToClient(newConn, 0, 0, new Order("DisableChatEntry", null, false) { ExtraData = (uint)Settings.JoinChatDelay }.Serialize());
|
||||
|
||||
Log.Write("server", "Client {0}: Accepted connection from {1}.", newConn.PlayerIndex, newConn.EndPoint);
|
||||
Log.Write("server", $"Client {newConn.PlayerIndex}: Accepted connection from {newConn.EndPoint}.");
|
||||
|
||||
if (client.Fingerprint != null)
|
||||
Log.Write("server", "Client {0}: Player fingerprint is {1}.", newConn.PlayerIndex, client.Fingerprint);
|
||||
Log.Write("server", $"Client {newConn.PlayerIndex}: Player fingerprint is {client.Fingerprint}.");
|
||||
|
||||
foreach (var t in serverTraits.WithInterface<IClientJoined>())
|
||||
t.ClientJoined(this, newConn);
|
||||
|
||||
SyncLobbyInfo();
|
||||
|
||||
Log.Write("server", "{0} ({1}) has joined the game.", client.Name, newConn.EndPoint);
|
||||
Log.Write("server", $"{client.Name} ({newConn.EndPoint}) has joined the game.");
|
||||
|
||||
if (Type != ServerType.Local)
|
||||
SendMessage($"{client.Name} has joined the game.");
|
||||
SendLocalizedMessage(Joined, Translation.Arguments("player", client.Name));
|
||||
|
||||
if (Type == ServerType.Dedicated)
|
||||
{
|
||||
@@ -507,12 +572,12 @@ namespace OpenRA.Server
|
||||
}
|
||||
|
||||
if ((LobbyInfo.GlobalSettings.MapStatus & Session.MapStatus.UnsafeCustomRules) != 0)
|
||||
SendOrderTo(newConn, "Message", "This map contains custom rules. Game experience may change.");
|
||||
SendLocalizedMessageTo(newConn, CustomRules);
|
||||
|
||||
if (!LobbyInfo.GlobalSettings.EnableSingleplayer)
|
||||
SendOrderTo(newConn, "Message", TwoHumansRequiredText);
|
||||
SendLocalizedMessageTo(newConn, TwoHumansRequired);
|
||||
else if (Map.Players.Players.Where(p => p.Value.Playable).All(p => !p.Value.AllowBots))
|
||||
SendOrderTo(newConn, "Message", "Bots have been disabled on this map.");
|
||||
SendLocalizedMessageTo(newConn, BotsDisabled);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -543,29 +608,25 @@ namespace OpenRA.Server
|
||||
if (!profile.KeyRevoked && CryptoUtil.VerifySignature(parameters, newConn.AuthToken, handshake.AuthSignature))
|
||||
{
|
||||
client.Fingerprint = handshake.Fingerprint;
|
||||
Log.Write("server", "{0} authenticated as {1} (UID {2})", newConn.EndPoint,
|
||||
profile.ProfileName, profile.ProfileID);
|
||||
Log.Write("server", $"{newConn.EndPoint} authenticated as {profile.ProfileName} (UID {profile.ProfileID})");
|
||||
}
|
||||
else if (profile.KeyRevoked)
|
||||
{
|
||||
profile = null;
|
||||
Log.Write("server", "{0} failed to authenticate as {1} (key revoked)", newConn.EndPoint, handshake.Fingerprint);
|
||||
Log.Write("server", $"{newConn.EndPoint} failed to authenticate as {handshake.Fingerprint} (key revoked)");
|
||||
}
|
||||
else
|
||||
{
|
||||
profile = null;
|
||||
Log.Write("server", "{0} failed to authenticate as {1} (signature verification failed)",
|
||||
newConn.EndPoint, handshake.Fingerprint);
|
||||
Log.Write("server", $"{newConn.EndPoint} failed to authenticate as {handshake.Fingerprint} (signature verification failed)");
|
||||
}
|
||||
}
|
||||
else
|
||||
Log.Write("server", "{0} failed to authenticate as {1} (invalid server response: `{2}` is not `Player`)",
|
||||
newConn.EndPoint, handshake.Fingerprint, yaml.Key);
|
||||
Log.Write("server", $"{newConn.EndPoint} failed to authenticate as {handshake.Fingerprint} (invalid server response: `{yaml.Key}` is not `Player`)");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Write("server", "{0} failed to authenticate as {1} (exception occurred)",
|
||||
newConn.EndPoint, handshake.Fingerprint);
|
||||
Log.Write("server", $"{newConn.EndPoint} failed to authenticate as {handshake.Fingerprint} (exception occurred)");
|
||||
Log.Write("server", ex.ToString());
|
||||
}
|
||||
|
||||
@@ -578,18 +639,18 @@ namespace OpenRA.Server
|
||||
|
||||
if (notAuthenticated)
|
||||
{
|
||||
Log.Write("server", "Rejected connection from {0}; Not authenticated.", newConn.EndPoint);
|
||||
SendOrderTo(newConn, "ServerError", "Server requires players to have an OpenRA forum account");
|
||||
Log.Write("server", $"Rejected connection from {newConn.EndPoint}; Not authenticated.");
|
||||
SendOrderTo(newConn, "ServerError", RequiresForumAccount);
|
||||
DropClient(newConn);
|
||||
}
|
||||
else if (blacklisted || notWhitelisted)
|
||||
{
|
||||
if (blacklisted)
|
||||
Log.Write("server", "Rejected connection from {0}; In server blacklist.", newConn.EndPoint);
|
||||
Log.Write("server", $"Rejected connection from {newConn.EndPoint}; In server blacklist.");
|
||||
else
|
||||
Log.Write("server", "Rejected connection from {0}; Not in server whitelist.", newConn.EndPoint);
|
||||
Log.Write("server", $"Rejected connection from {newConn.EndPoint}; Not in server whitelist.");
|
||||
|
||||
SendOrderTo(newConn, "ServerError", "You do not have permission to join this server");
|
||||
SendOrderTo(newConn, "ServerError", NoPermission);
|
||||
DropClient(newConn);
|
||||
}
|
||||
else
|
||||
@@ -601,8 +662,8 @@ namespace OpenRA.Server
|
||||
{
|
||||
if (Type == ServerType.Dedicated && (Settings.RequireAuthentication || Settings.ProfileIDWhitelist.Any()))
|
||||
{
|
||||
Log.Write("server", "Rejected connection from {0}; Not authenticated.", newConn.EndPoint);
|
||||
SendOrderTo(newConn, "ServerError", "Server requires players to have an OpenRA forum account");
|
||||
Log.Write("server", $"Rejected connection from {newConn.EndPoint}; Not authenticated.");
|
||||
SendOrderTo(newConn, "ServerError", RequiresForumAccount);
|
||||
DropClient(newConn);
|
||||
}
|
||||
else
|
||||
@@ -611,7 +672,7 @@ namespace OpenRA.Server
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Write("server", "Dropping connection {0} because an error occurred:", newConn.EndPoint);
|
||||
Log.Write("server", $"Dropping connection {newConn.EndPoint} because an error occurred:");
|
||||
Log.Write("server", ex.ToString());
|
||||
DropClient(newConn);
|
||||
}
|
||||
@@ -663,8 +724,7 @@ namespace OpenRA.Server
|
||||
catch (Exception e)
|
||||
{
|
||||
DropClient(c);
|
||||
Log.Write("server", "Dropping client {0} because dispatching orders failed: {1}",
|
||||
client.ToString(CultureInfo.InvariantCulture), e);
|
||||
Log.Write("server", $"Dropping client {client.ToString(CultureInfo.InvariantCulture)} because dispatching orders failed: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -707,7 +767,7 @@ namespace OpenRA.Server
|
||||
|
||||
void OutOfSync(int frame)
|
||||
{
|
||||
Log.Write("server", "Out of sync detected at frame {0}, cancel replay recording", frame);
|
||||
Log.Write("server", $"Out of sync detected at frame {frame}, cancel replay recording");
|
||||
|
||||
// Make sure the written file is not valid
|
||||
// TODO: storing a serverside replay on desync would be extremely useful
|
||||
@@ -870,6 +930,21 @@ namespace OpenRA.Server
|
||||
Console.WriteLine($"[{DateTime.Now.ToString(Settings.TimestampFormat)}] {text}");
|
||||
}
|
||||
|
||||
public void SendLocalizedMessage(string key, Dictionary<string, object> arguments = null)
|
||||
{
|
||||
var text = new LocalizedMessage(key, arguments).Serialize();
|
||||
DispatchServerOrdersToClients(Order.FromTargetString("LocalizedMessage", text, true));
|
||||
|
||||
if (Type == ServerType.Dedicated)
|
||||
Console.WriteLine($"[{DateTime.Now.ToString(Settings.TimestampFormat)}] {Ui.Translate(key, arguments)}");
|
||||
}
|
||||
|
||||
public void SendLocalizedMessageTo(Connection conn, string key, Dictionary<string, object> arguments = null)
|
||||
{
|
||||
var text = new LocalizedMessage(key, arguments).Serialize();
|
||||
DispatchOrdersToClient(conn, 0, 0, Order.FromTargetString("LocalizedMessage", text, true).Serialize());
|
||||
}
|
||||
|
||||
void InterpretServerOrder(Connection conn, Order o)
|
||||
{
|
||||
lock (LobbyInfo)
|
||||
@@ -882,9 +957,7 @@ namespace OpenRA.Server
|
||||
ValidateClient(conn, o.TargetString);
|
||||
else
|
||||
{
|
||||
Log.Write("server", "Rejected connection from {0}; Order `{1}` is not a `HandshakeResponse`.",
|
||||
conn.EndPoint, o.OrderString);
|
||||
|
||||
Log.Write("server", $"Rejected connection from {conn.EndPoint}; Order `{o.OrderString}` is not a `HandshakeResponse`.");
|
||||
DropClient(conn);
|
||||
}
|
||||
|
||||
@@ -900,8 +973,8 @@ namespace OpenRA.Server
|
||||
|
||||
if (handledBy == null)
|
||||
{
|
||||
Log.Write("server", "Unknown server command: {0}", o.TargetString);
|
||||
SendOrderTo(conn, "Message", $"Unknown server command: {o.TargetString}");
|
||||
Log.Write("server", $"Unknown server command: {o.TargetString}");
|
||||
SendLocalizedMessageTo(conn, UnknownServerCommand, Translation.Arguments("command", o.TargetString));
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -914,7 +987,7 @@ namespace OpenRA.Server
|
||||
if (!isAdmin && connected < Settings.JoinChatDelay)
|
||||
{
|
||||
var remaining = (Settings.JoinChatDelay - connected + 999) / 1000;
|
||||
SendOrderTo(conn, "Message", "Chat is disabled. Please try again in {0} seconds".F(remaining));
|
||||
SendLocalizedMessageTo(conn, ChatDisabled, Translation.Arguments("remaining", remaining));
|
||||
}
|
||||
else
|
||||
DispatchOrdersToClients(conn, 0, o.Serialize());
|
||||
@@ -1083,10 +1156,15 @@ namespace OpenRA.Server
|
||||
return;
|
||||
}
|
||||
|
||||
var suffix = "";
|
||||
if (State == ServerState.GameStarted)
|
||||
suffix = dropClient.IsObserver ? " (Spectator)" : dropClient.Team != 0 ? $" (Team {dropClient.Team})" : "";
|
||||
SendMessage($"{dropClient.Name}{suffix} has disconnected.");
|
||||
{
|
||||
if (dropClient.IsObserver)
|
||||
SendLocalizedMessage(ObserverDisconnected, Translation.Arguments("player", dropClient.Name));
|
||||
else
|
||||
SendLocalizedMessage(PlayerDisconnected, Translation.Arguments("player", dropClient.Name, "team", dropClient.Team));
|
||||
}
|
||||
else
|
||||
SendLocalizedMessage(LobbyDisconnected, Translation.Arguments("player", dropClient.Name));
|
||||
|
||||
LobbyInfo.Clients.RemoveAll(c => c.Index == toDrop.PlayerIndex);
|
||||
|
||||
@@ -1103,7 +1181,7 @@ namespace OpenRA.Server
|
||||
if (nextAdmin != null)
|
||||
{
|
||||
nextAdmin.IsAdmin = true;
|
||||
SendMessage($"{nextAdmin.Name} is now the admin.");
|
||||
SendLocalizedMessage(NewAdmin, Translation.Arguments("player", nextAdmin.Name));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1201,12 +1279,12 @@ namespace OpenRA.Server
|
||||
{
|
||||
lock (LobbyInfo)
|
||||
{
|
||||
Console.WriteLine("[{0}] Game started", DateTime.Now.ToString(Settings.TimestampFormat));
|
||||
Console.WriteLine($"[{DateTime.Now.ToString(Settings.TimestampFormat)}] Game started");
|
||||
|
||||
// Drop any players who are not ready
|
||||
foreach (var c in Conns.Where(c => !c.Validated || GetClient(c).IsInvalid).ToArray())
|
||||
{
|
||||
SendOrderTo(c, "ServerError", "You have been kicked from the server!");
|
||||
SendOrderTo(c, "ServerError", YouWereKicked);
|
||||
DropClient(c);
|
||||
}
|
||||
|
||||
|
||||
@@ -545,9 +545,9 @@ namespace OpenRA.Traits
|
||||
IsLocked = locked;
|
||||
}
|
||||
|
||||
public virtual string ValueChangedMessage(string playerName, string newValue)
|
||||
public virtual string Label(string value)
|
||||
{
|
||||
return playerName + " changed " + Name + " to " + Values[newValue] + ".";
|
||||
return Values[value];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,9 +562,9 @@ namespace OpenRA.Traits
|
||||
public LobbyBooleanOption(string id, string name, string description, bool visible, int displayorder, bool defaultValue, bool locked)
|
||||
: base(id, name, description, visible, displayorder, new ReadOnlyDictionary<string, string>(BoolValues), defaultValue.ToString(), locked) { }
|
||||
|
||||
public override string ValueChangedMessage(string playerName, string newValue)
|
||||
public override string Label(string newValue)
|
||||
{
|
||||
return playerName + " " + BoolValues[newValue].ToLowerInvariant() + " " + Name + ".";
|
||||
return BoolValues[newValue].ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,19 @@ using OpenRA.FileSystem;
|
||||
|
||||
namespace OpenRA
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Field)]
|
||||
public sealed class TranslationReferenceAttribute : Attribute
|
||||
{
|
||||
public readonly string[] RequiredVariableNames;
|
||||
|
||||
public TranslationReferenceAttribute() { }
|
||||
|
||||
public TranslationReferenceAttribute(params string[] requiredVariableNames)
|
||||
{
|
||||
RequiredVariableNames = requiredVariableNames;
|
||||
}
|
||||
}
|
||||
|
||||
public class Translation
|
||||
{
|
||||
readonly IEnumerable<MessageContext> messageContexts;
|
||||
@@ -71,6 +84,15 @@ namespace OpenRA
|
||||
return key;
|
||||
}
|
||||
|
||||
public bool HasAttribute(string key)
|
||||
{
|
||||
foreach (var messageContext in messageContexts)
|
||||
if (messageContext.HasMessage(key))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public string GetAttribute(string key, string attribute)
|
||||
{
|
||||
if (key == null)
|
||||
|
||||
121
OpenRA.Mods.Common/Lint/CheckTranslationReference.cs
Normal file
121
OpenRA.Mods.Common/Lint/CheckTranslationReference.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright 2007-2021 The OpenRA Developers (see AUTHORS)
|
||||
* 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;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Fluent.Net;
|
||||
using Fluent.Net.RuntimeAst;
|
||||
|
||||
namespace OpenRA.Mods.Common.Lint
|
||||
{
|
||||
class CheckTranslationReference : ILintPass
|
||||
{
|
||||
const BindingFlags Binding = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
|
||||
|
||||
readonly List<string> referencedKeys = new List<string>();
|
||||
readonly Dictionary<string, string[]> referencedVariablesPerKey = new Dictionary<string, string[]>();
|
||||
readonly List<string> variableReferences = new List<string>();
|
||||
|
||||
public void Run(Action<string> emitError, Action<string> emitWarning, ModData modData)
|
||||
{
|
||||
var language = "en";
|
||||
var translation = new Translation(language, modData.Manifest.Translations, modData.DefaultFileSystem);
|
||||
|
||||
foreach (var modType in modData.ObjectCreator.GetTypes())
|
||||
{
|
||||
foreach (var fieldInfo in modType.GetFields(Binding).Where(m => m.HasAttribute<TranslationReferenceAttribute>()))
|
||||
{
|
||||
if (fieldInfo.FieldType != typeof(string))
|
||||
emitError($"Translation attribute on non string field {fieldInfo.Name}.");
|
||||
|
||||
var key = (string)fieldInfo.GetValue(string.Empty);
|
||||
if (!translation.HasAttribute(key))
|
||||
emitError($"{key} not present in {language} translation.");
|
||||
|
||||
var translationReference = fieldInfo.GetCustomAttributes<TranslationReferenceAttribute>(true)[0];
|
||||
if (translationReference.RequiredVariableNames != null && translationReference.RequiredVariableNames.Length > 0)
|
||||
referencedVariablesPerKey.GetOrAdd(key, translationReference.RequiredVariableNames);
|
||||
|
||||
referencedKeys.Add(key);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var file in modData.Manifest.Translations)
|
||||
{
|
||||
var stream = modData.DefaultFileSystem.Open(file);
|
||||
using (var reader = new StreamReader(stream))
|
||||
{
|
||||
var runtimeParser = new RuntimeParser();
|
||||
var result = runtimeParser.GetResource(reader);
|
||||
|
||||
foreach (var entry in result.Entries)
|
||||
{
|
||||
if (!referencedKeys.Contains(entry.Key))
|
||||
emitWarning($"Unused key `{entry.Key}` in {file}.");
|
||||
|
||||
var message = entry.Value;
|
||||
var node = message.Value;
|
||||
variableReferences.Clear();
|
||||
if (node is Pattern pattern)
|
||||
{
|
||||
foreach (var element in pattern.Elements)
|
||||
{
|
||||
if (element is SelectExpression selectExpression)
|
||||
{
|
||||
foreach (var variant in selectExpression.Variants)
|
||||
{
|
||||
if (variant.Value is Pattern variantPattern)
|
||||
{
|
||||
foreach (var variantElement in variantPattern.Elements)
|
||||
CheckVariableReference(variantElement, entry, emitWarning, file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CheckVariableReference(element, entry, emitWarning, file);
|
||||
}
|
||||
|
||||
if (referencedVariablesPerKey.ContainsKey(entry.Key))
|
||||
{
|
||||
var referencedVariables = referencedVariablesPerKey[entry.Key];
|
||||
foreach (var referencedVariable in referencedVariables)
|
||||
{
|
||||
if (!variableReferences.Contains(referencedVariable))
|
||||
emitError($"Missing variable `{referencedVariable}` for key `{entry.Key}` in {file}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CheckVariableReference(Node element, KeyValuePair<string, Message> entry, Action<string> emitWarning, string file)
|
||||
{
|
||||
if (element is VariableReference variableReference)
|
||||
{
|
||||
variableReferences.Add(variableReference.Name);
|
||||
|
||||
if (referencedVariablesPerKey.ContainsKey(entry.Key))
|
||||
{
|
||||
var referencedVariables = referencedVariablesPerKey[entry.Key];
|
||||
if (!referencedVariables.Contains(variableReference.Name))
|
||||
emitWarning($"Unused variable `{variableReference.Name}` for key `{entry.Key}` in {file}.");
|
||||
}
|
||||
else
|
||||
emitWarning($"Unused variable `{variableReference.Name}` for key `{entry.Key}` in {file}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
50
OpenRA.Mods.Common/Lint/CheckTranslationSyntax.cs
Normal file
50
OpenRA.Mods.Common/Lint/CheckTranslationSyntax.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright 2007-2021 The OpenRA Developers (see AUTHORS)
|
||||
* 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;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Fluent.Net;
|
||||
using Fluent.Net.Ast;
|
||||
|
||||
namespace OpenRA.Mods.Common.Lint
|
||||
{
|
||||
class CheckTranslationSyntax : ILintPass
|
||||
{
|
||||
public void Run(Action<string> emitError, Action<string> emitWarning, ModData modData)
|
||||
{
|
||||
foreach (var file in modData.Manifest.Translations)
|
||||
{
|
||||
var stream = modData.DefaultFileSystem.Open(file);
|
||||
using (var reader = new StreamReader(stream))
|
||||
{
|
||||
var ids = new List<string>();
|
||||
var parser = new Parser();
|
||||
var resource = parser.Parse(reader);
|
||||
foreach (var entry in resource.Body)
|
||||
{
|
||||
if (entry is Junk junk)
|
||||
foreach (var annotation in junk.Annotations)
|
||||
emitError($"{annotation.Code}: {annotation.Message} in {file} line {annotation.Span.Start.Line}");
|
||||
|
||||
if (entry is MessageTermBase message)
|
||||
{
|
||||
if (ids.Contains(message.Id.Name))
|
||||
emitWarning($"Duplicate ID `{message.Id.Name}` in {file} line {message.Span.Start.Line}");
|
||||
|
||||
ids.Add(message.Id.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,132 @@ namespace OpenRA.Mods.Common.Server
|
||||
{
|
||||
public class LobbyCommands : ServerTrait, IInterpretCommand, INotifyServerStart, INotifyServerEmpty, IClientJoined
|
||||
{
|
||||
[TranslationReference]
|
||||
static readonly string CustomRules = "custom-rules";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string OnlyHostStartGame = "only-only-host-start-game";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string NoStartUntilRequiredSlotsFull = "no-start-until-required-slots-full";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string TwoHumansRequired = "two-humans-required";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string InsufficientEnabledSpawnPoints = "insufficient-enabled-spawnPoints";
|
||||
|
||||
[TranslationReference("command")]
|
||||
static readonly string MalformedCommand = "malformed-command";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string KickNone = "kick-none";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string NoKickGameStarted = "no-kick-game-started";
|
||||
|
||||
[TranslationReference("admin", "player")]
|
||||
static readonly string Kicked = "kicked";
|
||||
|
||||
[TranslationReference("admin", "player")]
|
||||
static readonly string TempBan = "temp-ban";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string NoTransferAdmin = "only-host-transfer-admin";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string EmptySlot = "empty-slot";
|
||||
|
||||
[TranslationReference("admin", "player")]
|
||||
static readonly string MoveSpectators = "move-spectators";
|
||||
|
||||
[TranslationReference("player", "name")]
|
||||
static readonly string Nick = "nick";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string StateUnchangedReady = "state-unchanged-ready";
|
||||
|
||||
[TranslationReference("command")]
|
||||
static readonly string StateUnchangedGameStarted = "state-unchanged-game-started";
|
||||
|
||||
[TranslationReference("faction")]
|
||||
static readonly string InvalidFactionSelected = "invalid-faction-selected";
|
||||
|
||||
[TranslationReference("factions")]
|
||||
static readonly string SupportedFactions = "supported-factions";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string RequiresHost = "requires-host";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string InvalidBotSlot = "invalid-bot-slot";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string InvalidBotType = "invalid-bot-type";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string HostChangeMap = "only-host-change-map";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string UnknownMap = "unknown-map";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string SearchingMap = "searching-map";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string NotAdmin = "only-host-change-configuration";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string InvalidConfigurationCommand = "invalid-configuration-command";
|
||||
|
||||
[TranslationReference("option")]
|
||||
static readonly string OptionLocked = "option-locked";
|
||||
|
||||
[TranslationReference("player", "map")]
|
||||
static readonly string ChangedMap = "changed-map";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string BotsDisabled = "bots-disabled";
|
||||
|
||||
[TranslationReference("player", "name", "value")]
|
||||
static readonly string ValueChanged = "value-changed";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string NoMoveSpectators = "only-host-move-spectators";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string AdminOption = "admin-option";
|
||||
|
||||
[TranslationReference("raw")]
|
||||
static readonly string NumberTeams = "number-teams";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string AdminClearSpawn = "admin-clear-spawn";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string SpawnOccupied = "spawn-occupied";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string SpawnLocked = "spawn-locked";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string AdminLobbyInfo = "admin-lobby-info";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string InvalidLobbyInfo = "invalid-lobby-info";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string AdminKick = "admin-kick";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string SlotClosed = "slot-closed";
|
||||
|
||||
[TranslationReference("player")]
|
||||
public static readonly string NewAdmin = "new-admin";
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string YouWereKicked = "you-were-kicked";
|
||||
|
||||
readonly IDictionary<string, Func<S, Connection, Session.Client, string, bool>> commandHandlers = new Dictionary<string, Func<S, Connection, Session.Client, string, bool>>
|
||||
{
|
||||
{ "state", State },
|
||||
@@ -56,13 +182,13 @@ namespace OpenRA.Mods.Common.Server
|
||||
{
|
||||
if (!server.LobbyInfo.Slots.ContainsKey(arg))
|
||||
{
|
||||
Log.Write("server", "Invalid slot: {0}", arg);
|
||||
Log.Write("server", $"Invalid slot: {arg}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (requiresHost && !client.IsAdmin)
|
||||
{
|
||||
server.SendOrderTo(conn, "Message", "Only the host can do that.");
|
||||
server.SendLocalizedMessageTo(conn, RequiresHost);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -70,22 +196,22 @@ namespace OpenRA.Mods.Common.Server
|
||||
}
|
||||
}
|
||||
|
||||
public static bool ValidateCommand(S server, Connection conn, Session.Client client, string cmd)
|
||||
public static bool ValidateCommand(S server, Connection conn, Session.Client client, string command)
|
||||
{
|
||||
lock (server.LobbyInfo)
|
||||
{
|
||||
// Kick command is always valid for the host
|
||||
if (cmd.StartsWith("kick "))
|
||||
if (command.StartsWith("kick "))
|
||||
return true;
|
||||
|
||||
if (server.State == ServerState.GameStarted)
|
||||
{
|
||||
server.SendOrderTo(conn, "Message", $"Cannot change state when game started. ({cmd})");
|
||||
server.SendLocalizedMessageTo(conn, StateUnchangedGameStarted, Translation.Arguments("command", command));
|
||||
return false;
|
||||
}
|
||||
else if (client.State == Session.ClientState.Ready && !(cmd.StartsWith("state") || cmd == "startgame"))
|
||||
else if (client.State == Session.ClientState.Ready && !(command.StartsWith("state") || command == "startgame"))
|
||||
{
|
||||
server.SendOrderTo(conn, "Message", "Cannot change state when marked as ready.");
|
||||
server.SendLocalizedMessageTo(conn, StateUnchangedReady);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -139,12 +265,13 @@ namespace OpenRA.Mods.Common.Server
|
||||
{
|
||||
if (!Enum<Session.ClientState>.TryParse(s, false, out var state))
|
||||
{
|
||||
server.SendOrderTo(conn, "Message", "Malformed state command");
|
||||
server.SendLocalizedMessageTo(conn, MalformedCommand, Translation.Arguments("command", "state"));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
client.State = state;
|
||||
Log.Write("server", "Player @{0} is {1}", conn.EndPoint, client.State);
|
||||
Log.Write("server", $"Player @{conn.EndPoint} is {client.State}");
|
||||
|
||||
server.SyncLobbyClients();
|
||||
CheckAutoStart(server);
|
||||
@@ -159,26 +286,26 @@ namespace OpenRA.Mods.Common.Server
|
||||
{
|
||||
if (!client.IsAdmin)
|
||||
{
|
||||
server.SendOrderTo(conn, "Message", "Only the host can start the game.");
|
||||
server.SendOrderTo(conn, "Message", OnlyHostStartGame);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (server.LobbyInfo.Slots.Any(sl => sl.Value.Required &&
|
||||
server.LobbyInfo.ClientInSlot(sl.Key) == null))
|
||||
{
|
||||
server.SendOrderTo(conn, "Message", "Unable to start the game until required slots are full.");
|
||||
server.SendOrderTo(conn, "Message", NoStartUntilRequiredSlotsFull);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!server.LobbyInfo.GlobalSettings.EnableSingleplayer && server.LobbyInfo.NonBotPlayers.Count() < 2)
|
||||
{
|
||||
server.SendOrderTo(conn, "Message", server.TwoHumansRequiredText);
|
||||
server.SendOrderTo(conn, "Message", TwoHumansRequired);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (LobbyUtils.InsufficientEnabledSpawnPoints(server.Map, server.LobbyInfo))
|
||||
{
|
||||
server.SendOrderTo(conn, "Message", "Unable to start the game until more spawn points are enabled.");
|
||||
server.SendOrderTo(conn, "Message", InsufficientEnabledSpawnPoints);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -194,7 +321,7 @@ namespace OpenRA.Mods.Common.Server
|
||||
{
|
||||
if (!server.LobbyInfo.Slots.ContainsKey(s))
|
||||
{
|
||||
Log.Write("server", "Invalid slot: {0}", s);
|
||||
Log.Write("server", $"Invalid slot: {s}");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -231,7 +358,7 @@ namespace OpenRA.Mods.Common.Server
|
||||
return true;
|
||||
}
|
||||
|
||||
server.SendOrderTo(conn, "Message", "Malformed allow_spectate command");
|
||||
server.SendLocalizedMessageTo(conn, MalformedCommand, Translation.Arguments("command", "allow_spectate"));
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -278,7 +405,7 @@ namespace OpenRA.Mods.Common.Server
|
||||
var occupantConn = server.Conns.FirstOrDefault(c => c.PlayerIndex == occupant.Index);
|
||||
if (occupantConn != null)
|
||||
{
|
||||
server.SendOrderTo(occupantConn, "ServerError", "Your slot was closed by the host.");
|
||||
server.SendOrderTo(conn, "ServerError", SlotClosed);
|
||||
server.DropClient(occupantConn);
|
||||
}
|
||||
}
|
||||
@@ -320,7 +447,7 @@ namespace OpenRA.Mods.Common.Server
|
||||
var parts = s.Split(' ');
|
||||
if (parts.Length < 3)
|
||||
{
|
||||
server.SendOrderTo(conn, "Message", "Malformed slot_bot command");
|
||||
server.SendLocalizedMessageTo(conn, MalformedCommand, Translation.Arguments("command", "slot_bot"));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -331,14 +458,14 @@ namespace OpenRA.Mods.Common.Server
|
||||
var bot = server.LobbyInfo.ClientInSlot(parts[0]);
|
||||
if (!Exts.TryParseIntegerInvariant(parts[1], out var controllerClientIndex))
|
||||
{
|
||||
Log.Write("server", "Invalid bot controller client index: {0}", parts[1]);
|
||||
Log.Write("server", $"Invalid bot controller client index: {parts[1]}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Invalid slot
|
||||
if (bot != null && bot.Bot == null)
|
||||
{
|
||||
server.SendOrderTo(conn, "Message", "Can't add bots to a slot with another client.");
|
||||
server.SendLocalizedMessageTo(conn, InvalidBotSlot);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -348,7 +475,7 @@ namespace OpenRA.Mods.Common.Server
|
||||
|
||||
if (botInfo == null)
|
||||
{
|
||||
server.SendOrderTo(conn, "Message", "Invalid bot type.");
|
||||
server.SendLocalizedMessageTo(conn, InvalidBotType);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -400,7 +527,7 @@ namespace OpenRA.Mods.Common.Server
|
||||
{
|
||||
if (!client.IsAdmin)
|
||||
{
|
||||
server.SendOrderTo(conn, "Message", "Only the host can change the map.");
|
||||
server.SendLocalizedMessageTo(conn, HostChangeMap);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -477,15 +604,15 @@ namespace OpenRA.Mods.Common.Server
|
||||
|
||||
server.SyncLobbyInfo();
|
||||
|
||||
server.SendMessage($"{client.Name} changed the map to {server.Map.Title}.");
|
||||
server.SendLocalizedMessage(ChangedMap, Translation.Arguments("player", client.Name, "map", server.Map.Title));
|
||||
|
||||
if ((server.LobbyInfo.GlobalSettings.MapStatus & Session.MapStatus.UnsafeCustomRules) != 0)
|
||||
server.SendMessage("This map contains custom rules. Game experience may change.");
|
||||
server.SendLocalizedMessage(CustomRules);
|
||||
|
||||
if (!server.LobbyInfo.GlobalSettings.EnableSingleplayer)
|
||||
server.SendMessage(server.TwoHumansRequiredText);
|
||||
server.SendLocalizedMessage(TwoHumansRequired);
|
||||
else if (server.Map.Players.Players.Where(p => p.Value.Playable).All(p => !p.Value.AllowBots))
|
||||
server.SendMessage("Bots have been disabled on this map.");
|
||||
server.SendLocalizedMessage(BotsDisabled);
|
||||
|
||||
var briefing = MissionBriefingOrDefault(server);
|
||||
if (briefing != null)
|
||||
@@ -493,14 +620,14 @@ namespace OpenRA.Mods.Common.Server
|
||||
}
|
||||
};
|
||||
|
||||
Action queryFailed = () => server.SendOrderTo(conn, "Message", "Map was not found on server.");
|
||||
Action queryFailed = () => server.SendLocalizedMessageTo(conn, UnknownMap);
|
||||
|
||||
var m = server.ModData.MapCache[s];
|
||||
if (m.Status == MapStatus.Available || m.Status == MapStatus.DownloadAvailable)
|
||||
selectMap(m);
|
||||
else if (server.Settings.QueryMapRepository)
|
||||
{
|
||||
server.SendOrderTo(conn, "Message", "Searching for map on the Resource Center...");
|
||||
server.SendLocalizedMessageTo(conn, SearchingMap);
|
||||
var mapRepository = server.ModData.Manifest.Get<WebServices>().MapRepository;
|
||||
var reported = false;
|
||||
server.ModData.MapCache.QueryRemoteMapDetails(mapRepository, new[] { s }, selectMap, _ =>
|
||||
@@ -524,7 +651,7 @@ namespace OpenRA.Mods.Common.Server
|
||||
{
|
||||
if (!client.IsAdmin)
|
||||
{
|
||||
server.SendOrderTo(conn, "Message", "Only the host can change the configuration.");
|
||||
server.SendLocalizedMessageTo(conn, NotAdmin);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -541,13 +668,13 @@ namespace OpenRA.Mods.Common.Server
|
||||
if (split.Length < 2 || !options.TryGetValue(split[0], out var option) ||
|
||||
!option.Values.ContainsKey(split[1]))
|
||||
{
|
||||
server.SendOrderTo(conn, "Message", "Invalid configuration command.");
|
||||
server.SendLocalizedMessageTo(conn, InvalidConfigurationCommand);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (option.IsLocked)
|
||||
{
|
||||
server.SendOrderTo(conn, "Message", $"{option.Name} cannot be changed.");
|
||||
server.SendLocalizedMessageTo(conn, OptionLocked, Translation.Arguments("option", option.Name));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -558,7 +685,7 @@ namespace OpenRA.Mods.Common.Server
|
||||
oo.Value = oo.PreferredValue = split[1];
|
||||
|
||||
server.SyncLobbyGlobalSettings();
|
||||
server.SendMessage(option.ValueChangedMessage(client.Name, split[1]));
|
||||
server.SendLocalizedMessage(ValueChanged, Translation.Arguments("player", client.Name, "name", option.Name, "value", option.Label(split[1])));
|
||||
|
||||
foreach (var c in server.LobbyInfo.Clients)
|
||||
c.State = Session.ClientState.NotReady;
|
||||
@@ -569,19 +696,19 @@ namespace OpenRA.Mods.Common.Server
|
||||
}
|
||||
}
|
||||
|
||||
static bool AssignTeams(S server, Connection conn, Session.Client client, string s)
|
||||
static bool AssignTeams(S server, Connection conn, Session.Client client, string raw)
|
||||
{
|
||||
lock (server.LobbyInfo)
|
||||
{
|
||||
if (!client.IsAdmin)
|
||||
{
|
||||
server.SendOrderTo(conn, "Message", "Only the host can set that option.");
|
||||
server.SendLocalizedMessageTo(conn, AdminOption);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Exts.TryParseIntegerInvariant(s, out var teamCount))
|
||||
if (!Exts.TryParseIntegerInvariant(raw, out var teamCount))
|
||||
{
|
||||
server.SendOrderTo(conn, "Message", $"Number of teams could not be parsed: {s}");
|
||||
server.SendLocalizedMessageTo(conn, NumberTeams, Translation.Arguments("raw", raw));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -618,14 +745,14 @@ namespace OpenRA.Mods.Common.Server
|
||||
{
|
||||
if (!client.IsAdmin)
|
||||
{
|
||||
server.SendOrderTo(conn, "Message", "Only the host can kick players.");
|
||||
server.SendLocalizedMessageTo(conn, AdminKick);
|
||||
return true;
|
||||
}
|
||||
|
||||
var split = s.Split(' ');
|
||||
if (split.Length < 2)
|
||||
{
|
||||
server.SendOrderTo(conn, "Message", "Malformed kick command");
|
||||
server.SendLocalizedMessageTo(conn, MalformedCommand, Translation.Arguments("command", "kick"));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -634,26 +761,26 @@ namespace OpenRA.Mods.Common.Server
|
||||
var kickConn = server.Conns.SingleOrDefault(c => server.GetClient(c)?.Index == kickClientID);
|
||||
if (kickConn == null)
|
||||
{
|
||||
server.SendOrderTo(conn, "Message", "No-one in that slot.");
|
||||
server.SendLocalizedMessageTo(conn, KickNone);
|
||||
return true;
|
||||
}
|
||||
|
||||
var kickClient = server.GetClient(kickConn);
|
||||
if (server.State == ServerState.GameStarted && !kickClient.IsObserver)
|
||||
{
|
||||
server.SendOrderTo(conn, "Message", "Only spectators can be kicked after the game has started.");
|
||||
server.SendLocalizedMessageTo(conn, NoKickGameStarted);
|
||||
return true;
|
||||
}
|
||||
|
||||
Log.Write("server", "Kicking client {0}.", kickClientID);
|
||||
server.SendMessage($"{client.Name} kicked {kickClient.Name} from the server.");
|
||||
server.SendOrderTo(kickConn, "ServerError", "You have been kicked from the server.");
|
||||
Log.Write("server", $"Kicking client {kickClientID}.");
|
||||
server.SendLocalizedMessage(Kicked, Translation.Arguments("admin", client.Name, "client", kickClient.Name));
|
||||
server.SendOrderTo(kickConn, "ServerError", YouWereKicked);
|
||||
server.DropClient(kickConn);
|
||||
|
||||
if (bool.TryParse(split[1], out var tempBan) && tempBan)
|
||||
{
|
||||
Log.Write("server", "Temporarily banning client {0} ({1}).", kickClientID, kickClient.IPAddress);
|
||||
server.SendMessage($"{client.Name} temporarily banned {kickClient.Name} from the server.");
|
||||
Log.Write("server", $"Temporarily banning client {kickClientID} ({kickClient.IPAddress}).");
|
||||
server.SendLocalizedMessage(TempBan, Translation.Arguments("admin", client.Name, "client", kickClient.Name));
|
||||
server.TempBans.Add(kickClient.IPAddress);
|
||||
}
|
||||
|
||||
@@ -670,7 +797,7 @@ namespace OpenRA.Mods.Common.Server
|
||||
{
|
||||
if (!client.IsAdmin)
|
||||
{
|
||||
server.SendOrderTo(conn, "Message", "Only admins can transfer admin to another player.");
|
||||
server.SendLocalizedMessageTo(conn, NoTransferAdmin);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -679,7 +806,7 @@ namespace OpenRA.Mods.Common.Server
|
||||
|
||||
if (newAdminConn == null)
|
||||
{
|
||||
server.SendOrderTo(conn, "Message", "No-one in that slot.");
|
||||
server.SendLocalizedMessageTo(conn, EmptySlot);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -693,7 +820,7 @@ namespace OpenRA.Mods.Common.Server
|
||||
foreach (var b in bots)
|
||||
b.BotControllerClientIndex = newAdminId;
|
||||
|
||||
server.SendMessage($"{newAdminClient.Name} is now the admin.");
|
||||
server.SendLocalizedMessage(NewAdmin, Translation.Arguments("player", newAdminClient.Name));
|
||||
Log.Write("server", $"{newAdminClient.Name} is now the admin.");
|
||||
server.SyncLobbyClients();
|
||||
|
||||
@@ -707,16 +834,15 @@ namespace OpenRA.Mods.Common.Server
|
||||
{
|
||||
if (!client.IsAdmin)
|
||||
{
|
||||
server.SendOrderTo(conn, "Message", "Only the host can move players to spectators.");
|
||||
server.SendLocalizedMessageTo(conn, NoMoveSpectators);
|
||||
return true;
|
||||
}
|
||||
|
||||
Exts.TryParseIntegerInvariant(s, out var targetId);
|
||||
var targetConn = server.Conns.SingleOrDefault(c => server.GetClient(c)?.Index == targetId);
|
||||
|
||||
if (targetConn == null)
|
||||
{
|
||||
server.SendOrderTo(conn, "Message", "No-one in that slot.");
|
||||
server.SendLocalizedMessageTo(conn, EmptySlot);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -727,7 +853,7 @@ namespace OpenRA.Mods.Common.Server
|
||||
targetClient.Handicap = 0;
|
||||
targetClient.Color = Color.White;
|
||||
targetClient.State = Session.ClientState.NotReady;
|
||||
server.SendMessage($"{client.Name} moved {targetClient.Name} to spectators.");
|
||||
server.SendLocalizedMessage(MoveSpectators, Translation.Arguments("admin", client.Name, "player", targetClient.Name));
|
||||
Log.Write("server", $"{client.Name} moved {targetClient.Name} to spectators.");
|
||||
server.SyncLobbyClients();
|
||||
CheckAutoStart(server);
|
||||
@@ -744,8 +870,8 @@ namespace OpenRA.Mods.Common.Server
|
||||
if (sanitizedName == client.Name)
|
||||
return true;
|
||||
|
||||
Log.Write("server", "Player@{0} is now known as {1}.", conn.EndPoint, sanitizedName);
|
||||
server.SendMessage($"{client.Name} is now known as {sanitizedName}.");
|
||||
Log.Write("server", $"Player@{conn.EndPoint} is now known as {sanitizedName}.");
|
||||
server.SendLocalizedMessage(Nick, Translation.Arguments("player", client.Name, "name", sanitizedName));
|
||||
client.Name = sanitizedName;
|
||||
server.SyncLobbyClients();
|
||||
|
||||
@@ -771,14 +897,15 @@ namespace OpenRA.Mods.Common.Server
|
||||
var factions = server.Map.WorldActorInfo.TraitInfos<FactionInfo>()
|
||||
.Where(f => f.Selectable).Select(f => f.InternalName);
|
||||
|
||||
if (!factions.Contains(parts[1]))
|
||||
var faction = parts[1];
|
||||
if (!factions.Contains(faction))
|
||||
{
|
||||
server.SendOrderTo(conn, "Message", $"Invalid faction selected: {parts[1]}");
|
||||
server.SendOrderTo(conn, "Message", $"Supported values: {factions.JoinWith(", ")}");
|
||||
server.SendLocalizedMessageTo(conn, InvalidFactionSelected, Translation.Arguments("faction", faction));
|
||||
server.SendLocalizedMessageTo(conn, SupportedFactions, Translation.Arguments("factions", factions.JoinWith(", ")));
|
||||
return true;
|
||||
}
|
||||
|
||||
targetClient.Faction = parts[1];
|
||||
targetClient.Faction = faction;
|
||||
server.SyncLobbyClients();
|
||||
|
||||
return true;
|
||||
@@ -858,7 +985,7 @@ namespace OpenRA.Mods.Common.Server
|
||||
var existingClient = server.LobbyInfo.Clients.FirstOrDefault(cc => cc.SpawnPoint == spawnPoint);
|
||||
if (client != existingClient && !client.IsAdmin)
|
||||
{
|
||||
server.SendOrderTo(conn, "Message", "Only admins can clear spawn points.");
|
||||
server.SendLocalizedMessageTo(conn, AdminClearSpawn);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -910,13 +1037,13 @@ namespace OpenRA.Mods.Common.Server
|
||||
if (!Exts.TryParseIntegerInvariant(parts[1], out var spawnPoint)
|
||||
|| spawnPoint < 0 || spawnPoint > server.Map.SpawnPoints.Length)
|
||||
{
|
||||
Log.Write("server", "Invalid spawn point: {0}", parts[1]);
|
||||
Log.Write("server", $"Invalid spawn point: {parts[1]}");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (server.LobbyInfo.Clients.Where(cc => cc != client).Any(cc => (cc.SpawnPoint == spawnPoint) && (cc.SpawnPoint != 0)))
|
||||
{
|
||||
server.SendOrderTo(conn, "Message", "You cannot occupy the same spawn point as another player.");
|
||||
server.SendLocalizedMessageTo(conn, SpawnOccupied);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -931,7 +1058,7 @@ namespace OpenRA.Mods.Common.Server
|
||||
|
||||
if (spawnLockedByAnotherSlot)
|
||||
{
|
||||
server.SendOrderTo(conn, "Message", "The spawn point is locked to another player slot.");
|
||||
server.SendLocalizedMessageTo(conn, SpawnLocked);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -978,7 +1105,7 @@ namespace OpenRA.Mods.Common.Server
|
||||
{
|
||||
if (!client.IsAdmin)
|
||||
{
|
||||
server.SendOrderTo(conn, "Message", "Only the host can set lobby info");
|
||||
server.SendLocalizedMessageTo(conn, AdminLobbyInfo);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -989,7 +1116,7 @@ namespace OpenRA.Mods.Common.Server
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
server.SendOrderTo(conn, "Message", "Invalid Lobby Info Sent");
|
||||
server.SendLocalizedMessageTo(conn, InvalidLobbyInfo);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -1079,8 +1206,8 @@ namespace OpenRA.Mods.Common.Server
|
||||
|
||||
Action<string> onError = message =>
|
||||
{
|
||||
if (connectionToEcho != null)
|
||||
server.SendOrderTo(connectionToEcho, "Message", message);
|
||||
if (connectionToEcho != null && message != null)
|
||||
server.SendLocalizedMessageTo(connectionToEcho, message);
|
||||
};
|
||||
|
||||
var terrainColors = server.ModData.DefaultTerrainInfo[server.Map.TileSet].RestrictedPlayerColors;
|
||||
|
||||
@@ -30,11 +30,24 @@ namespace OpenRA.Mods.Common.Server
|
||||
// 1 second (in milliseconds) minimum delay between pings
|
||||
const int RateLimitInterval = 1000;
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string NoPortForward = "no-port-forward";
|
||||
[TranslationReference]
|
||||
static readonly string BlacklistedTitle = "blacklisted-title";
|
||||
[TranslationReference]
|
||||
static readonly string InvalidErrorCode = "invalid-error-code";
|
||||
[TranslationReference]
|
||||
static readonly string Connected = "master-server-connected";
|
||||
[TranslationReference]
|
||||
static readonly string Error = "master-server-error";
|
||||
[TranslationReference]
|
||||
static readonly string GameOffline = "game-offline";
|
||||
|
||||
static readonly Beacon LanGameBeacon;
|
||||
static readonly Dictionary<int, string> MasterServerErrors = new Dictionary<int, string>()
|
||||
{
|
||||
{ 1, "Server port is not accessible from the internet." },
|
||||
{ 2, "Server name contains a blacklisted word." }
|
||||
{ 1, NoPortForward },
|
||||
{ 2, BlacklistedTitle }
|
||||
};
|
||||
|
||||
long lastPing = 0;
|
||||
@@ -143,25 +156,25 @@ namespace OpenRA.Mods.Common.Server
|
||||
var regex = new Regex(@"^\[(?<code>-?\d+)\](?<message>.*)");
|
||||
var match = regex.Match(masterResponseText);
|
||||
errorMessage = match.Success && int.TryParse(match.Groups["code"].Value, out errorCode) ?
|
||||
match.Groups["message"].Value.Trim() : "Failed to parse error message";
|
||||
match.Groups["message"].Value.Trim() : InvalidErrorCode;
|
||||
}
|
||||
|
||||
isInitialPing = false;
|
||||
lock (masterServerMessages)
|
||||
{
|
||||
masterServerMessages.Enqueue("Master server communication established.");
|
||||
masterServerMessages.Enqueue(Connected);
|
||||
if (errorCode != 0)
|
||||
{
|
||||
// Hardcoded error messages take precedence over the server-provided messages
|
||||
if (!MasterServerErrors.TryGetValue(errorCode, out var message))
|
||||
message = errorMessage;
|
||||
|
||||
masterServerMessages.Enqueue("Warning: " + message);
|
||||
masterServerMessages.Enqueue(message);
|
||||
|
||||
// Positive error codes indicate errors that prevent advertisement
|
||||
// Negative error codes are non-fatal warnings
|
||||
if (errorCode > 0)
|
||||
masterServerMessages.Enqueue("Game has not been advertised online.");
|
||||
masterServerMessages.Enqueue(GameOffline);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,7 +183,7 @@ namespace OpenRA.Mods.Common.Server
|
||||
{
|
||||
Log.Write("server", ex.ToString());
|
||||
lock (masterServerMessages)
|
||||
masterServerMessages.Enqueue("Master server communication failed.");
|
||||
masterServerMessages.Enqueue(Error);
|
||||
}
|
||||
|
||||
isBusy = false;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenRA.Server;
|
||||
using S = OpenRA.Server.Server;
|
||||
@@ -21,6 +22,18 @@ namespace OpenRA.Mods.Common.Server
|
||||
static readonly int ConnReportInterval = 20000; // Report every 20 seconds
|
||||
static readonly int ConnTimeout = 60000; // Drop unresponsive clients after 60 seconds
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string PlayerDropped = "player-dropped";
|
||||
|
||||
[TranslationReference("player")]
|
||||
static readonly string ConnectionProblems = "connection-problems";
|
||||
|
||||
[TranslationReference("player")]
|
||||
static readonly string Timeout = "timeout";
|
||||
|
||||
[TranslationReference("player", "timeout")]
|
||||
static readonly string TimeoutIn = "timeout-in";
|
||||
|
||||
long lastPing = 0;
|
||||
long lastConnReport = 0;
|
||||
bool isInitialPing = true;
|
||||
@@ -48,7 +61,7 @@ namespace OpenRA.Mods.Common.Server
|
||||
if (client == null)
|
||||
{
|
||||
server.DropClient(c);
|
||||
server.SendMessage("A player has been dropped after timing out.");
|
||||
server.SendLocalizedMessage(PlayerDropped);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -56,13 +69,13 @@ namespace OpenRA.Mods.Common.Server
|
||||
{
|
||||
if (!c.TimeoutMessageShown && c.TimeSinceLastResponse > PingInterval * 2)
|
||||
{
|
||||
server.SendMessage(client.Name + " is experiencing connection problems.");
|
||||
server.SendLocalizedMessage(ConnectionProblems, Translation.Arguments("player", client.Name));
|
||||
c.TimeoutMessageShown = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
server.SendMessage(client.Name + " has been dropped after timing out.");
|
||||
server.SendLocalizedMessage(Timeout, Translation.Arguments("player", client.Name));
|
||||
server.DropClient(c);
|
||||
}
|
||||
}
|
||||
@@ -79,7 +92,14 @@ namespace OpenRA.Mods.Common.Server
|
||||
{
|
||||
var client = server.GetClient(c);
|
||||
if (client != null)
|
||||
server.SendMessage($"{client.Name} will be dropped in {(ConnTimeout - c.TimeSinceLastResponse) / 1000} seconds.");
|
||||
{
|
||||
var timeout = (ConnTimeout - c.TimeSinceLastResponse) / 1000;
|
||||
server.SendLocalizedMessage(TimeoutIn, new Dictionary<string, object>()
|
||||
{
|
||||
{ "player", client.Name },
|
||||
{ "timeout", timeout }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,13 @@ namespace OpenRA.Mods.Common.Traits
|
||||
|
||||
public Color Color;
|
||||
|
||||
[TranslationReference]
|
||||
static readonly string PlayerColorTerrain = "player-color-terrain";
|
||||
[TranslationReference]
|
||||
static readonly string PlayerColorPlayer = "player-color-player";
|
||||
[TranslationReference]
|
||||
static readonly string InvalidPlayerColor = "invalid-player-color";
|
||||
|
||||
bool TryGetBlockingColor((float R, float G, float B) color, List<(float R, float G, float B)> candidateBlockers, out (float R, float G, float B) closestBlocker)
|
||||
{
|
||||
var closestDistance = SimilarityThreshold;
|
||||
@@ -148,9 +155,9 @@ namespace OpenRA.Mods.Common.Traits
|
||||
{
|
||||
var linear = Color.FromAhsv(hue, sat, V).ToLinear();
|
||||
if (TryGetBlockingColor(linear, terrainLinear, out var blocker))
|
||||
errorMessage = "Color was adjusted to be less similar to the terrain.";
|
||||
errorMessage = PlayerColorTerrain;
|
||||
else if (TryGetBlockingColor(linear, playerLinear, out blocker))
|
||||
errorMessage = "Color was adjusted to be less similar to another player.";
|
||||
errorMessage = PlayerColorPlayer;
|
||||
else
|
||||
{
|
||||
if (errorMessage != null)
|
||||
@@ -169,7 +176,7 @@ namespace OpenRA.Mods.Common.Traits
|
||||
}
|
||||
|
||||
// Failed to find a solution within a reasonable time: return a random color without any validation
|
||||
onError?.Invoke("Unable to determine a valid player color. A random color has been selected.");
|
||||
onError?.Invoke(InvalidPlayerColor);
|
||||
return Color.FromAhsv(random.NextFloat(), float2.Lerp(HsvSaturationRange[0], HsvSaturationRange[1], random.NextFloat()), V);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
||||
widget.Get<LabelWidget>("CONNECTING_DESC").GetText = () => $"Could not connect to {connection.Target}";
|
||||
|
||||
var connectionError = widget.Get<LabelWidget>("CONNECTION_ERROR");
|
||||
connectionError.GetText = () => orderManager.ServerError ?? connection.ErrorMessage ?? "Unknown error";
|
||||
connectionError.GetText = () => Ui.Translate(orderManager.ServerError) ?? connection.ErrorMessage ?? "Unknown error";
|
||||
|
||||
var panelTitle = widget.Get<LabelWidget>("TITLE");
|
||||
panelTitle.GetText = () => orderManager.AuthenticationFailed ? "Password Required" : "Connection Failed";
|
||||
|
||||
@@ -142,6 +142,9 @@ ChromeLayout:
|
||||
cnc|chrome/editor.yaml
|
||||
common|chrome/text-notifications.yaml
|
||||
|
||||
Translations:
|
||||
common|languages/en.ftl
|
||||
|
||||
Voices:
|
||||
cnc|audio/voices.yaml
|
||||
|
||||
|
||||
86
mods/common/languages/en.ftl
Normal file
86
mods/common/languages/en.ftl
Normal file
@@ -0,0 +1,86 @@
|
||||
## Server Orders
|
||||
|
||||
custom-rules = This map contains custom rules. Game experience may change.
|
||||
bots-disabled = Bots have been disabled on this map.
|
||||
two-humans-required = This server requires at least two human players to start a match.
|
||||
unknown-server-command = Unknown server command: { $command }
|
||||
only-only-host-start-game = Only the host can start the game.
|
||||
no-start-until-required-slots-full = Unable to start the game until required slots are full.
|
||||
insufficient-enabled-spawnPoints = Unable to start the game until more spawn points are enabled.
|
||||
malformed-command = Malformed { $command } command
|
||||
chat-disabled =
|
||||
{ $remaining ->
|
||||
[one] Chat is disabled. Please try again in { $remaining } second.
|
||||
*[other] Chat is disabled. Please try again in { $remaining } seconds.
|
||||
}
|
||||
state-unchanged-ready = Cannot change state when marked as ready.
|
||||
invalid-faction-selected = Invalid faction selected: { $faction }
|
||||
supported-factions = Supported values: { $factions }
|
||||
state-unchanged-game-started = Cannot change state when game started. ({ $command })
|
||||
requires-host = Only the host can do that.
|
||||
invalid-bot-slot = Can't add bots to a slot with another client.
|
||||
invalid-bot-type = Invalid bot type.
|
||||
only-host-change-map = Only the host can change the map.
|
||||
lobby-disconnected = { $player } has left.
|
||||
player-disconnected =
|
||||
{ $team ->
|
||||
[0] { $player } has disconnected.
|
||||
*[other] { $player } (Team { $team }) has disconnected.
|
||||
}
|
||||
observer-disconnected = { $player } (Spectator) has disconnected.
|
||||
unknown-map = Map was not found on server.
|
||||
searching-map = Searching for map on the Resource Center...
|
||||
only-host-change-configuration = Only the host can change the configuration.
|
||||
changed-map = { $player } changed the map to { $map }
|
||||
value-changed = { $player } changed { $name } to { $value }.
|
||||
you-were-kicked = You have been kicked from the server.
|
||||
kicked = { $admin } kicked { $player } from the server.
|
||||
temp-ban = { $admin } temporarily banned { $player } from the server.
|
||||
only-host-transfer-admin = Only admins can transfer admin to another player.
|
||||
only-host-move-spectators = Only the host can move players to spectators.
|
||||
empty-slot = No-one in that slot.
|
||||
move-spectators = { $admin } moved { $player } to spectators.
|
||||
nick = { $player } is now known as { $name }.
|
||||
player-dropped = A player has been dropped after timing out.
|
||||
connection-problems = { $player } is experiencing connection problems.
|
||||
timeout = { $player } has been dropped after timing out.
|
||||
timeout-in =
|
||||
{ $timeout ->
|
||||
[one] { $player } will be dropped in { $timeout } second.
|
||||
*[other] { $player } will be dropped in { $timeout } seconds.
|
||||
}
|
||||
error-game-started = The game has already started.
|
||||
requires-password = Server requires a password.
|
||||
incorrect-password = Incorrect password.
|
||||
incompatible-mod = Server is running an incompatible mod.
|
||||
incompatible-version = Server is running an incompatible version.
|
||||
incompatible-protocol = Server is running an incompatible protocol.
|
||||
banned = You have been banned from the server.
|
||||
temp-banned = You have been temporarily banned from the server.
|
||||
full = The game is full.
|
||||
joined = { $player } has joined the game.
|
||||
new-admin = { $player } is now the admin.
|
||||
option-locked = { $option } cannot be changed.
|
||||
invalid-configuration-command = Invalid configuration command.
|
||||
admin-option = Only the host can set that option.
|
||||
number-teams = Number of teams could not be parsed: { $raw }
|
||||
admin-kick = Only the host can kick players.
|
||||
kick-none = No-one in that slot.
|
||||
no-kick-game-started = Only spectators can be kicked after the game has started.
|
||||
admin-clear-spawn = Only admins can clear spawn points.
|
||||
spawn-occupied = You cannot occupy the same spawn point as another player.
|
||||
spawn-locked = The spawn point is locked to another player slot.
|
||||
admin-lobby-info = Only the host can set lobby info.
|
||||
invalid-lobby-info = Invalid lobby info sent.
|
||||
player-color-terrain = Color was adjusted to be less similar to the terrain.
|
||||
player-color-player = Color was adjusted to be less similar to another player.
|
||||
invalid-player-color = Unable to determine a valid player color. A random color has been selected.
|
||||
invalid-error-code = Failed to parse error message.
|
||||
master-server-connected = Master server communication established.
|
||||
master-server-error = "Master server communication failed."
|
||||
game-offline = Game has not been advertised online.
|
||||
no-port-forward = Server port is not accessible from the internet.
|
||||
blacklisted-title = Server name contains a blacklisted word.
|
||||
requires-forum-account = Server requires players to have an OpenRA forum account.
|
||||
no-permission = You do not have permission to join this server.
|
||||
slot-closed = Your slot was closed by the host.
|
||||
@@ -121,6 +121,9 @@ ChromeLayout:
|
||||
common|chrome/gamesave-loading.yaml
|
||||
common|chrome/text-notifications.yaml
|
||||
|
||||
Translations:
|
||||
common|languages/en.ftl
|
||||
|
||||
Weapons:
|
||||
d2k|weapons/debris.yaml
|
||||
d2k|weapons/smallguns.yaml
|
||||
|
||||
@@ -138,6 +138,9 @@ ChromeLayout:
|
||||
common|chrome/playerprofile.yaml
|
||||
common|chrome/text-notifications.yaml
|
||||
|
||||
Translations:
|
||||
common|languages/en.ftl
|
||||
|
||||
Weapons:
|
||||
ra|weapons/explosions.yaml
|
||||
ra|weapons/ballistics.yaml
|
||||
@@ -155,9 +158,6 @@ Notifications:
|
||||
Music:
|
||||
ra|audio/music.yaml
|
||||
|
||||
Translations:
|
||||
ra|languages/english.yaml
|
||||
|
||||
Hotkeys:
|
||||
common|hotkeys/game.yaml
|
||||
common|hotkeys/observer.yaml
|
||||
|
||||
@@ -183,6 +183,9 @@ ChromeLayout:
|
||||
common|chrome/editor.yaml
|
||||
common|chrome/text-notifications.yaml
|
||||
|
||||
Translations:
|
||||
common|languages/en.ftl
|
||||
|
||||
Voices:
|
||||
ts|audio/voices.yaml
|
||||
|
||||
|
||||
Reference in New Issue
Block a user