diff --git a/OpenRA.Game/Game.cs b/OpenRA.Game/Game.cs
index 848eaa2e4e..6d42f0ef8a 100644
--- a/OpenRA.Game/Game.cs
+++ b/OpenRA.Game/Game.cs
@@ -191,7 +191,7 @@ namespace OpenRA
public static void InitializeSettings(Arguments args)
{
- Settings = new Settings(Platform.ResolvePath("^", "settings.yaml"), args);
+ Settings = new Settings(Platform.ResolvePath(Path.Combine("^", "settings.yaml")), args);
}
internal static void Initialize(Arguments args)
diff --git a/OpenRA.Game/Manifest.cs b/OpenRA.Game/Manifest.cs
index ee962e4e82..8591017de5 100644
--- a/OpenRA.Game/Manifest.cs
+++ b/OpenRA.Game/Manifest.cs
@@ -29,7 +29,7 @@ namespace OpenRA
}
}
- // Describes what is to be loaded in order to run a mod
+ /// Describes what is to be loaded in order to run a mod.
public class Manifest
{
public static readonly Dictionary AllMods = LoadMods();
@@ -61,13 +61,16 @@ namespace OpenRA
readonly TypeDictionary modules = new TypeDictionary();
readonly Dictionary yaml;
- public Manifest(string mod)
+ public Manifest(string modId, string modPath = null)
{
- var path = Platform.ResolvePath(".", "mods", mod, "mod.yaml");
+ if (modPath == null)
+ modPath = ModMetadata.CandidateModPaths[modId];
+
+ var path = Path.Combine(modPath, "mod.yaml");
yaml = new MiniYaml(null, MiniYaml.FromFile(path)).ToDictionary();
Mod = FieldLoader.Load(yaml["Metadata"]);
- Mod.Id = mod;
+ Mod.Id = modId;
// TODO: Use fieldloader
Folders = YamlList(yaml, "Folders", true);
@@ -106,12 +109,10 @@ namespace OpenRA
RequiresMods = yaml["RequiresMods"].ToDictionary(my => my.Value);
// Allow inherited mods to import parent maps.
- var compat = new List();
- compat.Add(mod);
+ var compat = new List { Mod.Id };
if (yaml.ContainsKey("SupportsMapsFrom"))
- foreach (var c in yaml["SupportsMapsFrom"].Value.Split(','))
- compat.Add(c.Trim());
+ compat.AddRange(yaml["SupportsMapsFrom"].Value.Split(',').Select(c => c.Trim()));
MapCompatibility = compat.ToArray();
@@ -156,8 +157,10 @@ namespace OpenRA
if (!yaml.ContainsKey(key))
return new string[] { };
- var list = yaml[key].ToDictionary().Keys.ToArray();
- return parsePaths ? list.Select(Platform.ResolvePath).ToArray() : list;
+ if (parsePaths)
+ return yaml[key].Nodes.Select(node => Platform.ResolvePath(node.Key, node.Value.Value ?? string.Empty)).ToArray();
+
+ return yaml[key].ToDictionary().Keys.ToArray();
}
static IReadOnlyDictionary YamlDictionary(Dictionary yaml, string key, bool parsePaths = false)
@@ -168,15 +171,19 @@ namespace OpenRA
var inner = new Dictionary();
foreach (var node in yaml[key].Nodes)
{
+ var line = node.Key;
+ if (node.Value.Value != null)
+ line += ":" + node.Value.Value;
+
// '@' may be used in mod.yaml to indicate extra information (similar to trait @ tags).
// Applies to MapFolders (to indicate System and User directories) and Packages (to indicate package annotation).
- if (node.Key.Contains('@'))
+ if (line.Contains('@'))
{
- var split = node.Key.Split('@');
- inner.Add(split[0], split[1]);
+ var split = line.Split('@');
+ inner.Add(parsePaths ? Platform.ResolvePath(split[0]) : split[0], split[1]);
}
else
- inner.Add(node.Key, null);
+ inner.Add(line, null);
}
return new ReadOnlyDictionary(inner);
@@ -198,20 +205,16 @@ namespace OpenRA
static Dictionary LoadMods()
{
- var basePath = Platform.ResolvePath(".", "mods");
- var mods = Directory.GetDirectories(basePath)
- .Select(x => x.Substring(basePath.Length + 1));
-
var ret = new Dictionary();
- foreach (var mod in mods)
+ foreach (var mod in ModMetadata.CandidateModPaths)
{
- if (!File.Exists(Platform.ResolvePath(".", "mods", mod, "mod.yaml")))
+ if (!File.Exists(Path.Combine(mod.Value, "mod.yaml")))
continue;
try
{
- var manifest = new Manifest(mod);
- ret.Add(mod, manifest);
+ var manifest = new Manifest(mod.Key, mod.Value);
+ ret.Add(mod.Key, manifest);
}
catch (Exception ex)
{
diff --git a/OpenRA.Game/ModMetadata.cs b/OpenRA.Game/ModMetadata.cs
index f99f7bb5e8..2d24c3fc73 100644
--- a/OpenRA.Game/ModMetadata.cs
+++ b/OpenRA.Game/ModMetadata.cs
@@ -17,6 +17,7 @@ namespace OpenRA
{
public class ModMetadata
{
+ public static readonly Dictionary CandidateModPaths = GetCandidateMods();
public static readonly Dictionary AllMods = ValidateMods();
public string Id;
@@ -24,21 +25,19 @@ namespace OpenRA
public string Description;
public string Version;
public string Author;
+ public string LogoImagePath;
+ public string PreviewImagePath;
public bool Hidden;
public ContentInstaller Content;
static Dictionary ValidateMods()
{
- var basePath = Platform.ResolvePath(".", "mods");
- var mods = Directory.GetDirectories(basePath)
- .Select(x => x.Substring(basePath.Length + 1));
-
var ret = new Dictionary();
- foreach (var m in mods)
+ foreach (var pair in CandidateModPaths)
{
try
{
- var yamlPath = Platform.ResolvePath(".", "mods", m, "mod.yaml");
+ var yamlPath = Path.Combine(pair.Value, "mod.yaml");
if (!File.Exists(yamlPath))
continue;
@@ -47,22 +46,40 @@ namespace OpenRA
if (!nd.ContainsKey("Metadata"))
continue;
- var mod = FieldLoader.Load(nd["Metadata"]);
- mod.Id = m;
+ var metadata = FieldLoader.Load(nd["Metadata"]);
+ metadata.Id = pair.Key;
if (nd.ContainsKey("ContentInstaller"))
- mod.Content = FieldLoader.Load(nd["ContentInstaller"]);
+ metadata.Content = FieldLoader.Load(nd["ContentInstaller"]);
- ret.Add(m, mod);
+ ret.Add(pair.Key, metadata);
}
catch (Exception ex)
{
- Console.WriteLine("An exception occurred when trying to load ModMetadata for `{0}`:".F(m));
+ Console.WriteLine("An exception occurred when trying to load ModMetadata for `{0}`:".F(pair.Key));
Console.WriteLine(ex.Message);
}
}
return ret;
}
+
+ static Dictionary GetCandidateMods()
+ {
+ // Get mods that are in the game folder.
+ var basePath = Platform.ResolvePath(Path.Combine(".", "mods"));
+ var mods = Directory.GetDirectories(basePath)
+ .ToDictionary(x => x.Substring(basePath.Length + 1));
+
+ // Get mods that are in the support folder.
+ var supportPath = Platform.ResolvePath(Path.Combine("^", "mods"));
+ if (!Directory.Exists(supportPath))
+ return mods;
+
+ foreach (var pair in Directory.GetDirectories(supportPath).ToDictionary(x => x.Substring(supportPath.Length + 1)))
+ mods.Add(pair.Key, pair.Value);
+
+ return mods;
+ }
}
}
diff --git a/OpenRA.Game/Platform.cs b/OpenRA.Game/Platform.cs
index 62da9a8548..260684d20e 100644
--- a/OpenRA.Game/Platform.cs
+++ b/OpenRA.Game/Platform.cs
@@ -95,11 +95,19 @@ namespace OpenRA
public static string GameDir { get { return AppDomain.CurrentDomain.BaseDirectory; } }
- /// Replace special character prefixes with full paths
+ /// Replaces special character prefixes with full paths.
public static string ResolvePath(string path)
{
path = path.TrimEnd(new char[] { ' ', '\t' });
+ // If the path contains ':', chances are it is a package path.
+ // If it isn't, someone passed an already resolved path, which is wrong.
+ if (path.IndexOf(":", StringComparison.Ordinal) > 1)
+ {
+ var split = path.Split(':');
+ return ResolvePath(split[0], split[1]);
+ }
+
// paths starting with ^ are relative to the support dir
if (path.StartsWith("^"))
path = SupportDir + path.Substring(1);
@@ -111,7 +119,17 @@ namespace OpenRA
return path;
}
- /// Replace special character prefixes with full paths
+ /// Replaces package names with full paths. Avoid using this for non-package paths.
+ public static string ResolvePath(string package, string target)
+ {
+ // Resolve mod package paths.
+ if (ModMetadata.AllMods.ContainsKey(package))
+ package = ModMetadata.CandidateModPaths[package];
+
+ return ResolvePath(Path.Combine(package, target));
+ }
+
+ /// Replace special character prefixes with full paths.
public static string ResolvePath(params string[] path)
{
return ResolvePath(path.Aggregate(Path.Combine));
diff --git a/OpenRA.Game/Renderer.cs b/OpenRA.Game/Renderer.cs
index 114ddbd98b..6989ec3ca7 100644
--- a/OpenRA.Game/Renderer.cs
+++ b/OpenRA.Game/Renderer.cs
@@ -11,6 +11,7 @@
using System;
using System.Collections.Generic;
using System.Drawing;
+using System.IO;
using System.Linq;
using System.Reflection;
using OpenRA.Graphics;
@@ -49,7 +50,7 @@ namespace OpenRA
var resolution = GetResolution(graphicSettings);
var rendererName = serverSettings.Dedicated ? "Null" : graphicSettings.Renderer;
- var rendererPath = Platform.ResolvePath(".", "OpenRA.Platforms." + rendererName + ".dll");
+ var rendererPath = Platform.ResolvePath(Path.Combine(".", "OpenRA.Platforms." + rendererName + ".dll"));
Device = CreateDevice(Assembly.LoadFile(rendererPath), resolution.Width, resolution.Height, graphicSettings.Mode);
diff --git a/OpenRA.Game/Sound/Sound.cs b/OpenRA.Game/Sound/Sound.cs
index 7f49409871..c78e03b00b 100644
--- a/OpenRA.Game/Sound/Sound.cs
+++ b/OpenRA.Game/Sound/Sound.cs
@@ -33,7 +33,7 @@ namespace OpenRA
public Sound(string engineName)
{
- var enginePath = Platform.ResolvePath(".", "OpenRA.Platforms." + engineName + ".dll");
+ var enginePath = Platform.ResolvePath(Path.Combine(".", "OpenRA.Platforms." + engineName + ".dll"));
soundEngine = CreateDevice(Assembly.LoadFile(enginePath));
}
diff --git a/OpenRA.Mods.Common/Widgets/Logic/ModBrowserLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/ModBrowserLogic.cs
index bab41933bc..81ad20fc94 100644
--- a/OpenRA.Mods.Common/Widgets/Logic/ModBrowserLogic.cs
+++ b/OpenRA.Mods.Common/Widgets/Logic/ModBrowserLogic.cs
@@ -82,7 +82,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
{
try
{
- using (var preview = new Bitmap(Platform.ResolvePath(".", "mods", mod.Id, "preview.png")))
+ using (var preview = new Bitmap(Platform.ResolvePath(mod.PreviewImagePath)))
if (preview.Width == 296 && preview.Height == 196)
previews.Add(mod.Id, sheetBuilder.Add(preview));
}
@@ -90,7 +90,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
try
{
- using (var logo = new Bitmap(Platform.ResolvePath(".", "mods", mod.Id, "logo.png")))
+ using (var logo = new Bitmap(Platform.ResolvePath(mod.LogoImagePath)))
if (logo.Width == 96 && logo.Height == 96)
logos.Add(mod.Id, sheetBuilder.Add(logo));
}
diff --git a/mods/all/mod.yaml b/mods/all/mod.yaml
index de3152368c..da73d59a2e 100644
--- a/mods/all/mod.yaml
+++ b/mods/all/mod.yaml
@@ -17,7 +17,7 @@ Chrome:
Assemblies:
./mods/common/OpenRA.Mods.Common.dll
./mods/ra/OpenRA.Mods.RA.dll
- ./mods/d2k/OpenRA.Mods.D2k.dll
+ d2k:OpenRA.Mods.D2k.dll
./mods/cnc/OpenRA.Mods.Cnc.dll
./mods/ts/OpenRA.Mods.TS.dll
diff --git a/mods/cnc/mod.yaml b/mods/cnc/mod.yaml
index 17177eae48..ea251587f2 100644
--- a/mods/cnc/mod.yaml
+++ b/mods/cnc/mod.yaml
@@ -3,6 +3,8 @@ Metadata:
Description: Join the Global Defense Initiative or the Brotherhood of Nod in our\nrecreation of the classic game that started it all.\n\nTiberian Dawn modernizes the original Command & Conquer gameplay\nby introducing features from later games, including per-factory\nproduction queues, unit veterancy, and capturable tech structures.
Version: {DEV_VERSION}
Author: the OpenRA Developers
+ LogoImagePath: ./mods/cnc/logo.png
+ PreviewImagePath: ./mods/cnc/preview.png
RequiresMods:
modchooser: {DEV_VERSION}
diff --git a/mods/d2k/mod.yaml b/mods/d2k/mod.yaml
index b8171cf7f1..a94f39688c 100644
--- a/mods/d2k/mod.yaml
+++ b/mods/d2k/mod.yaml
@@ -3,96 +3,98 @@ Metadata:
Description: Three great houses fight for the precious spice, melange.\nHe who controls the spice controls the universe!\n\nTry to establish a foothold on the desert planet Arrakis\nwith its harsh environmental conditions and protect your\nharvesting operations from giant sandworms as well as\nruthless enemy factions.
Version: {DEV_VERSION}
Author: the OpenRA Developers
+ LogoImagePath: d2k:logo.png
+ PreviewImagePath: d2k:preview.png
RequiresMods:
modchooser: {DEV_VERSION}
Folders:
.
- ./mods/d2k
- ./mods/d2k/bits
- ./mods/d2k/bits/tex
- ./mods/d2k/bits/xmas
- ./mods/d2k/uibits
+ d2k:
+ d2k:bits
+ d2k:bits/tex
+ d2k:bits/xmas
+ d2k:uibits
~^Content/d2k
~^Content/d2k/GAMESFX
~^Content/d2k/Movies
~^Content/d2k/Music
MapFolders:
- ./mods/d2k/maps@System
+ d2k:maps@System
~^maps/d2k@User
Packages:
SOUND.RS
Rules:
- ./mods/d2k/rules/misc.yaml
- ./mods/d2k/rules/ai.yaml
- ./mods/d2k/rules/player.yaml
- ./mods/d2k/rules/world.yaml
- ./mods/d2k/rules/palettes.yaml
- ./mods/d2k/rules/defaults.yaml
- ./mods/d2k/rules/vehicles.yaml
- ./mods/d2k/rules/starport.yaml
- ./mods/d2k/rules/husks.yaml
- ./mods/d2k/rules/structures.yaml
- ./mods/d2k/rules/aircraft.yaml
- ./mods/d2k/rules/infantry.yaml
- ./mods/d2k/rules/arrakis.yaml
+ d2k:rules/misc.yaml
+ d2k:rules/ai.yaml
+ d2k:rules/player.yaml
+ d2k:rules/world.yaml
+ d2k:rules/palettes.yaml
+ d2k:rules/defaults.yaml
+ d2k:rules/vehicles.yaml
+ d2k:rules/starport.yaml
+ d2k:rules/husks.yaml
+ d2k:rules/structures.yaml
+ d2k:rules/aircraft.yaml
+ d2k:rules/infantry.yaml
+ d2k:rules/arrakis.yaml
Sequences:
- ./mods/d2k/sequences/aircraft.yaml
- ./mods/d2k/sequences/vehicles.yaml
- ./mods/d2k/sequences/infantry.yaml
- ./mods/d2k/sequences/structures.yaml
- ./mods/d2k/sequences/misc.yaml
+ d2k:sequences/aircraft.yaml
+ d2k:sequences/vehicles.yaml
+ d2k:sequences/infantry.yaml
+ d2k:sequences/structures.yaml
+ d2k:sequences/misc.yaml
TileSets:
- ./mods/d2k/tilesets/arrakis.yaml
+ d2k:tilesets/arrakis.yaml
MapGrid:
TileSize: 32,32
Type: Rectangular
Cursors:
- ./mods/d2k/cursors.yaml
+ d2k:cursors.yaml
Chrome:
- ./mods/d2k/chrome.yaml
+ d2k:chrome.yaml
Assemblies:
./mods/common/OpenRA.Mods.Common.dll
./mods/cnc/OpenRA.Mods.Cnc.dll
- ./mods/d2k/OpenRA.Mods.D2k.dll
+ d2k:OpenRA.Mods.D2k.dll
ChromeLayout:
- ./mods/d2k/chrome/ingame.yaml
+ d2k:chrome/ingame.yaml
./mods/ra/chrome/ingame-chat.yaml
./mods/ra/chrome/ingame-diplomacy.yaml
./mods/ra/chrome/ingame-fmvplayer.yaml
- ./mods/d2k/chrome/ingame-menu.yaml
+ d2k:chrome/ingame-menu.yaml
./mods/ra/chrome/ingame-info.yaml
./mods/ra/chrome/ingame-infoscripterror.yaml
./mods/ra/chrome/ingame-infobriefing.yaml
./mods/ra/chrome/ingame-infoobjectives.yaml
- ./mods/d2k/chrome/ingame-infostats.yaml
- ./mods/d2k/chrome/ingame-observer.yaml
+ d2k:chrome/ingame-infostats.yaml
+ d2k:chrome/ingame-observer.yaml
./mods/ra/chrome/ingame-observerstats.yaml
- ./mods/d2k/chrome/ingame-player.yaml
+ d2k:chrome/ingame-player.yaml
./mods/ra/chrome/ingame-perf.yaml
./mods/ra/chrome/ingame-debug.yaml
- ./mods/d2k/chrome/mainmenu.yaml
+ d2k:chrome/mainmenu.yaml
./mods/ra/chrome/settings.yaml
./mods/ra/chrome/credits.yaml
./mods/ra/chrome/lobby.yaml
./mods/ra/chrome/lobby-mappreview.yaml
- ./mods/d2k/chrome/lobby-players.yaml
- ./mods/d2k/chrome/lobby-options.yaml
+ d2k:chrome/lobby-players.yaml
+ d2k:chrome/lobby-options.yaml
./mods/ra/chrome/lobby-music.yaml
./mods/ra/chrome/lobby-kickdialogs.yaml
./mods/ra/chrome/lobby-globalchat.yaml
- ./mods/d2k/chrome/color-picker.yaml
+ d2k:chrome/color-picker.yaml
./mods/ra/chrome/map-chooser.yaml
./mods/ra/chrome/multiplayer.yaml
./mods/ra/chrome/multiplayer-browser.yaml
@@ -100,33 +102,33 @@ ChromeLayout:
./mods/ra/chrome/multiplayer-directconnect.yaml
./mods/ra/chrome/multiplayer-globalchat.yaml
./mods/ra/chrome/connection.yaml
- ./mods/d2k/chrome/dropdowns.yaml
+ d2k:chrome/dropdowns.yaml
./mods/ra/chrome/musicplayer.yaml
- ./mods/d2k/chrome/tooltips.yaml
+ d2k:chrome/tooltips.yaml
./mods/ra/chrome/assetbrowser.yaml
- ./mods/d2k/chrome/missionbrowser.yaml
+ d2k:chrome/missionbrowser.yaml
./mods/ra/chrome/confirmation-dialogs.yaml
./mods/ra/chrome/editor.yaml
./mods/ra/chrome/replaybrowser.yaml
Weapons:
- ./mods/d2k/weapons.yaml
- ./mods/d2k/weapons/debris.yaml
+ d2k:weapons.yaml
+ d2k:weapons/debris.yaml
Voices:
- ./mods/d2k/audio/voices.yaml
+ d2k:audio/voices.yaml
Notifications:
- ./mods/d2k/audio/notifications.yaml
+ d2k:audio/notifications.yaml
Music:
- ./mods/d2k/audio/music.yaml
+ d2k:audio/music.yaml
Translations:
- ./mods/d2k/languages/english.yaml
+ d2k:languages/english.yaml
LoadScreen: LogoStripeLoadScreen
- Image: ./mods/d2k/uibits/loadscreen.png
+ Image: d2k:uibits/loadscreen.png
Text: Filling Crates..., Breeding Sandworms..., Fuelling carryalls..., Deploying harvesters..., Preparing 'thopters..., Summoning mentats...
ContentInstaller:
@@ -161,7 +163,7 @@ LobbyDefaults:
TechLevel: Unrestricted
ChromeMetrics:
- ./mods/d2k/metrics.yaml
+ d2k:metrics.yaml
Fonts:
Regular:
@@ -171,7 +173,7 @@ Fonts:
Font:./mods/common/FreeSansBold.ttf
Size:14
Title:
- Font:./mods/d2k/Dune2k.ttf
+ Font:d2k:Dune2k.ttf
Size:32
MediumBold:
Font:./mods/common/FreeSansBold.ttf
@@ -190,7 +192,7 @@ Fonts:
Size:10
Missions:
- ./mods/d2k/missions.yaml
+ d2k:missions.yaml
SupportsMapsFrom: d2k
diff --git a/mods/ra/mod.yaml b/mods/ra/mod.yaml
index 4b188744e5..1d89a43aef 100644
--- a/mods/ra/mod.yaml
+++ b/mods/ra/mod.yaml
@@ -3,6 +3,8 @@ Metadata:
Description: In a world where Hitler was assassinated and the Third Reich never\nexisted, the Soviet Union seeks power over all of Europe. Allied\nagainst this Evil Empire, the free world faces a Cold War turned hot.\n\nRed Alert fuses the quick and fun gameplay of the original\nC&C: Red Alert, with balance improvements and new gameplay\nfeatures inspired by modern RTS games.
Version: {DEV_VERSION}
Author: the OpenRA Developers
+ LogoImagePath: ./mods/ra/logo.png
+ PreviewImagePath: ./mods/ra/preview.png
RequiresMods:
modchooser: {DEV_VERSION}
diff --git a/mods/ts/mod.yaml b/mods/ts/mod.yaml
index 8429f84ef7..0527483c93 100644
--- a/mods/ts/mod.yaml
+++ b/mods/ts/mod.yaml
@@ -3,6 +3,8 @@ Metadata:
Description: Developer stub, not yet ready for release!
Version: {DEV_VERSION}
Author: the OpenRA Developers
+ LogoImagePath: ./mods/ts/logo.png
+ PreviewImagePath: ./mods/ts/preview.png
RequiresMods:
modchooser: {DEV_VERSION}
@@ -142,7 +144,7 @@ ChromeLayout:
./mods/ra/chrome/ingame-infobriefing.yaml
./mods/ra/chrome/ingame-infoobjectives.yaml
./mods/ra/chrome/ingame-infostats.yaml
- ./mods/d2k/chrome/ingame-observer.yaml
+ d2k:chrome/ingame-observer.yaml
./mods/ts/chrome/ingame-observerstats.yaml
./mods/ts/chrome/ingame-player.yaml
./mods/ra/chrome/ingame-perf.yaml