Files
OpenRA/OpenRA.Game/Network/UnitOrders.cs
RoosterDragon 8f46247dc9 MiniYaml.From* methods support deferred execution.
Previously, the MiniYaml.From* helpers such as FromStream would consume the entire input and then return a list of top-level nodes. Now, the input is processed using deferred execution and top-level nodes are yielded as they are resolved from the input.

The motivating use-case is MapCache, which currently manually buffers nodes before passing to MiniYaml.FromString in order to improve responsiveness when large payloads are processed. Now that MiniYaml.FromStream yields results back as they come in, we can switch to that without disadvantage.

The maintains the performance where map cache can update search results as each node comes in over the network rather than having to wait for the entire batch to be transferred. Now we can also remove the string buffer that captures each node, reducing memory pressure and simplifying the code.
2025-04-13 14:12:25 +03:00

423 lines
13 KiB
C#

#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System.Collections.Generic;
using System.Linq;
using OpenRA.Server;
using OpenRA.Traits;
namespace OpenRA.Network
{
public static class UnitOrders
{
public const int ChatMessageMaxLength = 2500;
[FluentReference("player")]
const string Joined = "notification-joined";
[FluentReference("player")]
const string Left = "notification-lobby-disconnected";
[FluentReference]
const string GameStarted = "notification-game-has-started";
[FluentReference]
const string GameSaved = "notification-game-saved";
[FluentReference("player")]
const string GamePaused = "notification-game-paused";
[FluentReference("player")]
const string GameUnpaused = "notification-game-unpaused";
public static int? KickVoteTarget { get; internal set; }
static Player FindPlayerByClient(this World world, Session.Client c)
{
return world.Players.FirstOrDefault(p => p.ClientIndex == c.Index && p.PlayerReference.Playable);
}
static bool OrderNotFromServerOrWorldIsReplay(int clientId, World world) => clientId != 0 || (world != null && world.IsReplay);
internal static void ProcessOrder(OrderManager orderManager, World world, int clientId, Order order)
{
switch (order.OrderString)
{
// Server message
case "Message":
TextNotificationsManager.AddSystemLine(order.TargetString);
break;
// Client side resolved server message
case "FluentMessage":
{
if (string.IsNullOrEmpty(order.TargetString))
break;
var yaml = MiniYaml.FromString(order.TargetString, order.OrderString);
foreach (var node in yaml)
{
var message = new FluentMessage(node.Value);
if (message.Key == Joined)
TextNotificationsManager.AddPlayerJoinedLine(message.Key, message.Arguments);
else if (message.Key == Left)
TextNotificationsManager.AddPlayerLeftLine(message.Key, message.Arguments);
else
TextNotificationsManager.AddSystemLine(message.Key, message.Arguments);
}
break;
}
case "DisableChatEntry":
{
if (OrderNotFromServerOrWorldIsReplay(clientId, world))
break;
// Server may send MaxValue to indicate that it is disabled until further notice
if (order.ExtraData == uint.MaxValue)
TextNotificationsManager.ChatDisabledUntil = uint.MaxValue;
else
TextNotificationsManager.ChatDisabledUntil = Game.RunTime + order.ExtraData;
break;
}
case "StartKickVote":
{
if (OrderNotFromServerOrWorldIsReplay(clientId, world))
break;
KickVoteTarget = (int)order.ExtraData;
break;
}
case "EndKickVote":
{
if (OrderNotFromServerOrWorldIsReplay(clientId, world))
break;
if (KickVoteTarget == (int)order.ExtraData)
KickVoteTarget = null;
break;
}
case "Chat":
{
var client = orderManager.LobbyInfo.ClientWithIndex(clientId);
if (client == null)
break;
// Cut chat messages to the hard limit to avoid exploits
var message = order.TargetString;
if (message.Length > ChatMessageMaxLength)
message = order.TargetString[..ChatMessageMaxLength];
// ExtraData 0 means this is a normal chat order, everything else is team chat
if (order.ExtraData == 0)
{
var p = world?.FindPlayerByClient(client);
var suffix = (p != null && p.WinState == WinState.Lost) ? " (Dead)" : "";
suffix = client.IsObserver ? " (Spectator)" : suffix;
if (orderManager.LocalClient != null && client != orderManager.LocalClient && client.Team > 0 && client.Team == orderManager.LocalClient.Team)
suffix += " (Ally)";
TextNotificationsManager.AddChatLine(clientId, client.Name + suffix, message, client.Color);
break;
}
// We are still in the lobby
if (world == null)
{
var prefix = order.ExtraData == uint.MaxValue ? "[Spectators] " : "[Team] ";
if (orderManager.LocalClient != null && client.Team == orderManager.LocalClient.Team)
TextNotificationsManager.AddChatLine(clientId, prefix + client.Name, message, client.Color);
break;
}
var player = world.FindPlayerByClient(client);
var localClientIsObserver = world.IsReplay || (orderManager.LocalClient != null && orderManager.LocalClient.IsObserver)
|| (world.LocalPlayer != null && world.LocalPlayer.WinState != WinState.Undefined);
// ExtraData gives us the team number, uint.MaxValue means Spectators
if (order.ExtraData == uint.MaxValue && localClientIsObserver)
{
// Validate before adding the line
if (client.IsObserver || (player != null && player.WinState != WinState.Undefined))
TextNotificationsManager.AddChatLine(clientId, "[Spectators] " + client.Name, message, client.Color);
break;
}
var valid = client.Team == order.ExtraData && player != null && player.WinState == WinState.Undefined;
var isSameTeam = orderManager.LocalClient != null && order.ExtraData == orderManager.LocalClient.Team
&& world.LocalPlayer != null && world.LocalPlayer.WinState == WinState.Undefined;
if (valid && (isSameTeam || world.IsReplay))
TextNotificationsManager.AddChatLine(clientId, "[Team" + (world.IsReplay ? " " + order.ExtraData : "") + "] " + client.Name, message, client.Color);
break;
}
case "StartGame":
{
if (Game.ModData.MapCache[orderManager.LobbyInfo.GlobalSettings.Map].Status != MapStatus.Available)
{
Game.Disconnect();
Game.LoadShellMap();
// TODO: After adding a startup error dialog, notify the replay load failure.
break;
}
if (!string.IsNullOrEmpty(order.TargetString))
{
var data = MiniYaml.FromString(order.TargetString, order.OrderString).ToList();
var saveLastOrdersFrame = data.FirstOrDefault(n => n.Key == "SaveLastOrdersFrame");
if (saveLastOrdersFrame != null)
orderManager.GameSaveLastFrame =
FieldLoader.GetValue<int>("saveLastOrdersFrame", saveLastOrdersFrame.Value.Value);
var saveSyncFrame = data.FirstOrDefault(n => n.Key == "SaveSyncFrame");
if (saveSyncFrame != null)
orderManager.GameSaveLastSyncFrame =
FieldLoader.GetValue<int>("SaveSyncFrame", saveSyncFrame.Value.Value);
}
else
TextNotificationsManager.AddSystemLine(GameStarted);
Game.StartGame(orderManager.LobbyInfo.GlobalSettings.Map, WorldType.Regular);
break;
}
case "SaveTraitData":
{
var data = MiniYaml.FromString(order.TargetString, order.OrderString).First();
var traitIndex = Exts.ParseInt32Invariant(data.Key);
world?.AddGameSaveTraitData(traitIndex, data.Value);
break;
}
case "GameSaved":
if (!orderManager.World.IsReplay)
TextNotificationsManager.AddSystemLine(GameSaved);
foreach (var nsr in orderManager.World.WorldActor.TraitsImplementing<INotifyGameSaved>())
nsr.GameSaved(orderManager.World);
break;
case "PauseGame":
{
var client = orderManager.LobbyInfo.ClientWithIndex(clientId);
if (client != null)
{
var pause = order.TargetString == "Pause";
// Prevent injected unpause orders from restarting a finished game
if (orderManager.World.IsGameOver && !pause)
break;
if (orderManager.World.Paused != pause && world != null && world.LobbyInfo.NonBotClients.Count() > 1)
TextNotificationsManager.AddSystemLine(pause ? GamePaused : GameUnpaused, "player", client.Name);
orderManager.World.Paused = pause;
orderManager.World.PredictedPaused = pause;
}
break;
}
case "HandshakeRequest":
{
// Switch to the server's mod if we need and are able to
var mod = Game.ModData.Manifest;
var request = HandshakeRequest.Deserialize(order.TargetString, order.OrderString);
var externalKey = ExternalMod.MakeKey(request.Mod, request.Version);
if ((request.Mod != mod.Id || request.Version != mod.Metadata.Version) &&
Game.ExternalMods.TryGetValue(externalKey, out var external))
{
// The ConnectionFailedLogic will prompt the user to switch mods
CurrentServerSettings.ServerExternalMod = external;
orderManager.Connection.Dispose();
break;
}
Game.Settings.Player.Name = Settings.SanitizedPlayerName(Game.Settings.Player.Name);
Game.Settings.Save();
// Otherwise send the handshake with our current settings and let the server reject us
var info = new Session.Client()
{
Name = Game.Settings.Player.Name,
PreferredColor = Game.Settings.Player.Color,
Color = Game.Settings.Player.Color,
Faction = "Random",
SpawnPoint = 0,
Team = 0,
State = Session.ClientState.Invalid
};
var localProfile = Game.LocalPlayerProfile;
var response = new HandshakeResponse()
{
Client = info,
Mod = mod.Id,
Version = mod.Metadata.Version,
Password = CurrentServerSettings.Password,
Fingerprint = localProfile.Fingerprint,
OrdersProtocol = ProtocolVersion.Orders
};
if (request.AuthToken != null && response.Fingerprint != null)
response.AuthSignature = localProfile.Sign(request.AuthToken);
orderManager.IssueOrder(new Order("HandshakeResponse", null, false)
{
Type = OrderType.Handshake,
IsImmediate = true,
TargetString = response.Serialize()
});
break;
}
case "ServerError":
{
orderManager.ServerError = order.TargetString;
orderManager.AuthenticationFailed = false;
break;
}
case "AuthenticationError":
{
// The ConnectionFailedLogic will prompt the user for the password
orderManager.ServerError = order.TargetString;
orderManager.AuthenticationFailed = true;
break;
}
case "SyncInfo":
{
orderManager.LobbyInfo = Session.Deserialize(order.TargetString, order.OrderString);
Game.SyncLobbyInfo();
break;
}
case "SyncLobbyClients":
{
var clients = new List<Session.Client>();
var nodes = MiniYaml.FromString(order.TargetString, order.OrderString);
foreach (var node in nodes)
{
var strings = node.Key.Split('@');
if (strings[0] == "Client")
clients.Add(Session.Client.Deserialize(node.Value));
}
orderManager.LobbyInfo.Clients = clients;
Game.SyncLobbyInfo();
break;
}
case "SyncLobbySlots":
{
var slots = new Dictionary<string, Session.Slot>();
var nodes = MiniYaml.FromString(order.TargetString, order.OrderString);
foreach (var node in nodes)
{
var strings = node.Key.Split('@');
if (strings[0] == "Slot")
{
var slot = Session.Slot.Deserialize(node.Value);
slots.Add(slot.PlayerReference, slot);
}
}
orderManager.LobbyInfo.Slots = slots;
Game.SyncLobbyInfo();
break;
}
case "SyncLobbyGlobalSettings":
{
var nodes = MiniYaml.FromString(order.TargetString, order.OrderString);
foreach (var node in nodes)
{
var strings = node.Key.Split('@');
if (strings[0] == "GlobalSettings")
orderManager.LobbyInfo.GlobalSettings = Session.Global.Deserialize(node.Value);
}
Game.SyncLobbyInfo();
break;
}
case "SyncConnectionQuality":
{
var nodes = MiniYaml.FromString(order.TargetString, order.OrderString);
foreach (var node in nodes)
{
var strings = node.Key.Split('@');
if (strings[0] == "ConnectionQuality")
{
var client = orderManager.LobbyInfo.Clients.FirstOrDefault(c => c.Index == Exts.ParseInt32Invariant(strings[1]));
if (client != null)
client.ConnectionQuality = FieldLoader.GetValue<Session.ConnectionQuality>("ConnectionQuality", node.Value.Value);
}
}
break;
}
case "SyncMapPool":
{
orderManager.ServerMapPool = FieldLoader.GetValue<HashSet<string>>("SyncMapPool", order.TargetString);
break;
}
default:
{
if (world == null)
break;
if (order.GroupedActors == null)
ResolveOrder(order, world, orderManager, clientId);
else
foreach (var subject in order.GroupedActors)
ResolveOrder(Order.FromGroupedOrder(order, subject), world, orderManager, clientId);
break;
}
}
}
static void ResolveOrder(Order order, World world, OrderManager orderManager, int clientId)
{
if (order.Subject == null || order.Subject.IsDead)
return;
if (world.OrderValidators.All(vo => vo.OrderValidation(orderManager, world, clientId, order)))
order.Subject.ResolveOrder(order);
}
public static void Clear()
{
KickVoteTarget = null;
}
}
}