diff --git a/Makefile b/Makefile index 5bb87d9fdb..86ec6c557b 100644 --- a/Makefile +++ b/Makefile @@ -439,10 +439,11 @@ install-man-page: man-page install-linux-scripts: @echo "#!/bin/sh" > openra @echo 'cd "$(gameinstalldir)"' >> openra +# Note: this relies on the non-standard -f flag implemented by gnu readlink ifeq ($(DEBUG), $(filter $(DEBUG),false no n off 0)) - @echo 'mono OpenRA.Game.exe "$$@"' >> openra + @echo 'mono OpenRA.Game.exe Engine.LaunchPath="$(readlink -f $0)" "$$@"' >> openra else - @echo 'mono --debug OpenRA.Game.exe "$$@"' >> openra + @echo 'mono --debug OpenRA.Game.exe Engine.LaunchPath="$(readlink -f $0)" "$$@"' >> openra endif @echo 'if [ $$? != 0 -a $$? != 1 ]' >> openra @echo 'then' >> openra diff --git a/OpenRA.Game/ExternalMods.cs b/OpenRA.Game/ExternalMods.cs new file mode 100644 index 0000000000..b95df2b5ff --- /dev/null +++ b/OpenRA.Game/ExternalMods.cs @@ -0,0 +1,125 @@ +#region Copyright & License Information +/* + * Copyright 2007-2017 The OpenRA Developers (see AUTHORS) + * 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; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using OpenRA.Graphics; + +namespace OpenRA +{ + public class ExternalMod + { + public readonly string Id; + public readonly string Version; + public readonly string Title; + public readonly string LaunchPath; + public readonly string[] LaunchArgs; + public Sprite Icon { get; internal set; } + + public static string MakeKey(Manifest mod) { return MakeKey(mod.Id, mod.Metadata.Version); } + public static string MakeKey(ExternalMod mod) { return MakeKey(mod.Id, mod.Version); } + public static string MakeKey(string modId, string modVersion) { return modId + "-" + modVersion; } + } + + public class ExternalMods : IReadOnlyDictionary + { + readonly Dictionary mods; + readonly SheetBuilder sheetBuilder; + readonly string launchPath; + + public ExternalMods(string launchPath) + { + // Process.Start requires paths to not be quoted, even if they contain spaces + if (launchPath.First() == '"' && launchPath.Last() == '"') + launchPath = launchPath.Substring(1, launchPath.Length - 2); + + this.launchPath = launchPath; + sheetBuilder = new SheetBuilder(SheetType.BGRA, 256); + mods = LoadMods(); + } + + Dictionary LoadMods() + { + var ret = new Dictionary(); + var supportPath = Platform.ResolvePath(Path.Combine("^", "ModMetadata")); + if (!Directory.Exists(supportPath)) + return ret; + + foreach (var path in Directory.GetFiles(supportPath, "*.yaml")) + { + try + { + var yaml = MiniYaml.FromStream(File.OpenRead(path), path).First().Value; + var mod = FieldLoader.Load(yaml); + var iconNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Icon"); + if (iconNode != null && !string.IsNullOrEmpty(iconNode.Value.Value)) + { + using (var stream = new MemoryStream(Convert.FromBase64String(iconNode.Value.Value))) + using (var bitmap = new Bitmap(stream)) + mod.Icon = sheetBuilder.Add(bitmap); + } + + ret.Add(ExternalMod.MakeKey(mod), mod); + } + catch (Exception e) + { + Log.Write("debug", "Failed to parse mod metadata file '{0}'", path); + Log.Write("debug", e.ToString()); + } + } + + return ret; + } + + internal void Register(Manifest mod) + { + if (mod.Metadata.Hidden) + return; + + var iconData = ""; + using (var stream = mod.Package.GetStream("icon.png")) + if (stream != null) + iconData = Convert.ToBase64String(stream.ReadAllBytes()); + + var key = ExternalMod.MakeKey(mod); + var yaml = new List() + { + new MiniYamlNode("Registration", new MiniYaml("", new List() + { + new MiniYamlNode("Id", mod.Id), + new MiniYamlNode("Version", mod.Metadata.Version), + new MiniYamlNode("Title", mod.Metadata.Title), + new MiniYamlNode("Icon", iconData), + new MiniYamlNode("LaunchPath", launchPath), + new MiniYamlNode("LaunchArgs", "Game.Mod=" + mod.Id) + })) + }; + + var supportPath = Platform.ResolvePath(Path.Combine("^", "ModMetadata")); + Directory.CreateDirectory(supportPath); + + File.WriteAllLines(Path.Combine(supportPath, key + ".yaml"), yaml.ToLines(false).ToArray()); + } + + public ExternalMod this[string key] { get { return mods[key]; } } + public int Count { get { return mods.Count; } } + public ICollection Keys { get { return mods.Keys; } } + public ICollection Values { get { return mods.Values; } } + public bool ContainsKey(string key) { return mods.ContainsKey(key); } + public IEnumerator> GetEnumerator() { return mods.GetEnumerator(); } + public bool TryGetValue(string key, out ExternalMod value) { return mods.TryGetValue(key, out value); } + IEnumerator IEnumerable.GetEnumerator() { return mods.GetEnumerator(); } + } +} diff --git a/OpenRA.Game/Game.cs b/OpenRA.Game/Game.cs index 6a764b488c..50bb78bab7 100644 --- a/OpenRA.Game/Game.cs +++ b/OpenRA.Game/Game.cs @@ -37,6 +37,7 @@ namespace OpenRA public const int TimestepJankThreshold = 250; // Don't catch up for delays larger than 250ms public static InstalledMods Mods { get; private set; } + public static ExternalMods ExternalMods { get; private set; } public static ModData ModData; public static Settings Settings; @@ -314,10 +315,16 @@ namespace OpenRA GlobalChat = new GlobalChat(); Mods = new InstalledMods(customModPath); - Console.WriteLine("Available mods:"); + Console.WriteLine("Internal mods:"); foreach (var mod in Mods) Console.WriteLine("\t{0}: {1} ({2})", mod.Key, mod.Value.Metadata.Title, mod.Value.Metadata.Version); + var launchPath = args.GetValue("Engine.LaunchPath", Assembly.GetEntryAssembly().Location); + ExternalMods = new ExternalMods(launchPath); + Console.WriteLine("External mods:"); + foreach (var mod in ExternalMods) + Console.WriteLine("\t{0}: {1} ({2})", mod.Key, mod.Value.Title, mod.Value.Version); + InitializeMod(Settings.Game.Mod, args); } @@ -370,6 +377,7 @@ namespace OpenRA Sound.StopVideo(); ModData = new ModData(Mods[mod], Mods, true); + ExternalMods.Register(ModData.Manifest); using (new PerfTimer("LoadMaps")) ModData.MapCache.LoadMaps(); @@ -457,6 +465,28 @@ namespace OpenRA return shellmaps.Random(CosmeticRandom); } + public static void SwitchToExternalMod(ExternalMod mod, string[] launchArguments = null, Action onFailed = null) + { + try + { + var argsString = mod.LaunchArgs.Append(launchArguments) + .Select(a => "\"" + a + "\"").JoinWith(" "); + + var p = Process.Start(mod.LaunchPath, argsString); + if (p == null || p.HasExited) + onFailed(); + else + { + p.Close(); + Exit(); + } + } + catch + { + onFailed(); + } + } + static RunStatus state = RunStatus.Running; public static event Action OnQuit = () => { }; diff --git a/OpenRA.Game/InstalledMods.cs b/OpenRA.Game/InstalledMods.cs index 92d7e5a8a6..2d191b6509 100644 --- a/OpenRA.Game/InstalledMods.cs +++ b/OpenRA.Game/InstalledMods.cs @@ -12,9 +12,11 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Drawing; using System.IO; using System.Linq; using OpenRA.FileSystem; +using OpenRA.Graphics; using OpenRA.Primitives; namespace OpenRA @@ -22,9 +24,15 @@ namespace OpenRA public class InstalledMods : IReadOnlyDictionary { readonly Dictionary mods; + readonly SheetBuilder sheetBuilder; + + readonly Dictionary icons = new Dictionary(); + public readonly IReadOnlyDictionary Icons; public InstalledMods(string customModPath) { + sheetBuilder = new SheetBuilder(SheetType.BGRA, 256); + Icons = new ReadOnlyDictionary(icons); mods = GetInstalledMods(customModPath); } @@ -53,7 +61,7 @@ namespace OpenRA return mods; } - static Manifest LoadMod(string id, string path) + Manifest LoadMod(string id, string path) { IReadOnlyPackage package = null; try @@ -76,6 +84,11 @@ namespace OpenRA if (!package.Contains("mod.yaml")) throw new InvalidDataException(path + " is not a valid mod package"); + using (var stream = package.GetStream("icon.png")) + if (stream != null) + using (var bitmap = new Bitmap(stream)) + icons[id] = sheetBuilder.Add(bitmap); + // Mods in the support directory and oramod packages (which are listed later // in the CandidateMods list) override mods in the main install. return new Manifest(id, package); @@ -89,7 +102,7 @@ namespace OpenRA } } - static Dictionary GetInstalledMods(string customModPath) + Dictionary GetInstalledMods(string customModPath) { var ret = new Dictionary(); var candidates = GetCandidateMods(); diff --git a/OpenRA.Game/Network/GameServer.cs b/OpenRA.Game/Network/GameServer.cs index 62ffb58264..0ef4ac4c3a 100644 --- a/OpenRA.Game/Network/GameServer.cs +++ b/OpenRA.Game/Network/GameServer.cs @@ -39,16 +39,29 @@ namespace OpenRA.Network FieldLoader.Load(this, yaml); Manifest mod; + ExternalMod external; var modVersion = Mods.Split('@'); - if (modVersion.Length == 2 && Game.Mods.TryGetValue(modVersion[0], out mod)) + + ModLabel = "Unknown mod: {0}".F(Mods); + if (modVersion.Length == 2) { ModId = modVersion[0]; ModVersion = modVersion[1]; - ModLabel = "{0} ({1})".F(mod.Metadata.Title, modVersion[1]); - IsCompatible = Game.Settings.Debug.IgnoreVersionMismatch || ModVersion == mod.Metadata.Version; + + if (Game.Mods.TryGetValue(modVersion[0], out mod)) + { + ModLabel = "{0} ({1})".F(mod.Metadata.Title, modVersion[1]); + IsCompatible = Game.Settings.Debug.IgnoreVersionMismatch || ModVersion == mod.Metadata.Version; + } + + var externalKey = ExternalMod.MakeKey(modVersion[0], modVersion[1]); + if (!IsCompatible && Game.ExternalMods.TryGetValue(externalKey, out external) + && external.Version == modVersion[1]) + { + ModLabel = "{0} ({1})".F(external.Title, external.Version); + IsCompatible = true; + } } - else - ModLabel = "Unknown mod: {0}".F(Mods); var mapAvailable = Game.Settings.Game.AllowDownloading || Game.ModData.MapCache[Map].Status == MapStatus.Available; IsJoinable = IsCompatible && State == 1 && mapAvailable; diff --git a/OpenRA.Game/Network/OrderManager.cs b/OpenRA.Game/Network/OrderManager.cs index 246a221673..3028a70e50 100644 --- a/OpenRA.Game/Network/OrderManager.cs +++ b/OpenRA.Game/Network/OrderManager.cs @@ -34,6 +34,7 @@ namespace OpenRA.Network public string ServerError = "Server is not responding"; public bool AuthenticationFailed = false; + public ExternalMod ServerExternalMod = null; public int NetFrameNumber { get; private set; } public int LocalFrameNumber; diff --git a/OpenRA.Game/Network/UnitOrders.cs b/OpenRA.Game/Network/UnitOrders.cs index 4e2bbc9f4c..e3a2972292 100644 --- a/OpenRA.Game/Network/UnitOrders.cs +++ b/OpenRA.Game/Network/UnitOrders.cs @@ -135,19 +135,14 @@ namespace OpenRA.Network var mod = Game.ModData.Manifest; var request = HandshakeRequest.Deserialize(order.TargetString); - Manifest serverMod; - if (request.Mod != mod.Id && - Game.Mods.TryGetValue(request.Mod, out serverMod) && - serverMod.Metadata.Version == request.Version) + var externalKey = ExternalMod.MakeKey(request.Mod, request.Version); + ExternalMod external; + if ((request.Mod != mod.Id || request.Version != mod.Metadata.Version) + && Game.ExternalMods.TryGetValue(externalKey, out external)) { - var replay = orderManager.Connection as ReplayConnection; - var launchCommand = replay != null ? - "Launch.Replay=" + replay.Filename : - "Launch.Connect=" + orderManager.Host + ":" + orderManager.Port; - - Game.ModData.LoadScreen.Display(); - Game.InitializeMod(request.Mod, new Arguments(launchCommand)); - + // The ConnectionFailedLogic will prompt the user to switch mods + orderManager.ServerExternalMod = external; + orderManager.Connection.Dispose(); break; } @@ -187,6 +182,7 @@ namespace OpenRA.Network case "AuthenticationError": { + // The ConnectionFailedLogic will prompt the user for the password orderManager.ServerError = order.TargetString; orderManager.AuthenticationFailed = true; break; diff --git a/OpenRA.Game/OpenRA.Game.csproj b/OpenRA.Game/OpenRA.Game.csproj index 0bf91e7d6f..54e5e3518d 100644 --- a/OpenRA.Game/OpenRA.Game.csproj +++ b/OpenRA.Game/OpenRA.Game.csproj @@ -242,6 +242,7 @@ + diff --git a/OpenRA.GameMonitor/GameMonitor.cs b/OpenRA.GameMonitor/GameMonitor.cs index 3b185e9334..c730795408 100644 --- a/OpenRA.GameMonitor/GameMonitor.cs +++ b/OpenRA.GameMonitor/GameMonitor.cs @@ -27,12 +27,17 @@ namespace OpenRA [STAThread] static void Main(string[] args) { - var executableDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - var processName = Path.Combine(executableDirectory, "OpenRA.Game.exe"); + var launcherPath = Assembly.GetExecutingAssembly().Location; + var directory = Path.GetDirectoryName(launcherPath); + var enginePath = Path.Combine(directory, "OpenRA.Game.exe"); - Directory.SetCurrentDirectory(executableDirectory); + Directory.SetCurrentDirectory(directory); - var psi = new ProcessStartInfo(processName, string.Join(" ", args.Select(arg => "\"" + arg + "\""))); + var engineArgs = args + .Append("Engine.LaunchPath=" + launcherPath) + .Select(arg => "\"" + arg + "\""); + + var psi = new ProcessStartInfo(enginePath, string.Join(" ", engineArgs)); try { diff --git a/OpenRA.Mods.Common/Widgets/Logic/ConnectionLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/ConnectionLogic.cs index 096ab6eb7b..a37505dad9 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/ConnectionLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/ConnectionLogic.cs @@ -10,6 +10,7 @@ #endregion using System; +using OpenRA.Graphics; using OpenRA.Network; using OpenRA.Widgets; @@ -30,7 +31,9 @@ namespace OpenRA.Mods.Common.Widgets.Logic else if (om.Connection.ConnectionState == ConnectionState.NotConnected) { CloseWindow(); - Ui.OpenWindow("CONNECTIONFAILED_PANEL", new WidgetArgs() + + var switchPanel = om.ServerExternalMod != null ? "CONNECTION_SWITCHMOD_PANEL" : "CONNECTIONFAILED_PANEL"; + Ui.OpenWindow(switchPanel, new WidgetArgs() { { "orderManager", om }, { "onAbort", onAbort }, @@ -146,4 +149,89 @@ namespace OpenRA.Mods.Common.Widgets.Logic } } } -} \ No newline at end of file + + public class ConnectionSwitchModLogic : ChromeLogic + { + [ObjectCreator.UseCtor] + public ConnectionSwitchModLogic(Widget widget, OrderManager orderManager, Action onAbort, Action onRetry) + { + var panel = widget; + var abortButton = panel.Get("ABORT_BUTTON"); + var switchButton = panel.Get("SWITCH_BUTTON"); + + var modTitle = orderManager.ServerExternalMod.Title; + var modVersion = orderManager.ServerExternalMod.Version; + var modIcon = orderManager.ServerExternalMod.Icon; + + switchButton.OnClick = () => + { + var launchCommand = "Launch.Connect=" + orderManager.Host + ":" + orderManager.Port; + Game.SwitchToExternalMod(orderManager.ServerExternalMod, new[] { launchCommand }, () => + { + orderManager.ServerError = "Failed to switch mod."; + Ui.CloseWindow(); + Ui.OpenWindow("CONNECTIONFAILED_PANEL", new WidgetArgs() + { + { "orderManager", orderManager }, + { "onAbort", onAbort }, + { "onRetry", onRetry } + }); + }); + }; + + abortButton.Visible = onAbort != null; + abortButton.OnClick = () => { Ui.CloseWindow(); onAbort(); }; + + var width = 0; + var title = panel.GetOrNull("MOD_TITLE"); + if (title != null) + { + var font = Game.Renderer.Fonts[title.Font]; + var label = WidgetUtils.TruncateText(modTitle, title.Bounds.Width, font); + var labelWidth = font.Measure(label).X; + width = Math.Max(width, title.Bounds.X + labelWidth); + title.Bounds.Width = labelWidth; + title.GetText = () => label; + } + + var version = panel.GetOrNull("MOD_VERSION"); + if (version != null) + { + var font = Game.Renderer.Fonts[version.Font]; + var label = WidgetUtils.TruncateText(modVersion, version.Bounds.Width, font); + var labelWidth = font.Measure(label).X; + width = Math.Max(width, version.Bounds.X + labelWidth); + version.Bounds.Width = labelWidth; + version.GetText = () => label; + } + + var logo = panel.GetOrNull("MOD_ICON"); + if (logo != null && modIcon != null) + logo.GetSprite = () => modIcon; + + if (logo != null && modIcon == null) + { + // Hide the logo and center just the text + if (title != null) + title.Bounds.Offset(logo.Bounds.Left - title.Bounds.Left, 0); + + if (version != null) + version.Bounds.Offset(logo.Bounds.Left - version.Bounds.Left, 0); + + width -= logo.Bounds.Width; + } + else + { + // Add an equal logo margin on the right of the text + width += logo.Bounds.Width; + } + + var container = panel.GetOrNull("MOD_CONTAINER"); + if (container != null) + { + container.Bounds.Offset((container.Bounds.Width - width) / 2, 0); + container.Bounds.Width = width; + } + } + } +} diff --git a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs index 65978de228..b9500e8aac 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs @@ -91,7 +91,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic Action onRetry = password => ConnectionLogic.Connect(om.Host, om.Port, password, onConnect, onExit); - Ui.OpenWindow("CONNECTIONFAILED_PANEL", new WidgetArgs() + var switchPanel = om.ServerExternalMod != null ? "CONNECTION_SWITCHMOD_PANEL" : "CONNECTIONFAILED_PANEL"; + Ui.OpenWindow(switchPanel, new WidgetArgs() { { "orderManager", om }, { "onAbort", onExit }, diff --git a/launch-game.sh b/launch-game.sh index 72838f4d3f..80ffa4af69 100755 --- a/launch-game.sh +++ b/launch-game.sh @@ -1,6 +1,6 @@ #!/bin/sh -# launch script (executed by Desura) -mono OpenRA.Game.exe "$@" +# Note: this relies on the non-standard -f flag implemented by gnu readlink +mono OpenRA.Game.exe Engine.LaunchPath="$(readlink -f $0)" "$@" if [ $? != 0 -a $? != 1 ] then ZENITY=`which zenity` || echo "OpenRA needs zenity installed to display a graphical error dialog. See ~/.openra. for log files." diff --git a/mods/cnc/chrome/connection.yaml b/mods/cnc/chrome/connection.yaml index 1ef3c5f73b..1527a3f0ae 100644 --- a/mods/cnc/chrome/connection.yaml +++ b/mods/cnc/chrome/connection.yaml @@ -90,3 +90,72 @@ Container@CONNECTIONFAILED_PANEL: Width: 140 Height: 35 Text: Retry + +Container@CONNECTION_SWITCHMOD_PANEL: + Logic: ConnectionSwitchModLogic + X: (WINDOW_RIGHT - WIDTH)/2 + Y: (WINDOW_BOTTOM - 90)/2 + Width: 370 + Height: 134 + Children: + Label@TITLE: + Width: PARENT_RIGHT + Y: 0-25 + Font: BigBold + Contrast: true + Align: Center + Text: Switch Mod + Background@CONNECTION_BACKGROUND: + Width: 370 + Height: 120 + Background: panel-black + Children: + Label@DESC: + Y: 15 + Width: PARENT_RIGHT + Height: 25 + Text: This server is running a different mod: + Font: Bold + Align: Center + Container@MOD_CONTAINER: + X: 0 + Y: 42 + Width: PARENT_RIGHT + Children: + RGBASprite@MOD_ICON: + Y: 4 + Width: 32 + Height: 32 + Label@MOD_TITLE: + X: 37 + Width: PARENT_RIGHT - 37 + Height: 25 + Font: Bold + Align: Left + Label@MOD_VERSION: + X: 37 + Y: 15 + Width: PARENT_RIGHT - 37 + Height: 25 + Font: Tiny + Align: Left + Label@DESC2: + Y: 80 + Width: PARENT_RIGHT + Height: 25 + Text: Switch mods and join server? + Font: Bold + Align: Center + Button@ABORT_BUTTON: + Key: escape + Y: 119 + Width: 140 + Height: 35 + Text: Abort + Button@SWITCH_BUTTON: + Key: return + X: 230 + Y: 119 + Width: 140 + Height: 35 + Text: Switch diff --git a/mods/cnc/icon.png b/mods/cnc/icon.png new file mode 100644 index 0000000000..22b05bd91f Binary files /dev/null and b/mods/cnc/icon.png differ diff --git a/mods/d2k/icon.png b/mods/d2k/icon.png new file mode 100644 index 0000000000..a4c3913dbe Binary files /dev/null and b/mods/d2k/icon.png differ diff --git a/mods/ra/chrome/connection.yaml b/mods/ra/chrome/connection.yaml index ddebd34792..7fa93e7cb1 100644 --- a/mods/ra/chrome/connection.yaml +++ b/mods/ra/chrome/connection.yaml @@ -88,3 +88,71 @@ Background@CONNECTING_PANEL: Text: Abort Font: Bold Key: escape + +Background@CONNECTION_SWITCHMOD_PANEL: + Logic: ConnectionSwitchModLogic + X: (WINDOW_RIGHT - WIDTH)/2 + Y: (WINDOW_BOTTOM - HEIGHT)/2 + Width: 450 + Height: 191 + Children: + Label@TITLE: + X: 0 + Y: 20 + Width: 450 + Height: 25 + Align: Center + Font: Bold + Text: Switch Mod + Label@DESC: + Y: 45 + Width: PARENT_RIGHT + Height: 25 + Text: This server is running a different mod: + Font: Bold + Align: Center + Container@MOD_CONTAINER: + X: 0 + Y: 72 + Width: PARENT_RIGHT + Children: + RGBASprite@MOD_ICON: + Y: 4 + Width: 32 + Height: 32 + Label@MOD_TITLE: + X: 37 + Width: PARENT_RIGHT - 37 + Height: 25 + Font: Bold + Align: Left + Label@MOD_VERSION: + X: 37 + Y: 15 + Width: PARENT_RIGHT - 37 + Height: 25 + Font: Tiny + Align: Left + Label@DESC2: + Y: 110 + Width: PARENT_RIGHT + Height: 25 + Text: Switch mods and join server? + Font: Bold + Align: Center + Button@SWITCH_BUTTON: + X: PARENT_RIGHT - 430 + Y: PARENT_BOTTOM - 45 + Width: 160 + Height: 25 + Text: Switch + Font: Bold + Key: return + Button@ABORT_BUTTON: + X: PARENT_RIGHT - 180 + Y: PARENT_BOTTOM - 45 + Width: 160 + Height: 25 + Text: Cancel + Font: Bold + Key: escape diff --git a/mods/ra/icon.png b/mods/ra/icon.png new file mode 100644 index 0000000000..220da01e3b Binary files /dev/null and b/mods/ra/icon.png differ diff --git a/mods/ts/icon.png b/mods/ts/icon.png new file mode 100644 index 0000000000..a82496dac3 Binary files /dev/null and b/mods/ts/icon.png differ diff --git a/packaging/osx/buildpackage.sh b/packaging/osx/buildpackage.sh index 64430e8b10..f93fd95e1f 100755 --- a/packaging/osx/buildpackage.sh +++ b/packaging/osx/buildpackage.sh @@ -1,7 +1,7 @@ #!/bin/bash # OpenRA packaging script for Mac OSX -LAUNCHER_TAG="osx-launcher-20161223" +LAUNCHER_TAG="osx-launcher-20170211" if [ $# -ne "3" ]; then echo "Usage: `basename $0` tag files-dir outputdir"