diff --git a/OpenRA.Game/FileSystem/FileSystem.cs b/OpenRA.Game/FileSystem/FileSystem.cs index 94f92215cb..290cb50fc0 100644 --- a/OpenRA.Game/FileSystem/FileSystem.cs +++ b/OpenRA.Game/FileSystem/FileSystem.cs @@ -45,6 +45,8 @@ namespace OpenRA.FileSystem return new ZipFile(this, filename); if (filename.EndsWith(".oramap", StringComparison.InvariantCultureIgnoreCase)) return new ZipFile(this, filename); + if (filename.EndsWith(".oramod", StringComparison.InvariantCultureIgnoreCase)) + return new ZipFile(this, filename); if (filename.EndsWith(".RS", StringComparison.InvariantCultureIgnoreCase)) return new D2kSoundResources(this, filename); if (filename.EndsWith(".Z", StringComparison.InvariantCultureIgnoreCase)) @@ -61,8 +63,7 @@ namespace OpenRA.FileSystem IReadOnlyPackage parent; string subPath = null; if (TryGetPackageContaining(filename, out parent, out subPath)) - if (parent is Folder) - return new Folder(Path.Combine(((Folder)parent).Name, subPath)); + return OpenPackage(subPath, parent); return new Folder(Platform.ResolvePath(filename)); } @@ -90,6 +91,15 @@ namespace OpenRA.FileSystem if (filename.EndsWith(".oramap", StringComparison.InvariantCultureIgnoreCase)) return new ZipFile(this, filename, parent.GetStream(filename)); + if (parent is ZipFile) + return new ZipFolder(this, (ZipFile)parent, filename, filename); + + if (parent is ZipFolder) + { + var folder = (ZipFolder)parent; + return new ZipFolder(this, folder.Parent, folder.Name + "/" + filename, filename); + } + return null; } diff --git a/OpenRA.Game/FileSystem/ZipFolder.cs b/OpenRA.Game/FileSystem/ZipFolder.cs new file mode 100644 index 0000000000..68b0a5ee67 --- /dev/null +++ b/OpenRA.Game/FileSystem/ZipFolder.cs @@ -0,0 +1,75 @@ +#region Copyright & License Information +/* + * Copyright 2007-2015 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. For more information, + * see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using ICSharpCode.SharpZipLib.Zip; +using SZipFile = ICSharpCode.SharpZipLib.Zip.ZipFile; + +namespace OpenRA.FileSystem +{ + public sealed class ZipFolder : IReadOnlyPackage + { + public string Name { get; private set; } + public ZipFile Parent { get; private set; } + readonly string path; + + static ZipFolder() + { + ZipConstants.DefaultCodePage = Encoding.UTF8.CodePage; + } + + public ZipFolder(FileSystem context, ZipFile parent, string path, string filename) + { + if (filename.EndsWith("/")) + filename = filename.Substring(0, filename.Length - 1); + + Name = filename; + Parent = parent; + if (path.EndsWith("/")) + path = path.Substring(0, path.Length - 1); + + this.path = path; + } + + public Stream GetStream(string filename) + { + // Zip files use '/' as a path separator + return Parent.GetStream(path + '/' + filename); + } + + public IEnumerable Contents + { + get + { + foreach (var entry in Parent.Contents) + { + if (entry.StartsWith(path) && entry != path) + { + var filename = entry.Substring(path.Length + 1); + var dirLevels = filename.Split('/').Count(c => !string.IsNullOrEmpty(c)); + if (dirLevels == 1) + yield return filename; + } + } + } + } + + public bool Contains(string filename) + { + return Parent.Contains(path + '/' + filename); + } + + public void Dispose() { /* nothing to do */ } + } +} diff --git a/OpenRA.Game/ModMetadata.cs b/OpenRA.Game/ModMetadata.cs index e4cf61cfaf..7ce44e06b4 100644 --- a/OpenRA.Game/ModMetadata.cs +++ b/OpenRA.Game/ModMetadata.cs @@ -14,6 +14,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using OpenRA.FileSystem; +using OpenRA.Primitives; namespace OpenRA { @@ -40,10 +41,19 @@ namespace OpenRA try { IReadOnlyPackage package = null; - if (Directory.Exists(pair.Value)) - package = new Folder(pair.Value); + if (Directory.Exists(pair.Second)) + package = new Folder(pair.Second); else - throw new InvalidDataException(pair.Value + " is not a valid mod package"); + { + try + { + package = new ZipFile(null, pair.Second); + } + catch + { + throw new InvalidDataException(pair.Second + " is not a valid mod package"); + } + } if (!package.Contains("mod.yaml")) continue; @@ -54,7 +64,7 @@ namespace OpenRA continue; var metadata = FieldLoader.Load(nd["Metadata"]); - metadata.Id = pair.Key; + metadata.Id = pair.First; metadata.Package = package; if (nd.ContainsKey("RequiresMods")) @@ -65,11 +75,13 @@ namespace OpenRA if (nd.ContainsKey("ContentInstaller")) metadata.Content = FieldLoader.Load(nd["ContentInstaller"]); - ret.Add(pair.Key, metadata); + // Mods in the support directory and oramod packages (which are listed later + // in the CandidateMods list) override mods in the main install. + ret[pair.First] = metadata; } catch (Exception ex) { - Console.WriteLine("An exception occurred when trying to load ModMetadata for `{0}`:".F(pair.Key)); + Console.WriteLine("An exception occurred when trying to load ModMetadata for `{0}`:".F(pair.First)); Console.WriteLine(ex.Message); } } @@ -77,12 +89,16 @@ namespace OpenRA return ret; } - static Dictionary GetCandidateMods() + static IEnumerable> 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)); + .Select(x => Pair.New(x.Substring(basePath.Length + 1), x)) + .ToList(); + + foreach (var m in Directory.GetFiles(basePath, "*.oramod")) + mods.Add(Pair.New(Path.GetFileNameWithoutExtension(m), m)); // Get mods that are in the support folder. var supportPath = Platform.ResolvePath(Path.Combine("^", "mods")); @@ -90,7 +106,10 @@ namespace OpenRA return mods; foreach (var pair in Directory.GetDirectories(supportPath).ToDictionary(x => x.Substring(supportPath.Length + 1))) - mods.Add(pair.Key, pair.Value); + mods.Add(Pair.New(pair.Key, pair.Value)); + + foreach (var m in Directory.GetFiles(supportPath, "*.oramod")) + mods.Add(Pair.New(Path.GetFileNameWithoutExtension(m), m)); return mods; } diff --git a/OpenRA.Game/OpenRA.Game.csproj b/OpenRA.Game/OpenRA.Game.csproj index f8c5e82aa7..8b6134358c 100644 --- a/OpenRA.Game/OpenRA.Game.csproj +++ b/OpenRA.Game/OpenRA.Game.csproj @@ -255,6 +255,7 @@ + diff --git a/OpenRA.Mods.Common/Widgets/Logic/Editor/SaveMapLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Editor/SaveMapLogic.cs index dde32ffca1..5dcd4a7d8b 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Editor/SaveMapLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Editor/SaveMapLogic.cs @@ -121,7 +121,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic directoryDropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 210, writableDirectories, setupItem); } - var mapIsUnpacked = map.Package != null && map.Package is Folder; + var mapIsUnpacked = map.Package != null && (map.Package is Folder || map.Package is ZipFolder); var filename = widget.Get("FILENAME"); filename.Text = map.Package == null ? "" : mapIsUnpacked ? Path.GetFileName(map.Package.Name) : Path.GetFileNameWithoutExtension(map.Package.Name);