From ffc3f6e0d086ab1846d6feb75d52f9135e2dd3dd Mon Sep 17 00:00:00 2001 From: rob-v Date: Wed, 19 Apr 2017 10:20:51 +0200 Subject: [PATCH] LAN games discovery --- AUTHORS | 2 + Makefile | 3 +- OpenRA.Game/Platform.cs | 1 + OpenRA.Mods.Common/OpenRA.Mods.Common.csproj | 4 + .../ServerTraits/MasterServerPinger.cs | 93 ++++++++++++++----- .../Widgets/Logic/MultiplayerLogic.cs | 35 ++++++- mods/cnc/chrome/multiplayer-browser.yaml | 2 +- mods/common/chrome/multiplayer-browser.yaml | 2 +- packaging/windows/OpenRA.nsi | 2 + thirdparty/fetch-thirdparty-deps.ps1 | 8 ++ thirdparty/fetch-thirdparty-deps.sh | 7 ++ 11 files changed, 132 insertions(+), 27 deletions(-) diff --git a/AUTHORS b/AUTHORS index 41576e1129..86657f0ebb 100644 --- a/AUTHORS +++ b/AUTHORS @@ -170,6 +170,8 @@ Krueger and distributed under the GNU GPL terms. Using SmartIrc4Net developed by Mirco Bauer distributed under the LGPL version 2.1 or later. +Using rix0rrr.BeaconLib developed by Rico Huijbers +distributed under MIT License. Finally, special thanks goes to the original teams at Westwood Studios and EA for creating the classic diff --git a/Makefile b/Makefile index 48895d1c4c..0d59512b93 100644 --- a/Makefile +++ b/Makefile @@ -40,7 +40,7 @@ SDK ?= CSC = mcs $(SDK) CSFLAGS = -nologo -warn:4 -codepage:utf8 -unsafe -warnaserror DEFINE = TRACE -COMMON_LIBS = System.dll System.Core.dll System.Data.dll System.Data.DataSetExtensions.dll System.Drawing.dll System.Xml.dll thirdparty/download/ICSharpCode.SharpZipLib.dll thirdparty/download/FuzzyLogicLibrary.dll thirdparty/download/MaxMind.Db.dll thirdparty/download/Eluant.dll thirdparty/download/SmarIrc4net.dll +COMMON_LIBS = System.dll System.Core.dll System.Data.dll System.Data.DataSetExtensions.dll System.Drawing.dll System.Xml.dll thirdparty/download/ICSharpCode.SharpZipLib.dll thirdparty/download/FuzzyLogicLibrary.dll thirdparty/download/MaxMind.Db.dll thirdparty/download/Eluant.dll thirdparty/download/SmarIrc4net.dll thirdparty/download/rix0rrr.BeaconLib.dll NUNIT_LIBS_PATH := NUNIT_LIBS := $(NUNIT_LIBS_PATH)nunit.framework.dll @@ -358,6 +358,7 @@ install-core: @$(INSTALL_PROGRAM) Open.Nat.dll "$(DATA_INSTALL_DIR)" @$(INSTALL_PROGRAM) MaxMind.Db.dll "$(DATA_INSTALL_DIR)" @$(INSTALL_PROGRAM) SmarIrc4net.dll "$(DATA_INSTALL_DIR)" + @$(INSTALL_PROGRAM) rix0rrr.BeaconLib.dll "$(DATA_INSTALL_DIR)" @$(CP) *.sh "$(DATA_INSTALL_DIR)" install-linux-icons: diff --git a/OpenRA.Game/Platform.cs b/OpenRA.Game/Platform.cs index ade26aaaa9..4925d01a6f 100644 --- a/OpenRA.Game/Platform.cs +++ b/OpenRA.Game/Platform.cs @@ -22,6 +22,7 @@ namespace OpenRA public static class Platform { public static PlatformType CurrentPlatform { get { return currentPlatform.Value; } } + public static readonly Guid SessionGUID = Guid.NewGuid(); static Lazy currentPlatform = Exts.Lazy(GetCurrentPlatform); diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj index 8d9bb7bc9c..783258ad0a 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -41,6 +41,10 @@ ..\thirdparty\download\FuzzyLogicLibrary.dll False + + False + ..\thirdparty\download\rix0rrr.BeaconLib.dll + diff --git a/OpenRA.Mods.Common/ServerTraits/MasterServerPinger.cs b/OpenRA.Mods.Common/ServerTraits/MasterServerPinger.cs index cb11758db8..d33d8e741c 100644 --- a/OpenRA.Mods.Common/ServerTraits/MasterServerPinger.cs +++ b/OpenRA.Mods.Common/ServerTraits/MasterServerPinger.cs @@ -14,44 +14,60 @@ using System.Collections.Generic; using System.Linq; using System.Net; using System.Text; +using BeaconLib; using OpenRA.Server; using S = OpenRA.Server.Server; namespace OpenRA.Mods.Common.Server { - public class MasterServerPinger : ServerTrait, ITick, INotifySyncLobbyInfo, IStartGame, IEndGame + public class MasterServerPinger : ServerTrait, ITick, INotifyServerStart, INotifySyncLobbyInfo, IStartGame, IEndGame { // 3 minutes. Server has a 5 minute TTL for games, so give ourselves a bit of leeway. const int MasterPingInterval = 60 * 3; + static readonly Beacon LanGameBeacon = new Beacon("OpenRALANGame", (ushort)new Random(DateTime.Now.Millisecond).Next(2048, 60000)); + public int TickTimeout { get { return MasterPingInterval * 10000; } } - public void Tick(S server) - { - if ((Game.RunTime - lastPing > MasterPingInterval * 1000) || isInitialPing) - PingMasterServer(server); - else - lock (masterServerMessages) - while (masterServerMessages.Count > 0) - server.SendMessage(masterServerMessages.Dequeue()); - } - - public void LobbyInfoSynced(S server) { PingMasterServer(server); } - public void GameStarted(S server) { PingMasterServer(server); } - public void GameEnded(S server) { PingMasterServer(server); } - long lastPing = 0; bool isInitialPing = true; volatile bool isBusy; Queue masterServerMessages = new Queue(); - public void PingMasterServer(S server) + public void Tick(S server) { - if (isBusy || !server.Settings.AdvertiseOnline) return; + if ((Game.RunTime - lastPing > MasterPingInterval * 1000) || isInitialPing) + PublishGame(server); + else + lock (masterServerMessages) + while (masterServerMessages.Count > 0) + server.SendMessage(masterServerMessages.Dequeue()); + } - lastPing = Game.RunTime; - isBusy = true; + public void ServerStarted(S server) + { + if (!server.Ip.Equals(IPAddress.Loopback)) + LanGameBeacon.Start(); + } + public void LobbyInfoSynced(S server) + { + PublishGame(server); + } + + public void GameStarted(S server) + { + PublishGame(server); + } + + public void GameEnded(S server) + { + LanGameBeacon.Stop(); + PublishGame(server); + } + + void PublishGame(S server) + { var mod = server.ModData.Manifest; // important to grab these on the main server thread, not in the worker we're about to spawn -- they may be modified @@ -60,9 +76,21 @@ namespace OpenRA.Mods.Common.Server var numBots = server.LobbyInfo.Clients.Where(c1 => c1.Bot != null).Count(); var numSpectators = server.LobbyInfo.Clients.Where(c1 => c1.Bot == null && c1.Slot == null).Count(); var numSlots = server.LobbyInfo.Slots.Where(s => !s.Value.Closed).Count() - numBots; - var passwordProtected = string.IsNullOrEmpty(server.Settings.Password) ? 0 : 1; + var passwordProtected = !string.IsNullOrEmpty(server.Settings.Password); var clients = server.LobbyInfo.Clients.Where(c1 => c1.Bot == null).Select(c => Convert.ToBase64String(Encoding.UTF8.GetBytes(c.Name))).ToArray(); + UpdateMasterServer(server, numPlayers, numSlots, numBots, numSpectators, mod, passwordProtected, clients); + UpdateLANGameBeacon(server, numPlayers, numSlots, numBots, numSpectators, mod, passwordProtected); + } + + void UpdateMasterServer(S server, int numPlayers, int numSlots, int numBots, int numSpectators, Manifest mod, bool passwordProtected, string[] clients) + { + if (isBusy || !server.Settings.AdvertiseOnline) + return; + + lastPing = Game.RunTime; + isBusy = true; + Action a = () => { try @@ -84,7 +112,7 @@ namespace OpenRA.Mods.Common.Server server.LobbyInfo.GlobalSettings.Map, numSlots, numSpectators, - passwordProtected, + passwordProtected ? 1 : 0, string.Join(",", clients))); if (isInitialPing) @@ -116,5 +144,28 @@ namespace OpenRA.Mods.Common.Server a.BeginInvoke(null, null); } + + void UpdateLANGameBeacon(S server, int numPlayers, int numSlots, int numBots, int numSpectators, Manifest mod, bool passwordProtected) + { + var settings = server.Settings; + + // TODO: Serialize and send client names + var lanGameYaml = +@"Game: + Id: {0} + Name: {1} + Address: {2}:{3} + State: {4} + Players: {5} + MaxPlayers: {6} + Bots: {7} + Spectators: {8} + Map: {9} + Mods: {10}@{11} + Protected: {12}".F(Platform.SessionGUID, settings.Name, server.Ip, settings.ListenPort, (int)server.State, numPlayers, numSlots, numBots, numSpectators, + server.Map.Uid, mod.Id, mod.Metadata.Version, passwordProtected); + + LanGameBeacon.BeaconData = lanGameYaml; + } } } diff --git a/OpenRA.Mods.Common/Widgets/Logic/MultiplayerLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/MultiplayerLogic.cs index d78a241b85..3c80ca0f69 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/MultiplayerLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/MultiplayerLogic.cs @@ -10,12 +10,12 @@ #endregion using System; -using System.Collections; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Net; using System.Text; +using BeaconLib; using OpenRA.Network; using OpenRA.Server; using OpenRA.Widgets; @@ -39,6 +39,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic readonly Color incompatibleGameColor; readonly ModData modData; readonly WebServices services; + readonly Probe lanGameProbe; GameServer currentServer; MapPreview currentMap; @@ -53,6 +54,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic SearchStatus searchStatus = SearchStatus.Fetching; Download currentQuery; Widget serverList; + IEnumerable lanGameLocations; public string ProgressLabelText() { @@ -109,6 +111,11 @@ namespace OpenRA.Mods.Common.Widgets.Logic widget.Get("BACK_BUTTON").OnClick = () => { Ui.CloseWindow(); onExit(); }; Game.LoadWidget(null, "GLOBALCHAT_PANEL", widget.Get("GLOBALCHAT_ROOT"), new WidgetArgs()); + lanGameLocations = new List(); + lanGameProbe = new Probe("OpenRALANGame"); + lanGameProbe.BeaconsUpdated += locations => lanGameLocations = locations; + lanGameProbe.Start(); + RefreshServerList(); if (directConnectHost != null) @@ -322,7 +329,29 @@ namespace OpenRA.Mods.Common.Widgets.Logic } } - Game.RunAfterTick(() => RefreshServerListInner(games)); + var lanGames = new List(); + foreach (var bl in lanGameLocations) + { + var game = MiniYaml.FromString(bl.Data)[0].Value; + var idNode = game.Nodes.FirstOrDefault(n => n.Key == "Id"); + + // Skip beacons created by this instance and replace Id by expected int value + if (idNode != null && idNode.Value.Value != Platform.SessionGUID.ToString()) + { + idNode.Value.Value = "-1"; + + // Rewrite the server address with the correct IP + var addressNode = game.Nodes.FirstOrDefault(n => n.Key == "Address"); + if (addressNode != null) + addressNode.Value.Value = bl.Address.ToString().Split(':')[0] + ":" + addressNode.Value.Value.Split(':')[1]; + + lanGames.Add(new GameServer(game)); + } + } + + lanGames = lanGames.GroupBy(gs => gs.Address).Select(g => g.Last()).ToList(); + + Game.RunAfterTick(() => RefreshServerListInner(games.Concat(lanGames).ToList())); }; var queryURL = services.ServerList + "games?version={0}&mod={1}&modversion={2}".F( @@ -444,7 +473,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic if (location != null) { var font = Game.Renderer.Fonts[location.Font]; - var cachedServerLocation = GeoIP.LookupCountry(game.Address.Split(':')[0]); + var cachedServerLocation = game.Id != -1 ? GeoIP.LookupCountry(game.Address.Split(':')[0]) : "Local Network"; var label = WidgetUtils.TruncateText(cachedServerLocation, location.Bounds.Width, font); location.GetText = () => label; location.GetColor = () => canJoin ? location.TextColor : incompatibleGameColor; diff --git a/mods/cnc/chrome/multiplayer-browser.yaml b/mods/cnc/chrome/multiplayer-browser.yaml index 35ea13d13f..25fd6cffd6 100644 --- a/mods/cnc/chrome/multiplayer-browser.yaml +++ b/mods/cnc/chrome/multiplayer-browser.yaml @@ -24,7 +24,7 @@ Container@MULTIPLAYER_BROWSER_PANEL: X: 380 Width: 110 Height: 25 - Text: Country + Text: Location Font: Bold Label@STATUS: X: 495 diff --git a/mods/common/chrome/multiplayer-browser.yaml b/mods/common/chrome/multiplayer-browser.yaml index 64dca7b2cd..2e228cb1a0 100644 --- a/mods/common/chrome/multiplayer-browser.yaml +++ b/mods/common/chrome/multiplayer-browser.yaml @@ -24,7 +24,7 @@ Container@MULTIPLAYER_BROWSER_PANEL: X: 380 Width: 110 Height: 25 - Text: Country + Text: Location Font: Bold Label@STATUS: X: 495 diff --git a/packaging/windows/OpenRA.nsi b/packaging/windows/OpenRA.nsi index e1ec032e7c..91442e27ae 100644 --- a/packaging/windows/OpenRA.nsi +++ b/packaging/windows/OpenRA.nsi @@ -122,6 +122,7 @@ Section "Game" GAME File "${SRCDIR}\GeoLite2-Country.mmdb.gz" File "${SRCDIR}\eluant.dll" File "${SRCDIR}\SmarIrc4net.dll" + File "${SRCDIR}\rix0rrr.BeaconLib.dll" File "${DEPSDIR}\soft_oal.dll" File "${DEPSDIR}\SDL2.dll" File "${DEPSDIR}\freetype6.dll" @@ -235,6 +236,7 @@ Function ${UN}Clean Delete $INSTDIR\SDL2-CS.dll Delete $INSTDIR\OpenAL-CS.dll Delete $INSTDIR\SmarIrc4net.dll + Delete $INSTDIR\rix0rrr.BeaconLib.dll RMDir /r $INSTDIR\Support DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\OpenRA${SUFFIX}" diff --git a/thirdparty/fetch-thirdparty-deps.ps1 b/thirdparty/fetch-thirdparty-deps.ps1 index d0275dbd0c..6a11e9e407 100644 --- a/thirdparty/fetch-thirdparty-deps.ps1 +++ b/thirdparty/fetch-thirdparty-deps.ps1 @@ -158,4 +158,12 @@ if (!(Test-Path "SmarIrc4net.dll")) rmdir SmartIrc4net -Recurse } +if (!(Test-Path "rix0rrr.BeaconLib.dll")) +{ + echo "Fetching rix0rrr.BeaconLib from NuGet." + ./nuget.exe install rix0rrr.BeaconLib -Version 1.0.0 -ExcludeVersion -Verbosity quiet + cp rix0rrr.BeaconLib/lib/net40/rix0rrr.BeaconLib.dll . + rmdir rix0rrr.BeaconLib -Recurse +} + cd .. diff --git a/thirdparty/fetch-thirdparty-deps.sh b/thirdparty/fetch-thirdparty-deps.sh index 7f96cf16bb..7f72f04750 100755 --- a/thirdparty/fetch-thirdparty-deps.sh +++ b/thirdparty/fetch-thirdparty-deps.sh @@ -115,3 +115,10 @@ if [ ! -f SmarIrc4net.dll ]; then cp ./SmartIrc4net/lib/net40/SmarIrc4net* . rm -rf SmartIrc4net fi + +if [ ! -f rix0rrr.BeaconLib.dll ]; then + echo "Fetching rix0rrr.BeaconLib from NuGet." + get rix0rrr.BeaconLib 1.0.0 + cp ./rix0rrr.BeaconLib/lib/net40/rix0rrr.BeaconLib.dll . + rm -rf rix0rrr.BeaconLib +fi \ No newline at end of file