Initial commit: OpenRA game engine
Some checks failed
Continuous Integration / Linux (.NET 8.0) (push) Has been cancelled
Continuous Integration / Windows (.NET 8.0) (push) Has been cancelled

Fork from OpenRA/OpenRA with one-click launch script (start-ra.cmd)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
let5sne.win10
2026-01-10 21:46:54 +08:00
commit 9cf6ebb986
4065 changed files with 635973 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,217 @@
#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;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using BeaconLib;
using OpenRA.Network;
using OpenRA.Server;
using OpenRA.Support;
using S = OpenRA.Server.Server;
namespace OpenRA.Mods.Common.Server
{
public class MasterServerPinger : ServerTrait, ITick, INotifyServerStart, INotifyServerShutdown, INotifySyncLobbyInfo, IStartGame, IEndGame
{
// 3 minutes (in milliseconds). Server has a 5 minute TTL for games, so give ourselves a bit of leeway.
const int MasterPingInterval = 60 * 3 * 1000;
// 1 second (in milliseconds) minimum delay between pings
const int RateLimitInterval = 1000;
[FluentReference]
const string NoPortForward = "notification-no-port-forward";
[FluentReference]
const string BlacklistedTitle = "notification-blacklisted-server-name";
[FluentReference]
const string InvalidErrorCode = "notification-invalid-error-code";
[FluentReference]
const string Connected = "notification-master-server-connected";
[FluentReference]
const string Error = "notification-master-server-error";
[FluentReference]
const string GameOffline = "notification-game-offline";
static readonly ushort LanAdvertisePort = (ushort)new Random(DateTime.Now.Millisecond).Next(2048, 60000);
static readonly FrozenDictionary<int, string> MasterServerErrors = new Dictionary<int, string>
{
{ 1, NoPortForward },
{ 2, BlacklistedTitle }
}.ToFrozenDictionary();
Beacon lanGameBeacon;
long lastPing = 0;
long lastChanged = 0;
bool isInitialPing = true;
volatile bool isBusy;
readonly Queue<string> masterServerMessages = [];
public void Tick(S server)
{
if (!server.IsMultiplayer)
return;
// Force an update if the last one was too long ago so the advertisement doesn't time out
if (Game.RunTime - lastChanged > MasterPingInterval)
lastChanged = Game.RunTime;
// Update the master server and LAN clients if something has changed
// Note that isBusy is set while the master server ping is running on a
// background thread, and limits LAN pings as well as master server pings for simplicity.
if ((server.Settings.AdvertiseOnline || server.Settings.AdvertiseOnLocalNetwork)
&& !isBusy && ((lastChanged > lastPing && Game.RunTime - lastPing > RateLimitInterval) || isInitialPing))
{
var gs = new GameServer(server);
if (server.Settings.AdvertiseOnline)
UpdateMasterServer(server, gs.ToPOSTData(false));
if (server.Settings.AdvertiseOnLocalNetwork && lanGameBeacon != null)
lanGameBeacon.BeaconData = gs.ToPOSTData(true);
lastPing = Game.RunTime;
}
if (server.Settings.AdvertiseOnline)
lock (masterServerMessages)
while (masterServerMessages.Count > 0)
server.SendFluentMessage(masterServerMessages.Dequeue());
}
void INotifyServerStart.ServerStarted(S server)
{
if (server.IsMultiplayer && server.Settings.AdvertiseOnLocalNetwork)
{
if (lanGameBeacon == null)
CreateLanGameBeacon();
lanGameBeacon?.Start();
}
}
void INotifyServerShutdown.ServerShutdown(S server)
{
if (!server.IsMultiplayer)
return;
if (server.Settings.AdvertiseOnline)
{
// Announce that the game has ended to remove it from the list.
var gameServer = new GameServer(server);
UpdateMasterServer(server, gameServer.ToPOSTData(false));
}
lanGameBeacon?.Stop();
lanGameBeacon = null;
}
public void LobbyInfoSynced(S server)
{
lastChanged = Game.RunTime;
}
public void GameStarted(S server)
{
lastChanged = Game.RunTime;
}
public void GameEnded(S server)
{
lanGameBeacon?.Stop();
lanGameBeacon = null;
lastChanged = Game.RunTime;
}
void UpdateMasterServer(S server, string postData)
{
isBusy = true;
Task.Run(async () =>
{
try
{
var endpoint = server.ModData.GetOrCreate<WebServices>().ServerAdvertise;
var client = HttpClientFactory.Create();
var response = await client.PostAsync(endpoint, new StringContent(postData));
var masterResponseText = await response.Content.ReadAsStringAsync();
if (isInitialPing)
{
Log.Write("server", "Master server: " + masterResponseText);
var errorCode = 0;
var errorMessage = string.Empty;
if (!string.IsNullOrWhiteSpace(masterResponseText))
{
var regex = new Regex(@"^\[(?<code>-?\d+)\](?<message>.*)");
var match = regex.Match(masterResponseText);
errorMessage = match.Success && int.TryParse(match.Groups["code"].Value, out errorCode) ?
match.Groups["message"].Value.Trim() : InvalidErrorCode;
}
isInitialPing = false;
lock (masterServerMessages)
{
masterServerMessages.Enqueue(Connected);
if (errorCode != 0)
{
// Hardcoded error messages take precedence over the server-provided messages
if (!MasterServerErrors.TryGetValue(errorCode, out var message))
message = errorMessage;
masterServerMessages.Enqueue(message);
// Positive error codes indicate errors that prevent advertisement
// Negative error codes are non-fatal warnings
if (errorCode > 0)
masterServerMessages.Enqueue(GameOffline);
}
}
}
}
catch (Exception ex)
{
Log.Write("server", ex.ToString());
lock (masterServerMessages)
masterServerMessages.Enqueue(Error);
}
isBusy = false;
});
}
void CreateLanGameBeacon()
{
try
{
lanGameBeacon?.Stop();
lanGameBeacon = new Beacon("OpenRALANGame", LanAdvertisePort);
}
catch (Exception ex)
{
lanGameBeacon = null;
Log.Write("server", "BeaconLib.Beacon: " + ex.Message);
}
}
}
}

View File

@@ -0,0 +1,104 @@
#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.Linq;
using OpenRA.Server;
using S = OpenRA.Server.Server;
namespace OpenRA.Mods.Common.Server
{
public class PlayerPinger : ServerTrait, ITick
{
[FluentReference]
const string PlayerDropped = "notification-player-dropped";
[FluentReference("player")]
const string ConnectionProblems = "notification-connection-problems";
[FluentReference("player")]
const string Timeout = "notification-timeout-dropped";
[FluentReference("player", "timeout")]
const string TimeoutIn = "notification-timeout-dropped-in";
const int PingInterval = 5000; // Ping every 5 seconds
const int ConnReportInterval = 20000; // Report every 20 seconds
const int ConnTimeout = 60000; // Drop unresponsive clients after 60 seconds
long lastPing = 0;
long lastConnReport = 0;
bool isInitialPing = true;
public void Tick(S server)
{
if ((Game.RunTime - lastPing > PingInterval) || isInitialPing)
{
isInitialPing = false;
lastPing = Game.RunTime;
// Ignore client timeout in singleplayer games to make debugging easier
var nonBotClientCount = 0;
lock (server.LobbyInfo)
nonBotClientCount = server.LobbyInfo.NonBotClients.Count();
if (nonBotClientCount >= 2 || server.Type == ServerType.Dedicated)
{
foreach (var c in server.Conns.ToList())
{
if (!c.Validated)
continue;
var client = server.GetClient(c);
if (client == null)
{
server.DropClient(c);
server.SendFluentMessage(PlayerDropped);
continue;
}
if (c.TimeSinceLastResponse < ConnTimeout)
{
if (!c.TimeoutMessageShown && c.TimeSinceLastResponse > PingInterval * 2)
{
server.SendFluentMessage(ConnectionProblems, "player", client.Name);
c.TimeoutMessageShown = true;
}
}
else
{
server.SendFluentMessage(Timeout, "player", client.Name);
server.DropClient(c);
}
}
if (Game.RunTime - lastConnReport > ConnReportInterval)
{
lastConnReport = Game.RunTime;
var timeouts = server.Conns
.Where(c => c.Validated && c.TimeSinceLastResponse > ConnReportInterval && c.TimeSinceLastResponse < ConnTimeout)
.OrderBy(c => c.TimeSinceLastResponse);
foreach (var c in timeouts)
{
var client = server.GetClient(c);
if (client != null)
{
var timeout = (ConnTimeout - c.TimeSinceLastResponse) / 1000;
server.SendFluentMessage(TimeoutIn, "player", client.Name, "timeout", timeout);
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,191 @@
#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.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
{
[YamlNode("Skirmish", shared: false)]
public class SkirmishSettings : SettingsModule { }
sealed class SkirmishSlot
{
static string LoadSlot(MiniYaml yaml) => yaml.Value;
[FieldLoader.LoadUsing(nameof(LoadSlot))]
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;
}
public MiniYaml ToYaml()
{
var yaml = FieldSaver.Save(this);
return yaml.WithValue(Slot).WithNodes(yaml.Nodes.RemoveAll(n => n.Key == nameof(Slot)));
}
}
static bool TryInitializeFromSettings(S server, SkirmishSettings skirmishSettings, Connection conn)
{
var nodes = skirmishSettings.Yaml.Build();
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 selectableFactions = server.Map.WorldActorInfo.TraitInfos<FactionInfo>()
.Where(f => f.Selectable)
.Select(f => f.InternalName)
.ToList();
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);
client.Faction = LobbyCommands.SanitizePlayerFaction(server, client.Faction, selectableFactions);
}
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);
client.Faction = LobbyCommands.SanitizePlayerFaction(server, client.Faction, selectableFactions);
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 playerClient = server.LobbyInfo.NonBotClients.First();
var nodes = 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", new SkirmishSlot(playerClient).ToYaml()),
new("Bots", new MiniYaml("", server.LobbyInfo.Clients.Where(c => c.IsBot)
.Select(b => new MiniYamlNode(b.Bot, new SkirmishSlot(b).ToYaml()))))
};
var skirmishSettings = server.ModData.GetSettings<SkirmishSettings>();
skirmishSettings.Yaml.Nodes = new MiniYamlBuilder("", nodes).Nodes;
skirmishSettings.Save();
}
void IClientJoined.ClientJoined(S server, Connection conn)
{
if (server.Type != ServerType.Skirmish)
return;
var skirmishSettings = server.ModData.GetSettings<SkirmishSettings>();
if (TryInitializeFromSettings(server, skirmishSettings, 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);
}
}
}