Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Persist skirmish settings between sessions #21206

Merged
merged 4 commits into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions OpenRA.Game/Game.cs
Expand Up @@ -936,7 +936,7 @@ public static ConnectionTarget CreateServer(ServerSettings settings)
return server.GetEndpointForLocalConnection();
}

public static ConnectionTarget CreateLocalServer(string map)
public static ConnectionTarget CreateLocalServer(string map, bool isSkirmish = false)
{
var settings = new ServerSettings()
{
Expand All @@ -952,7 +952,7 @@ public static ConnectionTarget CreateLocalServer(string map)
{
new(IPAddress.Loopback, 0)
};
server = new Server.Server(endpoints, settings, ModData, ServerType.Local);
server = new Server.Server(endpoints, settings, ModData, isSkirmish ? ServerType.Skirmish : ServerType.Local);

return server.GetEndpointForLocalConnection();
}
Expand Down
2 changes: 1 addition & 1 deletion OpenRA.Game/Server/Connection.cs
Expand Up @@ -115,7 +115,7 @@ void SendReceiveLoop(object s)
frame = BitConverter.ToInt32(bytes, 4);
state = ReceiveState.Data;

if (expectLength < 0 || (server.Type != ServerType.Local && expectLength > MaxOrderLength))
if (expectLength < 0 || (server.IsMultiplayer && expectLength > MaxOrderLength))
{
Log.Write("server", $"Closing socket connection to {EndPoint} because of excessive order length: {expectLength}");
return;
Expand Down
34 changes: 21 additions & 13 deletions OpenRA.Game/Server/Server.cs
Expand Up @@ -40,8 +40,9 @@ public enum ServerState
public enum ServerType
{
Local = 0,
Multiplayer = 1,
Dedicated = 2
Skirmish = 1,
Multiplayer = 2,
Dedicated = 3
PunkPun marked this conversation as resolved.
Show resolved Hide resolved
}

public sealed class Server
Expand Down Expand Up @@ -117,6 +118,7 @@ public sealed class Server

public readonly MersenneTwister Random = new();
public readonly ServerType Type;
public bool IsMultiplayer => Type == ServerType.Dedicated || Type == ServerType.Multiplayer;

public readonly List<Connection> Conns = new();

Expand Down Expand Up @@ -304,10 +306,10 @@ public Server(List<IPEndPoint> endpoints, ServerSettings settings, ModData modDa

randomSeed = (int)DateTime.Now.ToBinary();

if (type != ServerType.Local && settings.EnableGeoIP)
if (IsMultiplayer && settings.EnableGeoIP)
GeoIP.Initialize();

if (type != ServerType.Local)
if (IsMultiplayer)
Nat.TryForwardPort(Settings.ListenPort, Settings.ListenPort);

foreach (var trait in modData.Manifest.ServerTraits)
Expand Down Expand Up @@ -381,7 +383,7 @@ public Server(List<IPEndPoint> endpoints, ServerSettings settings, ModData modDa
if (State == ServerState.ShuttingDown)
{
EndGame();
if (type != ServerType.Local)
if (IsMultiplayer)
Nat.TryRemovePortForward();
break;
}
Expand Down Expand Up @@ -489,7 +491,7 @@ void ValidateClient(Connection newConn, string data)
{
Name = OpenRA.Settings.SanitizedPlayerName(handshake.Client.Name),
IPAddress = ipAddress.ToString(),
AnonymizedIPAddress = Type != ServerType.Local && Settings.ShareAnonymizedIPs ? Session.AnonymizeIP(ipAddress) : null,
AnonymizedIPAddress = IsMultiplayer && Settings.ShareAnonymizedIPs ? Session.AnonymizeIP(ipAddress) : null,
Location = GeoIP.LookupCountry(ipAddress),
Index = newConn.PlayerIndex,
PreferredColor = handshake.Client.PreferredColor,
Expand Down Expand Up @@ -601,7 +603,7 @@ void CompleteConnection()
}
}

if (Type == ServerType.Local)
if (!IsMultiplayer)
{
// Local servers can only be joined by the local client, so we can trust their identity without validation
client.Fingerprint = handshake.Fingerprint;
Expand Down Expand Up @@ -992,10 +994,7 @@ void InterpretServerOrder(Connection conn, Order o)
{
case "Command":
{
var handledBy = serverTraits.WithInterface<IInterpretCommand>()
.FirstOrDefault(t => t.InterpretCommand(this, conn, GetClient(conn), o.TargetString));

if (handledBy == null)
if (!InterpretCommand(o.TargetString, conn))
{
Log.Write("server", $"Unknown server command: {o.TargetString}");
SendLocalizedMessageTo(conn, UnknownServerCommand, Translation.Arguments("command", o.TargetString));
Expand All @@ -1006,7 +1005,7 @@ void InterpretServerOrder(Connection conn, Order o)

case "Chat":
{
if (Type == ServerType.Local || !playerMessageTracker.IsPlayerAtFloodLimit(conn))
if (!IsMultiplayer || !playerMessageTracker.IsPlayerAtFloodLimit(conn))
DispatchOrdersToClients(conn, 0, o.Serialize());

break;
Expand Down Expand Up @@ -1352,7 +1351,7 @@ public void StartGame()

State = ServerState.GameStarted;

if (Type != ServerType.Local)
if (IsMultiplayer)
OrderLatency = gameSpeed.OrderLatency;

if (GameSave == null && LobbyInfo.GlobalSettings.GameSavesEnabled)
Expand Down Expand Up @@ -1412,6 +1411,15 @@ public void StartGame()
}
}

public bool InterpretCommand(string command, Connection conn)
{
foreach (var t in serverTraits.WithInterface<IInterpretCommand>())
if (t.InterpretCommand(this, conn, GetClient(conn), command))
return true;

return false;
}

public ConnectionTarget GetEndpointForLocalConnection()
{
var endpoints = new List<DnsEndPoint>();
Expand Down
8 changes: 7 additions & 1 deletion OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs
Expand Up @@ -729,6 +729,12 @@ static bool Option(S server, Connection conn, Session.Client client, string s)
if (oo.Value == split[1])
return true;

if (!option.Values.ContainsKey(split[1]))
{
server.SendLocalizedMessageTo(conn, InvalidConfigurationCommand);
return true;
}

oo.Value = oo.PreferredValue = split[1];

server.SyncLobbyGlobalSettings();
Expand Down Expand Up @@ -1410,7 +1416,7 @@ public static void LoadMapSettings(S server, Session.Global gs, MapPreview map)
}
}

static Color SanitizePlayerColor(S server, Color askedColor, int playerIndex, Connection connectionToEcho = null)
public static Color SanitizePlayerColor(S server, Color askedColor, int playerIndex, Connection connectionToEcho = null)
{
lock (server.LobbyInfo)
{
Expand Down
2 changes: 1 addition & 1 deletion OpenRA.Mods.Common/ServerTraits/MasterServerPinger.cs
Expand Up @@ -102,7 +102,7 @@ public void Tick(S server)

void INotifyServerStart.ServerStarted(S server)
{
if (server.Type != ServerType.Local && LanGameBeacon != null)
if (server.IsMultiplayer && LanGameBeacon != null)
LanGameBeacon.Start();
}

Expand Down
173 changes: 173 additions & 0 deletions OpenRA.Mods.Common/ServerTraits/SkirmishLogic.cs
@@ -0,0 +1,173 @@
#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.IO;
using System.Linq;
using OpenRA.Network;
using OpenRA.Primitives;
using OpenRA.Server;
using OpenRA.Traits;
using S = OpenRA.Server.Server;

namespace OpenRA.Mods.Common.Server
{
public class SkirmishLogic : ServerTrait, IClientJoined, INotifySyncLobbyInfo
{
class SkirmishSlot
PunkPun marked this conversation as resolved.
Show resolved Hide resolved
{
[FieldLoader.Serialize(FromYamlKey = true)]
public readonly string Slot;
public readonly Color Color;
public readonly string Faction;
public readonly int SpawnPoint;
public readonly int Team;
public readonly int Handicap;

public SkirmishSlot() { }

public SkirmishSlot(Session.Client c)
{
Slot = c.Slot;
Color = c.Color;
Faction = c.Faction;
SpawnPoint = c.SpawnPoint;
Team = c.Team;
Handicap = c.Handicap;
}

public static void DeserializeToClient(MiniYaml yaml, Session.Client c)
{
var s = FieldLoader.Load<SkirmishSlot>(yaml);
c.Slot = s.Slot;
c.Color = c.PreferredColor = s.Color;
c.Faction = s.Faction;
c.SpawnPoint = s.SpawnPoint;
c.Team = s.Team;
c.Handicap = s.Handicap;
}
}

static bool TryInitializeFromFile(S server, string path, Connection conn)
{
if (!File.Exists(path))
return false;

var nodes = new MiniYaml("", MiniYaml.FromFile(path));
var mapNode = nodes.NodeWithKeyOrDefault("Map");
if (mapNode == null)
return false;

// Only set players and options if the map is available
if (server.LobbyInfo.GlobalSettings.Map != mapNode.Value.Value)
{
var map = server.ModData.MapCache[mapNode.Value.Value];
if (map.Status != MapStatus.Available || !server.InterpretCommand($"map {map.Uid}", conn))
return false;
}

var optionsNode = nodes.NodeWithKeyOrDefault("Options");
if (optionsNode != null)
{
var options = server.Map.PlayerActorInfo.TraitInfos<ILobbyOptions>()
.Concat(server.Map.WorldActorInfo.TraitInfos<ILobbyOptions>())
.SelectMany(t => t.LobbyOptions(server.Map))
.ToDictionary(o => o.Id, o => o);

foreach (var optionNode in optionsNode.Value.Nodes)
{
if (options.TryGetValue(optionNode.Key, out var option) && !option.IsLocked && option.Values.ContainsKey(optionNode.Value.Value))
{
var oo = server.LobbyInfo.GlobalSettings.LobbyOptions[option.Id];
oo.Value = oo.PreferredValue = optionNode.Value.Value;
}
}
}

var playerNode = nodes.NodeWithKeyOrDefault("Player");
if (playerNode != null)
{
var client = server.GetClient(conn);
SkirmishSlot.DeserializeToClient(playerNode.Value, client);
client.Color = LobbyCommands.SanitizePlayerColor(server, client.Color, client.Index);
}

var botsNode = nodes.NodeWithKeyOrDefault("Bots");
if (botsNode != null)
{
var botController = server.LobbyInfo.Clients.First(c => c.IsAdmin);
foreach (var botNode in botsNode.Value.Nodes)
{
var botInfo = server.Map.PlayerActorInfo.TraitInfos<IBotInfo>()
.FirstOrDefault(b => b.Type == botNode.Key);

if (botInfo == null)
continue;

var client = new Session.Client
{
Index = server.ChooseFreePlayerIndex(),
Name = botInfo.Name,
Bot = botInfo.Type,
Slot = botNode.Value.Value,
State = Session.ClientState.NotReady,
BotControllerClientIndex = botController.Index
};

SkirmishSlot.DeserializeToClient(botNode.Value, client);

// Validate whether color is allowed and get an alternative if it isn't
if (client.Slot != null && !server.LobbyInfo.Slots[client.Slot].LockColor)
client.Color = LobbyCommands.SanitizePlayerColor(server, client.Color, client.Index);

server.LobbyInfo.Clients.Add(client);
S.SyncClientToPlayerReference(client, server.Map.Players.Players[client.Slot]);
}
}

return true;
}

void INotifySyncLobbyInfo.LobbyInfoSynced(S server)
{
if (server.Type != ServerType.Skirmish)
return;

var path = Path.Combine(Platform.SupportDir, $"skirmish.{server.ModData.Manifest.Id}.yaml");
var playerClient = server.LobbyInfo.NonBotClients.First();
new List<MiniYamlNode>
{
new("Map", server.LobbyInfo.GlobalSettings.Map),
new("Options", new MiniYaml("", server.LobbyInfo.GlobalSettings.LobbyOptions
.Select(kv => new MiniYamlNode(kv.Key, kv.Value.Value)))),
new("Player", FieldSaver.Save(new SkirmishSlot(playerClient))),
new("Bots", new MiniYaml("", server.LobbyInfo.Clients.Where(c => c.IsBot)
.Select(b => new MiniYamlNode(b.Bot, FieldSaver.Save(new SkirmishSlot(b))))))
}.WriteToFile(path);
}

void IClientJoined.ClientJoined(S server, Connection conn)
{
if (server.Type != ServerType.Skirmish)
return;

var skirmishFile = Path.Combine(Platform.SupportDir, $"skirmish.{server.ModData.Manifest.Id}.yaml");
if (TryInitializeFromFile(server, skirmishFile, conn))
return;

var slot = server.LobbyInfo.FirstEmptyBotSlot();
var bot = server.Map.PlayerActorInfo.TraitInfos<IBotInfo>().Select(t => t.Type).FirstOrDefault();
var botController = server.LobbyInfo.Clients.FirstOrDefault(c => c.IsAdmin);
if (slot != null && bot != null)
server.InterpretCommand($"slot_bot {slot} {botController.Index} {bot}", conn);
}
}
}
21 changes: 3 additions & 18 deletions OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs
Expand Up @@ -98,7 +98,6 @@ enum PanelType { Players, Options, Music, Servers, Kick, ForceStart }
Session.MapStatus mapStatus;

bool chatEnabled;
bool addBotOnMapLoad;
bool disableTeamChat;
bool insufficientPlayerSpawns;
bool teamChat;
Expand Down Expand Up @@ -552,10 +551,6 @@ void StartGame()
});
}

// Add a bot on the first lobbyinfo update
if (skirmishMode)
addBotOnMapLoad = true;

if (logicArgs.TryGetValue("ChatLineSound", out var yaml))
chatLineSound = yaml.Value;
if (logicArgs.TryGetValue("PlayerJoinedSound", out yaml))
Expand Down Expand Up @@ -647,22 +642,12 @@ void UpdateCurrentMap()
return;

map = modData.MapCache[uid];

// Tell the server that we have the map
if (map.Status == MapStatus.Available)
{
// Tell the server that we have the map
orderManager.IssueOrder(Order.Command($"state {Session.ClientState.NotReady}"));

if (addBotOnMapLoad)
{
var slot = orderManager.LobbyInfo.FirstEmptyBotSlot();
var bot = map.PlayerActorInfo.TraitInfos<IBotInfo>().Select(t => t.Type).FirstOrDefault();
var botController = orderManager.LobbyInfo.Clients.FirstOrDefault(c => c.IsAdmin);
if (slot != null && bot != null)
orderManager.IssueOrder(Order.Command($"slot_bot {slot} {botController.Index} {bot}"));

addBotOnMapLoad = false;
}
}
// We don't have the map
else if (map.Status != MapStatus.DownloadAvailable && Game.Settings.Game.AllowDownloading)
modData.MapCache.QueryRemoteMapDetails(services.MapRepository, new[] { uid });
}
Expand Down
2 changes: 1 addition & 1 deletion OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs
Expand Up @@ -459,7 +459,7 @@ void StartSkirmishGame()
Game.Settings.Server.Map = map;
Game.Settings.Save();

ConnectionLogic.Connect(Game.CreateLocalServer(map),
ConnectionLogic.Connect(Game.CreateLocalServer(map, isSkirmish: true),
"",
OpenSkirmishLobbyPanel,
() => { Game.CloseServer(); SwitchMenu(MenuType.Main); });
Expand Down
1 change: 1 addition & 0 deletions mods/cnc/mod.yaml
Expand Up @@ -178,6 +178,7 @@ LoadScreen: CncLoadScreen

ServerTraits:
LobbyCommands
SkirmishLogic
PlayerPinger
MasterServerPinger
LobbySettingsNotification
Expand Down