Files
OpenRA/OpenRA.Mods.Common/ServerTraits/MasterServerPinger.cs
RoosterDragon 9e548fd0f0 Freeze non-mutable readonly static dictionaries.
Since these will live for the lifetime of the process, it is worth freezing them on creation to improve lookup performance. If the dictionaries were iterated, change to an immutable array instead to preserve the order.
2025-12-12 09:51:27 +02:00

218 lines
6.1 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;
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.Manifest.Get<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);
}
}
}
}