Added translation support for server orders.

This commit is contained in:
Matthias Mailänder
2021-12-18 20:12:28 +01:00
committed by teinarss
parent ee95d2591f
commit 0260884369
19 changed files with 834 additions and 149 deletions

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

View File

@@ -34,6 +34,22 @@ namespace OpenRA.Network
TextNotificationsManager.AddSystemLine(order.TargetString); TextNotificationsManager.AddSystemLine(order.TargetString);
break; 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": case "DisableChatEntry":
{ {
// Order must originate from the server // Order must originate from the server

View File

@@ -171,7 +171,7 @@ namespace OpenRA.Server
var sent = socket.Send(data, start, length - start, SocketFlags.None, out var error); var sent = socket.Send(data, start, length - start, SocketFlags.None, out var error);
if (error == SocketError.WouldBlock) 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; socket.Blocking = true;
sent = socket.Send(data, start, length - start, SocketFlags.None); sent = socket.Send(data, start, length - start, SocketFlags.None);
socket.Blocking = false; socket.Blocking = false;

View File

@@ -77,6 +77,6 @@ namespace OpenRA.Server
// The protocol for server and world orders // The protocol for server and world orders
// This applies after the handshake has completed, and is provided to support // This applies after the handshake has completed, and is provided to support
// alternative server implementations that wish to support multiple versions in parallel // alternative server implementations that wish to support multiple versions in parallel
public const int Orders = 18; public const int Orders = 19;
} }
} }

View File

@@ -21,11 +21,13 @@ using System.Net.Sockets;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using OpenRA;
using OpenRA.FileFormats; using OpenRA.FileFormats;
using OpenRA.Network; using OpenRA.Network;
using OpenRA.Primitives; using OpenRA.Primitives;
using OpenRA.Support; using OpenRA.Support;
using OpenRA.Traits; using OpenRA.Traits;
using OpenRA.Widgets;
namespace OpenRA.Server namespace OpenRA.Server
{ {
@@ -45,8 +47,6 @@ namespace OpenRA.Server
public sealed class 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 MersenneTwister Random = new MersenneTwister();
public readonly ServerType Type; public readonly ServerType Type;
@@ -79,6 +79,72 @@ namespace OpenRA.Server
readonly List<GameInformation.Player> worldPlayers = new List<GameInformation.Player>(); readonly List<GameInformation.Player> worldPlayers = new List<GameInformation.Player>();
readonly Stopwatch pingUpdated = Stopwatch.StartNew(); 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 public ServerState State
{ {
get => internalState; get => internalState;
@@ -177,7 +243,7 @@ namespace OpenRA.Server
catch (Exception ex) catch (Exception ex)
{ {
if (ex is SocketException || ex is ArgumentException) 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 else
throw; throw;
} }
@@ -214,7 +280,7 @@ namespace OpenRA.Server
catch (SocketException ex) catch (SocketException ex)
{ {
lastException = 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>()) foreach (var t in serverTraits.WithInterface<INotifyServerStart>())
t.ServerStarted(this); t.ServerStarted(this);
Log.Write("server", "Initial mod: {0}", ModData.Manifest.Id); Log.Write("server", $"Initial mod: {ModData.Manifest.Id}");
Log.Write("server", "Initial map: {0}", LobbyInfo.GlobalSettings.Map); Log.Write("server", $"Initial map: {LobbyInfo.GlobalSettings.Map}");
while (true) while (true)
{ {
@@ -380,9 +446,9 @@ namespace OpenRA.Server
{ {
if (State == ServerState.GameStarted) 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); DropClient(newConn);
return; return;
} }
@@ -391,7 +457,7 @@ namespace OpenRA.Server
if (!string.IsNullOrEmpty(Settings.Password) && handshake.Password != Settings.Password) 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); SendOrderTo(newConn, "AuthenticationError", message);
DropClient(newConn); DropClient(newConn);
return; return;
@@ -416,29 +482,27 @@ namespace OpenRA.Server
if (ModData.Manifest.Id != handshake.Mod) if (ModData.Manifest.Id != handshake.Mod)
{ {
Log.Write("server", "Rejected connection from {0}; mods do not match.", Log.Write("server", $"Rejected connection from {newConn.EndPoint}; mods do not match.");
newConn.EndPoint);
SendOrderTo(newConn, "ServerError", "Server is running an incompatible mod"); SendOrderTo(newConn, "ServerError", IncompatibleMod);
DropClient(newConn); DropClient(newConn);
return; return;
} }
if (ModData.Manifest.Metadata.Version != handshake.Version) 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); DropClient(newConn);
return; return;
} }
if (handshake.OrdersProtocol != ProtocolVersion.Orders) if (handshake.OrdersProtocol != ProtocolVersion.Orders)
{ {
Log.Write("server", "Rejected connection from {0}; incompatible Orders protocol version {1}.", Log.Write("server", $"Rejected connection from {newConn.EndPoint}; incompatible Orders protocol version {handshake.OrdersProtocol}.");
newConn.EndPoint, handshake.OrdersProtocol);
SendOrderTo(newConn, "ServerError", "Server is running an incompatible protocol"); SendOrderTo(newConn, "ServerError", IncompatibleProtocol);
DropClient(newConn); DropClient(newConn);
return; return;
} }
@@ -447,8 +511,9 @@ namespace OpenRA.Server
var bans = Settings.Ban.Union(TempBans); var bans = Settings.Ban.Union(TempBans);
if (bans.Contains(client.IPAddress)) if (bans.Contains(client.IPAddress))
{ {
Log.Write("server", "Rejected connection from {0}; Banned.", newConn.EndPoint); Log.Write("server", $"Rejected connection from {newConn.EndPoint}; Banned.");
SendOrderTo(newConn, "ServerError", $"You have been {(Settings.Ban.Contains(client.IPAddress) ? "banned" : "temporarily banned")} from the server"); var message = Settings.Ban.Contains(client.IPAddress) ? Banned : TempBanned;
SendOrderTo(newConn, "ServerError", message);
DropClient(newConn); DropClient(newConn);
return; return;
} }
@@ -458,11 +523,11 @@ namespace OpenRA.Server
lock (LobbyInfo) lock (LobbyInfo)
{ {
client.Slot = LobbyInfo.FirstEmptySlot(); 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) if (client.IsObserver && !LobbyInfo.GlobalSettings.AllowSpectators)
{ {
SendOrderTo(newConn, "ServerError", "The game is full"); SendOrderTo(newConn, "ServerError", Full);
DropClient(newConn); DropClient(newConn);
return; return;
} }
@@ -480,20 +545,20 @@ namespace OpenRA.Server
if (!client.IsAdmin && Settings.JoinChatDelay > 0) if (!client.IsAdmin && Settings.JoinChatDelay > 0)
DispatchOrdersToClient(newConn, 0, 0, new Order("DisableChatEntry", null, false) { ExtraData = (uint)Settings.JoinChatDelay }.Serialize()); 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) 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>()) foreach (var t in serverTraits.WithInterface<IClientJoined>())
t.ClientJoined(this, newConn); t.ClientJoined(this, newConn);
SyncLobbyInfo(); 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) if (Type != ServerType.Local)
SendMessage($"{client.Name} has joined the game."); SendLocalizedMessage(Joined, Translation.Arguments("player", client.Name));
if (Type == ServerType.Dedicated) if (Type == ServerType.Dedicated)
{ {
@@ -507,12 +572,12 @@ namespace OpenRA.Server
} }
if ((LobbyInfo.GlobalSettings.MapStatus & Session.MapStatus.UnsafeCustomRules) != 0) 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) 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)) 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)) if (!profile.KeyRevoked && CryptoUtil.VerifySignature(parameters, newConn.AuthToken, handshake.AuthSignature))
{ {
client.Fingerprint = handshake.Fingerprint; client.Fingerprint = handshake.Fingerprint;
Log.Write("server", "{0} authenticated as {1} (UID {2})", newConn.EndPoint, Log.Write("server", $"{newConn.EndPoint} authenticated as {profile.ProfileName} (UID {profile.ProfileID})");
profile.ProfileName, profile.ProfileID);
} }
else if (profile.KeyRevoked) else if (profile.KeyRevoked)
{ {
profile = null; 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 else
{ {
profile = null; profile = null;
Log.Write("server", "{0} failed to authenticate as {1} (signature verification failed)", Log.Write("server", $"{newConn.EndPoint} failed to authenticate as {handshake.Fingerprint} (signature verification failed)");
newConn.EndPoint, handshake.Fingerprint);
} }
} }
else else
Log.Write("server", "{0} failed to authenticate as {1} (invalid server response: `{2}` is not `Player`)", Log.Write("server", $"{newConn.EndPoint} failed to authenticate as {handshake.Fingerprint} (invalid server response: `{yaml.Key}` is not `Player`)");
newConn.EndPoint, handshake.Fingerprint, yaml.Key);
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.Write("server", "{0} failed to authenticate as {1} (exception occurred)", Log.Write("server", $"{newConn.EndPoint} failed to authenticate as {handshake.Fingerprint} (exception occurred)");
newConn.EndPoint, handshake.Fingerprint);
Log.Write("server", ex.ToString()); Log.Write("server", ex.ToString());
} }
@@ -578,18 +639,18 @@ namespace OpenRA.Server
if (notAuthenticated) if (notAuthenticated)
{ {
Log.Write("server", "Rejected connection from {0}; Not authenticated.", newConn.EndPoint); Log.Write("server", $"Rejected connection from {newConn.EndPoint}; Not authenticated.");
SendOrderTo(newConn, "ServerError", "Server requires players to have an OpenRA forum account"); SendOrderTo(newConn, "ServerError", RequiresForumAccount);
DropClient(newConn); DropClient(newConn);
} }
else if (blacklisted || notWhitelisted) else if (blacklisted || notWhitelisted)
{ {
if (blacklisted) 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 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); DropClient(newConn);
} }
else else
@@ -601,8 +662,8 @@ namespace OpenRA.Server
{ {
if (Type == ServerType.Dedicated && (Settings.RequireAuthentication || Settings.ProfileIDWhitelist.Any())) if (Type == ServerType.Dedicated && (Settings.RequireAuthentication || Settings.ProfileIDWhitelist.Any()))
{ {
Log.Write("server", "Rejected connection from {0}; Not authenticated.", newConn.EndPoint); Log.Write("server", $"Rejected connection from {newConn.EndPoint}; Not authenticated.");
SendOrderTo(newConn, "ServerError", "Server requires players to have an OpenRA forum account"); SendOrderTo(newConn, "ServerError", RequiresForumAccount);
DropClient(newConn); DropClient(newConn);
} }
else else
@@ -611,7 +672,7 @@ namespace OpenRA.Server
} }
catch (Exception ex) 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()); Log.Write("server", ex.ToString());
DropClient(newConn); DropClient(newConn);
} }
@@ -663,8 +724,7 @@ namespace OpenRA.Server
catch (Exception e) catch (Exception e)
{ {
DropClient(c); DropClient(c);
Log.Write("server", "Dropping client {0} because dispatching orders failed: {1}", Log.Write("server", $"Dropping client {client.ToString(CultureInfo.InvariantCulture)} because dispatching orders failed: {e}");
client.ToString(CultureInfo.InvariantCulture), e);
} }
} }
@@ -707,7 +767,7 @@ namespace OpenRA.Server
void OutOfSync(int frame) 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 // Make sure the written file is not valid
// TODO: storing a serverside replay on desync would be extremely useful // 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}"); 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) void InterpretServerOrder(Connection conn, Order o)
{ {
lock (LobbyInfo) lock (LobbyInfo)
@@ -882,9 +957,7 @@ namespace OpenRA.Server
ValidateClient(conn, o.TargetString); ValidateClient(conn, o.TargetString);
else else
{ {
Log.Write("server", "Rejected connection from {0}; Order `{1}` is not a `HandshakeResponse`.", Log.Write("server", $"Rejected connection from {conn.EndPoint}; Order `{o.OrderString}` is not a `HandshakeResponse`.");
conn.EndPoint, o.OrderString);
DropClient(conn); DropClient(conn);
} }
@@ -900,8 +973,8 @@ namespace OpenRA.Server
if (handledBy == null) if (handledBy == null)
{ {
Log.Write("server", "Unknown server command: {0}", o.TargetString); Log.Write("server", $"Unknown server command: {o.TargetString}");
SendOrderTo(conn, "Message", $"Unknown server command: {o.TargetString}"); SendLocalizedMessageTo(conn, UnknownServerCommand, Translation.Arguments("command", o.TargetString));
} }
break; break;
@@ -914,7 +987,7 @@ namespace OpenRA.Server
if (!isAdmin && connected < Settings.JoinChatDelay) if (!isAdmin && connected < Settings.JoinChatDelay)
{ {
var remaining = (Settings.JoinChatDelay - connected + 999) / 1000; 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 else
DispatchOrdersToClients(conn, 0, o.Serialize()); DispatchOrdersToClients(conn, 0, o.Serialize());
@@ -1083,10 +1156,15 @@ namespace OpenRA.Server
return; return;
} }
var suffix = "";
if (State == ServerState.GameStarted) 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); LobbyInfo.Clients.RemoveAll(c => c.Index == toDrop.PlayerIndex);
@@ -1103,7 +1181,7 @@ namespace OpenRA.Server
if (nextAdmin != null) if (nextAdmin != null)
{ {
nextAdmin.IsAdmin = true; 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) 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 // Drop any players who are not ready
foreach (var c in Conns.Where(c => !c.Validated || GetClient(c).IsInvalid).ToArray()) 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); DropClient(c);
} }

View File

@@ -545,9 +545,9 @@ namespace OpenRA.Traits
IsLocked = locked; 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) 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) { } : 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();
} }
} }

View File

@@ -19,6 +19,19 @@ using OpenRA.FileSystem;
namespace OpenRA 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 public class Translation
{ {
readonly IEnumerable<MessageContext> messageContexts; readonly IEnumerable<MessageContext> messageContexts;
@@ -71,6 +84,15 @@ namespace OpenRA
return key; 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) public string GetAttribute(string key, string attribute)
{ {
if (key == null) if (key == null)

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

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

View File

@@ -24,6 +24,132 @@ namespace OpenRA.Mods.Common.Server
{ {
public class LobbyCommands : ServerTrait, IInterpretCommand, INotifyServerStart, INotifyServerEmpty, IClientJoined 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>> readonly IDictionary<string, Func<S, Connection, Session.Client, string, bool>> commandHandlers = new Dictionary<string, Func<S, Connection, Session.Client, string, bool>>
{ {
{ "state", State }, { "state", State },
@@ -56,13 +182,13 @@ namespace OpenRA.Mods.Common.Server
{ {
if (!server.LobbyInfo.Slots.ContainsKey(arg)) if (!server.LobbyInfo.Slots.ContainsKey(arg))
{ {
Log.Write("server", "Invalid slot: {0}", arg); Log.Write("server", $"Invalid slot: {arg}");
return false; return false;
} }
if (requiresHost && !client.IsAdmin) if (requiresHost && !client.IsAdmin)
{ {
server.SendOrderTo(conn, "Message", "Only the host can do that."); server.SendLocalizedMessageTo(conn, RequiresHost);
return false; 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) lock (server.LobbyInfo)
{ {
// Kick command is always valid for the host // Kick command is always valid for the host
if (cmd.StartsWith("kick ")) if (command.StartsWith("kick "))
return true; return true;
if (server.State == ServerState.GameStarted) 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; 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; return false;
} }
@@ -139,12 +265,13 @@ namespace OpenRA.Mods.Common.Server
{ {
if (!Enum<Session.ClientState>.TryParse(s, false, out var state)) 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; return true;
} }
client.State = state; 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(); server.SyncLobbyClients();
CheckAutoStart(server); CheckAutoStart(server);
@@ -159,26 +286,26 @@ namespace OpenRA.Mods.Common.Server
{ {
if (!client.IsAdmin) if (!client.IsAdmin)
{ {
server.SendOrderTo(conn, "Message", "Only the host can start the game."); server.SendOrderTo(conn, "Message", OnlyHostStartGame);
return true; return true;
} }
if (server.LobbyInfo.Slots.Any(sl => sl.Value.Required && if (server.LobbyInfo.Slots.Any(sl => sl.Value.Required &&
server.LobbyInfo.ClientInSlot(sl.Key) == null)) 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; return true;
} }
if (!server.LobbyInfo.GlobalSettings.EnableSingleplayer && server.LobbyInfo.NonBotPlayers.Count() < 2) if (!server.LobbyInfo.GlobalSettings.EnableSingleplayer && server.LobbyInfo.NonBotPlayers.Count() < 2)
{ {
server.SendOrderTo(conn, "Message", server.TwoHumansRequiredText); server.SendOrderTo(conn, "Message", TwoHumansRequired);
return true; return true;
} }
if (LobbyUtils.InsufficientEnabledSpawnPoints(server.Map, server.LobbyInfo)) 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; return true;
} }
@@ -194,7 +321,7 @@ namespace OpenRA.Mods.Common.Server
{ {
if (!server.LobbyInfo.Slots.ContainsKey(s)) if (!server.LobbyInfo.Slots.ContainsKey(s))
{ {
Log.Write("server", "Invalid slot: {0}", s); Log.Write("server", $"Invalid slot: {s}");
return false; return false;
} }
@@ -231,7 +358,7 @@ namespace OpenRA.Mods.Common.Server
return true; return true;
} }
server.SendOrderTo(conn, "Message", "Malformed allow_spectate command"); server.SendLocalizedMessageTo(conn, MalformedCommand, Translation.Arguments("command", "allow_spectate"));
return true; return true;
} }
@@ -278,7 +405,7 @@ namespace OpenRA.Mods.Common.Server
var occupantConn = server.Conns.FirstOrDefault(c => c.PlayerIndex == occupant.Index); var occupantConn = server.Conns.FirstOrDefault(c => c.PlayerIndex == occupant.Index);
if (occupantConn != null) if (occupantConn != null)
{ {
server.SendOrderTo(occupantConn, "ServerError", "Your slot was closed by the host."); server.SendOrderTo(conn, "ServerError", SlotClosed);
server.DropClient(occupantConn); server.DropClient(occupantConn);
} }
} }
@@ -320,7 +447,7 @@ namespace OpenRA.Mods.Common.Server
var parts = s.Split(' '); var parts = s.Split(' ');
if (parts.Length < 3) if (parts.Length < 3)
{ {
server.SendOrderTo(conn, "Message", "Malformed slot_bot command"); server.SendLocalizedMessageTo(conn, MalformedCommand, Translation.Arguments("command", "slot_bot"));
return true; return true;
} }
@@ -331,14 +458,14 @@ namespace OpenRA.Mods.Common.Server
var bot = server.LobbyInfo.ClientInSlot(parts[0]); var bot = server.LobbyInfo.ClientInSlot(parts[0]);
if (!Exts.TryParseIntegerInvariant(parts[1], out var controllerClientIndex)) 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; return false;
} }
// Invalid slot // Invalid slot
if (bot != null && bot.Bot == null) 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; return true;
} }
@@ -348,7 +475,7 @@ namespace OpenRA.Mods.Common.Server
if (botInfo == null) if (botInfo == null)
{ {
server.SendOrderTo(conn, "Message", "Invalid bot type."); server.SendLocalizedMessageTo(conn, InvalidBotType);
return true; return true;
} }
@@ -400,7 +527,7 @@ namespace OpenRA.Mods.Common.Server
{ {
if (!client.IsAdmin) if (!client.IsAdmin)
{ {
server.SendOrderTo(conn, "Message", "Only the host can change the map."); server.SendLocalizedMessageTo(conn, HostChangeMap);
return true; return true;
} }
@@ -477,15 +604,15 @@ namespace OpenRA.Mods.Common.Server
server.SyncLobbyInfo(); 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) 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) 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)) 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); var briefing = MissionBriefingOrDefault(server);
if (briefing != null) 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]; var m = server.ModData.MapCache[s];
if (m.Status == MapStatus.Available || m.Status == MapStatus.DownloadAvailable) if (m.Status == MapStatus.Available || m.Status == MapStatus.DownloadAvailable)
selectMap(m); selectMap(m);
else if (server.Settings.QueryMapRepository) 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 mapRepository = server.ModData.Manifest.Get<WebServices>().MapRepository;
var reported = false; var reported = false;
server.ModData.MapCache.QueryRemoteMapDetails(mapRepository, new[] { s }, selectMap, _ => server.ModData.MapCache.QueryRemoteMapDetails(mapRepository, new[] { s }, selectMap, _ =>
@@ -524,7 +651,7 @@ namespace OpenRA.Mods.Common.Server
{ {
if (!client.IsAdmin) if (!client.IsAdmin)
{ {
server.SendOrderTo(conn, "Message", "Only the host can change the configuration."); server.SendLocalizedMessageTo(conn, NotAdmin);
return true; return true;
} }
@@ -541,13 +668,13 @@ namespace OpenRA.Mods.Common.Server
if (split.Length < 2 || !options.TryGetValue(split[0], out var option) || if (split.Length < 2 || !options.TryGetValue(split[0], out var option) ||
!option.Values.ContainsKey(split[1])) !option.Values.ContainsKey(split[1]))
{ {
server.SendOrderTo(conn, "Message", "Invalid configuration command."); server.SendLocalizedMessageTo(conn, InvalidConfigurationCommand);
return true; return true;
} }
if (option.IsLocked) if (option.IsLocked)
{ {
server.SendOrderTo(conn, "Message", $"{option.Name} cannot be changed."); server.SendLocalizedMessageTo(conn, OptionLocked, Translation.Arguments("option", option.Name));
return true; return true;
} }
@@ -558,7 +685,7 @@ namespace OpenRA.Mods.Common.Server
oo.Value = oo.PreferredValue = split[1]; oo.Value = oo.PreferredValue = split[1];
server.SyncLobbyGlobalSettings(); 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) foreach (var c in server.LobbyInfo.Clients)
c.State = Session.ClientState.NotReady; 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) lock (server.LobbyInfo)
{ {
if (!client.IsAdmin) if (!client.IsAdmin)
{ {
server.SendOrderTo(conn, "Message", "Only the host can set that option."); server.SendLocalizedMessageTo(conn, AdminOption);
return true; 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; return true;
} }
@@ -618,14 +745,14 @@ namespace OpenRA.Mods.Common.Server
{ {
if (!client.IsAdmin) if (!client.IsAdmin)
{ {
server.SendOrderTo(conn, "Message", "Only the host can kick players."); server.SendLocalizedMessageTo(conn, AdminKick);
return true; return true;
} }
var split = s.Split(' '); var split = s.Split(' ');
if (split.Length < 2) if (split.Length < 2)
{ {
server.SendOrderTo(conn, "Message", "Malformed kick command"); server.SendLocalizedMessageTo(conn, MalformedCommand, Translation.Arguments("command", "kick"));
return true; return true;
} }
@@ -634,26 +761,26 @@ namespace OpenRA.Mods.Common.Server
var kickConn = server.Conns.SingleOrDefault(c => server.GetClient(c)?.Index == kickClientID); var kickConn = server.Conns.SingleOrDefault(c => server.GetClient(c)?.Index == kickClientID);
if (kickConn == null) if (kickConn == null)
{ {
server.SendOrderTo(conn, "Message", "No-one in that slot."); server.SendLocalizedMessageTo(conn, KickNone);
return true; return true;
} }
var kickClient = server.GetClient(kickConn); var kickClient = server.GetClient(kickConn);
if (server.State == ServerState.GameStarted && !kickClient.IsObserver) 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; return true;
} }
Log.Write("server", "Kicking client {0}.", kickClientID); Log.Write("server", $"Kicking client {kickClientID}.");
server.SendMessage($"{client.Name} kicked {kickClient.Name} from the server."); server.SendLocalizedMessage(Kicked, Translation.Arguments("admin", client.Name, "client", kickClient.Name));
server.SendOrderTo(kickConn, "ServerError", "You have been kicked from the server."); server.SendOrderTo(kickConn, "ServerError", YouWereKicked);
server.DropClient(kickConn); server.DropClient(kickConn);
if (bool.TryParse(split[1], out var tempBan) && tempBan) if (bool.TryParse(split[1], out var tempBan) && tempBan)
{ {
Log.Write("server", "Temporarily banning client {0} ({1}).", kickClientID, kickClient.IPAddress); Log.Write("server", $"Temporarily banning client {kickClientID} ({kickClient.IPAddress}).");
server.SendMessage($"{client.Name} temporarily banned {kickClient.Name} from the server."); server.SendLocalizedMessage(TempBan, Translation.Arguments("admin", client.Name, "client", kickClient.Name));
server.TempBans.Add(kickClient.IPAddress); server.TempBans.Add(kickClient.IPAddress);
} }
@@ -670,7 +797,7 @@ namespace OpenRA.Mods.Common.Server
{ {
if (!client.IsAdmin) if (!client.IsAdmin)
{ {
server.SendOrderTo(conn, "Message", "Only admins can transfer admin to another player."); server.SendLocalizedMessageTo(conn, NoTransferAdmin);
return true; return true;
} }
@@ -679,7 +806,7 @@ namespace OpenRA.Mods.Common.Server
if (newAdminConn == null) if (newAdminConn == null)
{ {
server.SendOrderTo(conn, "Message", "No-one in that slot."); server.SendLocalizedMessageTo(conn, EmptySlot);
return true; return true;
} }
@@ -693,7 +820,7 @@ namespace OpenRA.Mods.Common.Server
foreach (var b in bots) foreach (var b in bots)
b.BotControllerClientIndex = newAdminId; 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."); Log.Write("server", $"{newAdminClient.Name} is now the admin.");
server.SyncLobbyClients(); server.SyncLobbyClients();
@@ -707,16 +834,15 @@ namespace OpenRA.Mods.Common.Server
{ {
if (!client.IsAdmin) if (!client.IsAdmin)
{ {
server.SendOrderTo(conn, "Message", "Only the host can move players to spectators."); server.SendLocalizedMessageTo(conn, NoMoveSpectators);
return true; return true;
} }
Exts.TryParseIntegerInvariant(s, out var targetId); Exts.TryParseIntegerInvariant(s, out var targetId);
var targetConn = server.Conns.SingleOrDefault(c => server.GetClient(c)?.Index == targetId); var targetConn = server.Conns.SingleOrDefault(c => server.GetClient(c)?.Index == targetId);
if (targetConn == null) if (targetConn == null)
{ {
server.SendOrderTo(conn, "Message", "No-one in that slot."); server.SendLocalizedMessageTo(conn, EmptySlot);
return true; return true;
} }
@@ -727,7 +853,7 @@ namespace OpenRA.Mods.Common.Server
targetClient.Handicap = 0; targetClient.Handicap = 0;
targetClient.Color = Color.White; targetClient.Color = Color.White;
targetClient.State = Session.ClientState.NotReady; 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."); Log.Write("server", $"{client.Name} moved {targetClient.Name} to spectators.");
server.SyncLobbyClients(); server.SyncLobbyClients();
CheckAutoStart(server); CheckAutoStart(server);
@@ -744,8 +870,8 @@ namespace OpenRA.Mods.Common.Server
if (sanitizedName == client.Name) if (sanitizedName == client.Name)
return true; return true;
Log.Write("server", "Player@{0} is now known as {1}.", conn.EndPoint, sanitizedName); Log.Write("server", $"Player@{conn.EndPoint} is now known as {sanitizedName}.");
server.SendMessage($"{client.Name} is now known as {sanitizedName}."); server.SendLocalizedMessage(Nick, Translation.Arguments("player", client.Name, "name", sanitizedName));
client.Name = sanitizedName; client.Name = sanitizedName;
server.SyncLobbyClients(); server.SyncLobbyClients();
@@ -771,14 +897,15 @@ namespace OpenRA.Mods.Common.Server
var factions = server.Map.WorldActorInfo.TraitInfos<FactionInfo>() var factions = server.Map.WorldActorInfo.TraitInfos<FactionInfo>()
.Where(f => f.Selectable).Select(f => f.InternalName); .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.SendLocalizedMessageTo(conn, InvalidFactionSelected, Translation.Arguments("faction", faction));
server.SendOrderTo(conn, "Message", $"Supported values: {factions.JoinWith(", ")}"); server.SendLocalizedMessageTo(conn, SupportedFactions, Translation.Arguments("factions", factions.JoinWith(", ")));
return true; return true;
} }
targetClient.Faction = parts[1]; targetClient.Faction = faction;
server.SyncLobbyClients(); server.SyncLobbyClients();
return true; return true;
@@ -858,7 +985,7 @@ namespace OpenRA.Mods.Common.Server
var existingClient = server.LobbyInfo.Clients.FirstOrDefault(cc => cc.SpawnPoint == spawnPoint); var existingClient = server.LobbyInfo.Clients.FirstOrDefault(cc => cc.SpawnPoint == spawnPoint);
if (client != existingClient && !client.IsAdmin) if (client != existingClient && !client.IsAdmin)
{ {
server.SendOrderTo(conn, "Message", "Only admins can clear spawn points."); server.SendLocalizedMessageTo(conn, AdminClearSpawn);
return true; return true;
} }
@@ -910,13 +1037,13 @@ namespace OpenRA.Mods.Common.Server
if (!Exts.TryParseIntegerInvariant(parts[1], out var spawnPoint) if (!Exts.TryParseIntegerInvariant(parts[1], out var spawnPoint)
|| spawnPoint < 0 || spawnPoint > server.Map.SpawnPoints.Length) || 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; return true;
} }
if (server.LobbyInfo.Clients.Where(cc => cc != client).Any(cc => (cc.SpawnPoint == spawnPoint) && (cc.SpawnPoint != 0))) 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; return true;
} }
@@ -931,7 +1058,7 @@ namespace OpenRA.Mods.Common.Server
if (spawnLockedByAnotherSlot) if (spawnLockedByAnotherSlot)
{ {
server.SendOrderTo(conn, "Message", "The spawn point is locked to another player slot."); server.SendLocalizedMessageTo(conn, SpawnLocked);
return true; return true;
} }
} }
@@ -978,7 +1105,7 @@ namespace OpenRA.Mods.Common.Server
{ {
if (!client.IsAdmin) if (!client.IsAdmin)
{ {
server.SendOrderTo(conn, "Message", "Only the host can set lobby info"); server.SendLocalizedMessageTo(conn, AdminLobbyInfo);
return true; return true;
} }
@@ -989,7 +1116,7 @@ namespace OpenRA.Mods.Common.Server
} }
catch (Exception) catch (Exception)
{ {
server.SendOrderTo(conn, "Message", "Invalid Lobby Info Sent"); server.SendLocalizedMessageTo(conn, InvalidLobbyInfo);
} }
return true; return true;
@@ -1079,8 +1206,8 @@ namespace OpenRA.Mods.Common.Server
Action<string> onError = message => Action<string> onError = message =>
{ {
if (connectionToEcho != null) if (connectionToEcho != null && message != null)
server.SendOrderTo(connectionToEcho, "Message", message); server.SendLocalizedMessageTo(connectionToEcho, message);
}; };
var terrainColors = server.ModData.DefaultTerrainInfo[server.Map.TileSet].RestrictedPlayerColors; var terrainColors = server.ModData.DefaultTerrainInfo[server.Map.TileSet].RestrictedPlayerColors;

View File

@@ -30,11 +30,24 @@ namespace OpenRA.Mods.Common.Server
// 1 second (in milliseconds) minimum delay between pings // 1 second (in milliseconds) minimum delay between pings
const int RateLimitInterval = 1000; 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 Beacon LanGameBeacon;
static readonly Dictionary<int, string> MasterServerErrors = new Dictionary<int, string>() static readonly Dictionary<int, string> MasterServerErrors = new Dictionary<int, string>()
{ {
{ 1, "Server port is not accessible from the internet." }, { 1, NoPortForward },
{ 2, "Server name contains a blacklisted word." } { 2, BlacklistedTitle }
}; };
long lastPing = 0; long lastPing = 0;
@@ -143,25 +156,25 @@ namespace OpenRA.Mods.Common.Server
var regex = new Regex(@"^\[(?<code>-?\d+)\](?<message>.*)"); var regex = new Regex(@"^\[(?<code>-?\d+)\](?<message>.*)");
var match = regex.Match(masterResponseText); var match = regex.Match(masterResponseText);
errorMessage = match.Success && int.TryParse(match.Groups["code"].Value, out errorCode) ? 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; isInitialPing = false;
lock (masterServerMessages) lock (masterServerMessages)
{ {
masterServerMessages.Enqueue("Master server communication established."); masterServerMessages.Enqueue(Connected);
if (errorCode != 0) if (errorCode != 0)
{ {
// Hardcoded error messages take precedence over the server-provided messages // Hardcoded error messages take precedence over the server-provided messages
if (!MasterServerErrors.TryGetValue(errorCode, out var message)) if (!MasterServerErrors.TryGetValue(errorCode, out var message))
message = errorMessage; message = errorMessage;
masterServerMessages.Enqueue("Warning: " + message); masterServerMessages.Enqueue(message);
// Positive error codes indicate errors that prevent advertisement // Positive error codes indicate errors that prevent advertisement
// Negative error codes are non-fatal warnings // Negative error codes are non-fatal warnings
if (errorCode > 0) 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()); Log.Write("server", ex.ToString());
lock (masterServerMessages) lock (masterServerMessages)
masterServerMessages.Enqueue("Master server communication failed."); masterServerMessages.Enqueue(Error);
} }
isBusy = false; isBusy = false;

View File

@@ -9,6 +9,7 @@
*/ */
#endregion #endregion
using System.Collections.Generic;
using System.Linq; using System.Linq;
using OpenRA.Server; using OpenRA.Server;
using S = OpenRA.Server.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 ConnReportInterval = 20000; // Report every 20 seconds
static readonly int ConnTimeout = 60000; // Drop unresponsive clients after 60 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 lastPing = 0;
long lastConnReport = 0; long lastConnReport = 0;
bool isInitialPing = true; bool isInitialPing = true;
@@ -48,7 +61,7 @@ namespace OpenRA.Mods.Common.Server
if (client == null) if (client == null)
{ {
server.DropClient(c); server.DropClient(c);
server.SendMessage("A player has been dropped after timing out."); server.SendLocalizedMessage(PlayerDropped);
continue; continue;
} }
@@ -56,13 +69,13 @@ namespace OpenRA.Mods.Common.Server
{ {
if (!c.TimeoutMessageShown && c.TimeSinceLastResponse > PingInterval * 2) 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; c.TimeoutMessageShown = true;
} }
} }
else else
{ {
server.SendMessage(client.Name + " has been dropped after timing out."); server.SendLocalizedMessage(Timeout, Translation.Arguments("player", client.Name));
server.DropClient(c); server.DropClient(c);
} }
} }
@@ -79,7 +92,14 @@ namespace OpenRA.Mods.Common.Server
{ {
var client = server.GetClient(c); var client = server.GetClient(c);
if (client != null) 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 }
});
}
} }
} }
} }

View File

@@ -60,6 +60,13 @@ namespace OpenRA.Mods.Common.Traits
public Color Color; 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) 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; var closestDistance = SimilarityThreshold;
@@ -148,9 +155,9 @@ namespace OpenRA.Mods.Common.Traits
{ {
var linear = Color.FromAhsv(hue, sat, V).ToLinear(); var linear = Color.FromAhsv(hue, sat, V).ToLinear();
if (TryGetBlockingColor(linear, terrainLinear, out var blocker)) 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)) else if (TryGetBlockingColor(linear, playerLinear, out blocker))
errorMessage = "Color was adjusted to be less similar to another player."; errorMessage = PlayerColorPlayer;
else else
{ {
if (errorMessage != null) 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 // 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); return Color.FromAhsv(random.NextFloat(), float2.Lerp(HsvSaturationRange[0], HsvSaturationRange[1], random.NextFloat()), V);
} }
} }

View File

@@ -107,7 +107,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
widget.Get<LabelWidget>("CONNECTING_DESC").GetText = () => $"Could not connect to {connection.Target}"; widget.Get<LabelWidget>("CONNECTING_DESC").GetText = () => $"Could not connect to {connection.Target}";
var connectionError = widget.Get<LabelWidget>("CONNECTION_ERROR"); 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"); var panelTitle = widget.Get<LabelWidget>("TITLE");
panelTitle.GetText = () => orderManager.AuthenticationFailed ? "Password Required" : "Connection Failed"; panelTitle.GetText = () => orderManager.AuthenticationFailed ? "Password Required" : "Connection Failed";

View File

@@ -142,6 +142,9 @@ ChromeLayout:
cnc|chrome/editor.yaml cnc|chrome/editor.yaml
common|chrome/text-notifications.yaml common|chrome/text-notifications.yaml
Translations:
common|languages/en.ftl
Voices: Voices:
cnc|audio/voices.yaml cnc|audio/voices.yaml

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

View File

@@ -121,6 +121,9 @@ ChromeLayout:
common|chrome/gamesave-loading.yaml common|chrome/gamesave-loading.yaml
common|chrome/text-notifications.yaml common|chrome/text-notifications.yaml
Translations:
common|languages/en.ftl
Weapons: Weapons:
d2k|weapons/debris.yaml d2k|weapons/debris.yaml
d2k|weapons/smallguns.yaml d2k|weapons/smallguns.yaml

View File

@@ -138,6 +138,9 @@ ChromeLayout:
common|chrome/playerprofile.yaml common|chrome/playerprofile.yaml
common|chrome/text-notifications.yaml common|chrome/text-notifications.yaml
Translations:
common|languages/en.ftl
Weapons: Weapons:
ra|weapons/explosions.yaml ra|weapons/explosions.yaml
ra|weapons/ballistics.yaml ra|weapons/ballistics.yaml
@@ -155,9 +158,6 @@ Notifications:
Music: Music:
ra|audio/music.yaml ra|audio/music.yaml
Translations:
ra|languages/english.yaml
Hotkeys: Hotkeys:
common|hotkeys/game.yaml common|hotkeys/game.yaml
common|hotkeys/observer.yaml common|hotkeys/observer.yaml

View File

@@ -183,6 +183,9 @@ ChromeLayout:
common|chrome/editor.yaml common|chrome/editor.yaml
common|chrome/text-notifications.yaml common|chrome/text-notifications.yaml
Translations:
common|languages/en.ftl
Voices: Voices:
ts|audio/voices.yaml ts|audio/voices.yaml