diff --git a/OpenRA.Game/FileSystem/FileSystem.cs b/OpenRA.Game/FileSystem/FileSystem.cs index e980e4a03b..9c92de38bb 100644 --- a/OpenRA.Game/FileSystem/FileSystem.cs +++ b/OpenRA.Game/FileSystem/FileSystem.cs @@ -34,17 +34,6 @@ namespace OpenRA.FileSystem Cache> fileIndex = new Cache>(_ => new List()); - public IReadWritePackage CreatePackage(string filename) - { - var content = new Dictionary(); - if (filename.EndsWith(".zip", StringComparison.InvariantCultureIgnoreCase)) - return new ZipFile(this, filename, content); - if (filename.EndsWith(".oramap", StringComparison.InvariantCultureIgnoreCase)) - return new ZipFile(this, filename, content); - - return new Folder(filename, content); - } - public IReadOnlyPackage OpenPackage(string filename) { if (filename.EndsWith(".mix", StringComparison.InvariantCultureIgnoreCase)) @@ -75,6 +64,32 @@ namespace OpenRA.FileSystem return new Folder(Platform.ResolvePath(filename)); } + public IReadOnlyPackage OpenPackage(string filename, IReadOnlyPackage parent) + { + // HACK: limit support to zip and folder until we generalize the PackageLoader support + if (parent is Folder) + { + var path = Path.Combine(parent.Name, filename); + + // HACK: work around SharpZipLib's lack of support for writing to in-memory files + if (filename.EndsWith(".zip", StringComparison.InvariantCultureIgnoreCase)) + return new ZipFile(this, path); + if (filename.EndsWith(".oramap", StringComparison.InvariantCultureIgnoreCase)) + return new ZipFile(this, path); + + var subFolder = Platform.ResolvePath(path); + if (Directory.Exists(subFolder)) + return new Folder(subFolder); + } + + if (filename.EndsWith(".zip", StringComparison.InvariantCultureIgnoreCase)) + return new ZipFile(this, filename, parent.GetStream(filename)); + if (filename.EndsWith(".oramap", StringComparison.InvariantCultureIgnoreCase)) + return new ZipFile(this, filename, parent.GetStream(filename)); + + return null; + } + public IReadWritePackage OpenWritablePackage(string filename) { if (filename.EndsWith(".zip", StringComparison.InvariantCultureIgnoreCase)) @@ -144,7 +159,13 @@ namespace OpenRA.FileSystem packagesForFile.RemoveAll(p => p == package); mountedPackages.Remove(package); - explicitMounts.Remove(package.Name); + var explicitKeys = explicitMounts.Where(kv => kv.Value == package) + .Select(kv => kv.Key) + .ToList(); + + foreach (var key in explicitKeys) + explicitMounts.Remove(key); + package.Dispose(); } else diff --git a/OpenRA.Game/FileSystem/Folder.cs b/OpenRA.Game/FileSystem/Folder.cs index 6f0e448f43..ebdbe08069 100644 --- a/OpenRA.Game/FileSystem/Folder.cs +++ b/OpenRA.Game/FileSystem/Folder.cs @@ -18,16 +18,6 @@ namespace OpenRA.FileSystem { readonly string path; - // Create a new folder package - public Folder(string path, Dictionary contents) - { - this.path = path; - if (Directory.Exists(path)) - Directory.Delete(path, true); - - Write(contents); - } - public Folder(string path) { this.path = path; @@ -43,6 +33,8 @@ namespace OpenRA.FileSystem { foreach (var filename in Directory.GetFiles(path, "*", SearchOption.TopDirectoryOnly)) yield return Path.GetFileName(filename); + foreach (var filename in Directory.GetDirectories(path)) + yield return Path.GetFileName(filename); } } @@ -54,18 +46,31 @@ namespace OpenRA.FileSystem public bool Contains(string filename) { - return File.Exists(Path.Combine(path, filename)); + var combined = Path.Combine(path, filename); + return combined.StartsWith(path) && File.Exists(combined); } - public void Write(Dictionary contents) + public void Update(string filename, byte[] contents) { if (!Directory.Exists(path)) Directory.CreateDirectory(path); - foreach (var file in contents) - using (var dataStream = File.Create(Path.Combine(path, file.Key))) - using (var writer = new BinaryWriter(dataStream)) - writer.Write(file.Value); + using (var s = File.Create(Path.Combine(path, filename))) + s.Write(contents, 0, contents.Length); + } + + public void Delete(string filename) + { + // HACK: ZipFiles can't be loaded as read-write from a stream, so we are + // forced to bypass the parent package and load them with their full path + // in FileSystem.OpenPackage. Their internal name therefore contains the + // full parent path too. We need to be careful to not add a second path + // prefix to these hacked packages. + var filePath = filename.StartsWith(path) ? filename : Path.Combine(path, filename); + if (Directory.Exists(filePath)) + Directory.Delete(filePath, true); + else if (File.Exists(filePath)) + File.Delete(filePath); } public void Dispose() { } diff --git a/OpenRA.Game/FileSystem/IPackage.cs b/OpenRA.Game/FileSystem/IPackage.cs index 05e56ebfd7..5e6355068e 100644 --- a/OpenRA.Game/FileSystem/IPackage.cs +++ b/OpenRA.Game/FileSystem/IPackage.cs @@ -25,6 +25,7 @@ namespace OpenRA.FileSystem public interface IReadWritePackage : IReadOnlyPackage { - void Write(Dictionary contents); + void Update(string filename, byte[] contents); + void Delete(string filename); } } diff --git a/OpenRA.Game/FileSystem/ZipFile.cs b/OpenRA.Game/FileSystem/ZipFile.cs index ed97c83786..217ec076e5 100644 --- a/OpenRA.Game/FileSystem/ZipFile.cs +++ b/OpenRA.Game/FileSystem/ZipFile.cs @@ -27,31 +27,24 @@ namespace OpenRA.FileSystem ZipConstants.DefaultCodePage = Encoding.UTF8.CodePage; } - public ZipFile(FileSystem context, string filename) + public ZipFile(FileSystem context, string filename, Stream stream, bool createOrClearContents = false) { Name = filename; - try - { - // Pull the file into memory, don't keep it open. - pkg = new SZipFile(new MemoryStream(File.ReadAllBytes(filename))); - } - catch (ZipException e) - { - Log.Write("debug", "Couldn't load zip file: {0}", e.Message); - } + if (createOrClearContents) + pkg = SZipFile.Create(stream); + else + pkg = new SZipFile(stream); } - // Create a new zip with the specified contents. - public ZipFile(FileSystem context, string filename, Dictionary contents) + public ZipFile(IReadOnlyFileSystem context, string filename, bool createOrClearContents = false) { Name = filename; - if (File.Exists(filename)) - File.Delete(filename); - - pkg = SZipFile.Create(filename); - Write(contents); + if (createOrClearContents) + pkg = SZipFile.Create(filename); + else + pkg = new SZipFile(filename); } public Stream GetStream(string filename) @@ -83,19 +76,18 @@ namespace OpenRA.FileSystem return pkg.GetEntry(filename) != null; } - public void Write(Dictionary contents) + public void Update(string filename, byte[] contents) { - // TODO: Clear existing content? - pkg.Close(); - pkg = SZipFile.Create(Name); pkg.BeginUpdate(); - - foreach (var kvp in contents) - pkg.Add(new StaticMemoryDataSource(kvp.Value), kvp.Key); - + pkg.Add(new StaticMemoryDataSource(contents), filename); + pkg.CommitUpdate(); + } + + public void Delete(string filename) + { + pkg.BeginUpdate(); + pkg.Delete(filename); pkg.CommitUpdate(); - pkg.Close(); - pkg = new SZipFile(new MemoryStream(File.ReadAllBytes(Name))); } public void Dispose() diff --git a/OpenRA.Game/Map/Map.cs b/OpenRA.Game/Map/Map.cs index e8c7cd928e..33518de4ff 100644 --- a/OpenRA.Game/Map/Map.cs +++ b/OpenRA.Game/Map/Map.cs @@ -490,9 +490,9 @@ namespace OpenRA return rules.Value; } - public void Save(IReadWritePackage toContainer) + public void Save(IReadWritePackage toPackage) { - MapFormat = 8; + MapFormat = SupportedMapFormat; var root = new List(); var fields = new[] @@ -534,31 +534,19 @@ namespace OpenRA root.Add(new MiniYamlNode("Notifications", null, NotificationDefinitions)); root.Add(new MiniYamlNode("Translations", null, TranslationDefinitions)); - var entries = new Dictionary(); - entries.Add("map.bin", SaveBinaryData()); - var s = root.WriteToString(); - entries.Add("map.yaml", Encoding.UTF8.GetBytes(s)); - - // Add any custom assets - if (Package != null) - { + // Saving to a new package: copy over all the content from the map + if (Package != null && toPackage != Package) foreach (var file in Package.Contents) - { - if (file == "map.bin" || file == "map.yaml") - continue; + toPackage.Update(file, Package.GetStream(file).ReadAllBytes()); - entries.Add(file, Package.GetStream(file).ReadAllBytes()); - } - } - - // Saving the map to a new location - Package = toContainer; - - // Update existing package - toContainer.Write(entries); + // Update the package with the new map data + var s = root.WriteToString(); + toPackage.Update("map.yaml", Encoding.UTF8.GetBytes(s)); + toPackage.Update("map.bin", SaveBinaryData()); + Package = toPackage; // Update UID to match the newly saved data - Uid = ComputeUID(toContainer); + Uid = ComputeUID(toPackage); } public CellLayer LoadMapTiles() diff --git a/OpenRA.Game/Map/MapCache.cs b/OpenRA.Game/Map/MapCache.cs index 5c6ce00eea..e3722fc8d6 100644 --- a/OpenRA.Game/Map/MapCache.cs +++ b/OpenRA.Game/Map/MapCache.cs @@ -26,6 +26,8 @@ namespace OpenRA public sealed class MapCache : IEnumerable, IDisposable { public static readonly MapPreview UnknownMap = new MapPreview(null, MapGridType.Rectangular, null); + public readonly IReadOnlyDictionary MapLocations; + readonly Cache previews; readonly ModData modData; readonly SheetBuilder sheetBuilder; @@ -41,6 +43,36 @@ namespace OpenRA var gridType = Exts.Lazy(() => modData.Manifest.Get().Type); previews = new Cache(uid => new MapPreview(uid, gridType.Value, this)); sheetBuilder = new SheetBuilder(SheetType.BGRA); + + // Enumerate map directories + var mapLocations = new Dictionary(); + foreach (var kv in modData.Manifest.MapFolders) + { + var name = kv.Key; + var classification = string.IsNullOrEmpty(kv.Value) + ? MapClassification.Unknown : Enum.Parse(kv.Value); + + IReadOnlyPackage package; + var optional = name.StartsWith("~"); + if (optional) + name = name.Substring(1); + + try + { + package = modData.ModFiles.OpenPackage(name); + } + catch + { + if (optional) + continue; + + throw; + } + + mapLocations.Add(package, classification); + } + + MapLocations = new ReadOnlyDictionary(mapLocations); } public void LoadMaps() @@ -49,29 +81,33 @@ namespace OpenRA if (!modData.Manifest.Contains()) return; - // Expand the dictionary (dir path, dir type) to a dictionary of (map path, dir type) - var mapPaths = modData.Manifest.MapFolders.SelectMany(kv => - FindMapsIn(modData.ModFiles, kv.Key).ToDictionary(p => p, p => string.IsNullOrEmpty(kv.Value) - ? MapClassification.Unknown : Enum.Parse(kv.Value))); - - var mapGrid = modData.Manifest.Get(); - foreach (var path in mapPaths) + var mapGrid = Game.ModData.Manifest.Get(); + foreach (var kv in MapLocations) { - try + foreach (var map in kv.Key.Contents) { - using (new Support.PerfTimer(path.Key)) + IReadOnlyPackage mapPackage = null; + try { - var package = modData.ModFiles.OpenPackage(path.Key); - var uid = Map.ComputeUID(package); - previews[uid].UpdateFromMap(package, path.Value, modData.Manifest.MapCompatibility, mapGrid.Type); + using (new Support.PerfTimer(map)) + { + mapPackage = modData.ModFiles.OpenPackage(map, kv.Key); + if (mapPackage == null) + continue; + + var uid = Map.ComputeUID(mapPackage); + previews[uid].UpdateFromMap(mapPackage, kv.Key, kv.Value, modData.Manifest.MapCompatibility, mapGrid.Type); + } + } + catch (Exception e) + { + if (mapPackage != null) + mapPackage.Dispose(); + Console.WriteLine("Failed to load map: {0}", map); + Console.WriteLine("Details: {0}", e); + Log.Write("debug", "Failed to load map: {0}", map); + Log.Write("debug", "Details: {0}", e); } - } - catch (Exception e) - { - Console.WriteLine("Failed to load map: {0}", path); - Console.WriteLine("Details: {0}", e); - Log.Write("debug", "Failed to load map: {0}", path); - Log.Write("debug", "Details: {0}", e); } } } @@ -119,31 +155,6 @@ namespace OpenRA new Download(url, _ => { }, onInfoComplete); } - public static IEnumerable FindMapsIn(FileSystem.FileSystem context, string dir) - { - string[] noMaps = { }; - - // Ignore optional flag - if (dir.StartsWith("~")) - dir = dir.Substring(1); - - // HACK: We currently only support maps loaded from Folders - // This is a temporary workaround that resolves the filesystem paths to a system directory - IReadOnlyPackage package; - string filename; - if (context.TryGetPackageContaining(dir, out package, out filename)) - dir = Path.Combine(package.Name, filename); - else if (Directory.Exists(Platform.ResolvePath(dir))) - dir = Platform.ResolvePath(dir); - else - return noMaps; - - var dirsWithMaps = Directory.GetDirectories(dir) - .Where(d => Directory.GetFiles(d, "map.yaml").Any() && Directory.GetFiles(d, "map.bin").Any()); - - return dirsWithMaps.Concat(Directory.GetFiles(dir, "*.oramap")); - } - void LoadAsyncInternal() { Log.Write("debug", "MapCache.LoadAsyncInternal started"); @@ -269,6 +280,9 @@ namespace OpenRA return; } + foreach (var p in previews.Values) + p.Dispose(); + // We need to let the loader thread exit before we can dispose our sheet builder. // Ideally we should dispose our resources before returning, but we don't to block waiting on the loader thread to exit. // Instead, we'll queue disposal to be run once it has exited. diff --git a/OpenRA.Game/Map/MapPreview.cs b/OpenRA.Game/Map/MapPreview.cs index e5cd1964d1..4eae134097 100644 --- a/OpenRA.Game/Map/MapPreview.cs +++ b/OpenRA.Game/Map/MapPreview.cs @@ -53,13 +53,14 @@ namespace OpenRA public readonly bool downloading; } - public class MapPreview + public class MapPreview : IDisposable { static readonly CPos[] NoSpawns = new CPos[] { }; MapCache cache; public readonly string Uid; public IReadOnlyPackage Package { get; private set; } + IReadOnlyPackage parentPackage; public string Title { get; private set; } public string Type { get; private set; } @@ -116,7 +117,7 @@ namespace OpenRA Visibility = MapVisibility.Lobby; } - public void UpdateFromMap(IReadOnlyPackage p, MapClassification classification, string[] mapCompatibility, MapGridType gridType) + public void UpdateFromMap(IReadOnlyPackage p, IReadOnlyPackage parent, MapClassification classification, string[] mapCompatibility, MapGridType gridType) { Dictionary yaml; using (var yamlStream = p.GetStream("map.yaml")) @@ -128,6 +129,7 @@ namespace OpenRA } Package = p; + parentPackage = parent; GridType = gridType; Class = classification; @@ -270,19 +272,22 @@ namespace OpenRA return; Status = MapStatus.Downloading; - var baseMapPath = Platform.ResolvePath("^", "maps", Game.ModData.Manifest.Mod.Id); - - // Create the map directory if it doesn't exist - if (!Directory.Exists(baseMapPath)) - Directory.CreateDirectory(baseMapPath); + var installLocation = cache.MapLocations.FirstOrDefault(p => p.Value == MapClassification.User); + if (installLocation.Key == null || !(installLocation.Key is IReadWritePackage)) + { + Log.Write("debug", "Map install directory not found"); + Status = MapStatus.DownloadError; + return; + } + var mapInstallPackage = installLocation.Key as IReadWritePackage; var modData = Game.ModData; new Thread(() => { // Request the filename from the server // Run in a worker thread to avoid network delays var mapUrl = Game.Settings.Game.MapRepository + Uid; - var mapPath = string.Empty; + var mapFilename = string.Empty; try { var request = WebRequest.Create(mapUrl); @@ -296,11 +301,11 @@ namespace OpenRA return; } - mapPath = System.IO.Path.Combine(baseMapPath, res.Headers["Content-Disposition"].Replace("attachment; filename = ", "")); + mapFilename = res.Headers["Content-Disposition"].Replace("attachment; filename = ", ""); } Action onDownloadProgress = i => { DownloadBytes = i.BytesReceived; DownloadPercentage = i.ProgressPercentage; }; - Action onDownloadComplete = (i, cancelled) => + Action onDownloadComplete = (i, cancelled) => { download = null; @@ -313,15 +318,16 @@ namespace OpenRA return; } - Log.Write("debug", "Downloaded map to '{0}'", mapPath); + mapInstallPackage.Update(mapFilename, i.Result); + Log.Write("debug", "Downloaded map to '{0}'", mapFilename); Game.RunAfterTick(() => { - using (var package = modData.ModFiles.OpenPackage(mapPath)) - UpdateFromMap(package, MapClassification.User, null, GridType); + var package = modData.ModFiles.OpenPackage(mapFilename, mapInstallPackage); + UpdateFromMap(package, mapInstallPackage, MapClassification.User, null, GridType); }); }; - download = new Download(mapUrl, mapPath, onDownloadProgress, onDownloadComplete); + download = new Download(mapUrl, onDownloadProgress, onDownloadComplete); } catch (Exception e) { @@ -344,5 +350,22 @@ namespace OpenRA { Status = MapStatus.Unavailable; } + + public void Dispose() + { + if (Package != null) + { + Package.Dispose(); + Package = null; + } + } + + public void Delete() + { + Invalidate(); + var deleteFromPackage = parentPackage as IReadWritePackage; + if (deleteFromPackage != null) + deleteFromPackage.Delete(Package.Name); + } } } diff --git a/OpenRA.Mods.Common/UtilityCommands/ImportLegacyMapCommand.cs b/OpenRA.Mods.Common/UtilityCommands/ImportLegacyMapCommand.cs index f3efe50437..099583d101 100644 --- a/OpenRA.Mods.Common/UtilityCommands/ImportLegacyMapCommand.cs +++ b/OpenRA.Mods.Common/UtilityCommands/ImportLegacyMapCommand.cs @@ -14,6 +14,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using OpenRA.FileSystem; using OpenRA.Graphics; using OpenRA.Mods.Common.FileFormats; using OpenRA.Mods.Common.Traits; @@ -95,7 +96,7 @@ namespace OpenRA.Mods.Common.UtilityCommands Map.FixOpenAreas(Rules); var dest = Path.GetFileNameWithoutExtension(args[1]) + ".oramap"; - var package = modData.ModFiles.CreatePackage(dest); + var package = new ZipFile(modData.ModFiles, dest, true); Map.Save(package); Console.WriteLine(dest + " saved."); } diff --git a/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs b/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs index b6405dd13e..e1a4d69047 100644 --- a/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs +++ b/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs @@ -785,17 +785,7 @@ namespace OpenRA.Mods.Common.UtilityCommands yaml.Nodes.First(n => n.Key == "MapFormat").Value = new MiniYaml(Map.SupportedMapFormat.ToString()); - var entries = new Dictionary(); - entries.Add("map.yaml", Encoding.UTF8.GetBytes(yaml.Nodes.WriteToString())); - foreach (var file in package.Contents) - { - if (file == "map.yaml") - continue; - - entries.Add(file, package.GetStream(file).ReadAllBytes()); - } - - package.Write(entries); + package.Update("map.yaml", Encoding.UTF8.GetBytes(yaml.Nodes.WriteToString())); } } } diff --git a/OpenRA.Mods.Common/Widgets/Logic/Editor/SaveMapLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Editor/SaveMapLogic.cs index db20d5f782..dde32ffca1 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Editor/SaveMapLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Editor/SaveMapLogic.cs @@ -28,6 +28,20 @@ namespace OpenRA.Mods.Common.Widgets.Logic public string UiLabel; } + class SaveDirectory + { + public readonly Folder Folder; + public readonly string DisplayName; + public readonly MapClassification Classification; + + public SaveDirectory(Folder folder, MapClassification classification) + { + Folder = folder; + DisplayName = Platform.UnresolvePath(Folder.Name); + Classification = classification; + } + } + [ObjectCreator.UseCtor] public SaveMapLogic(Widget widget, ModData modData, Action onSave, Action onExit, Map map, List playerDefinitions, List actorDefinitions) @@ -60,58 +74,57 @@ namespace OpenRA.Mods.Common.Widgets.Logic visibilityDropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 210, mapVisibility, setupItem); } - Func makeMapDirectory = dir => - { - if (dir.StartsWith("~")) - dir = dir.Substring(1); + var writableDirectories = new List(); + SaveDirectory selectedDirectory = null; - IReadOnlyPackage package; - string f; - if (modData.ModFiles.TryGetPackageContaining(dir, out package, out f)) - dir = Path.Combine(package.Name, f); - - return Platform.UnresolvePath(dir); - }; - - var mapDirectories = modData.Manifest.MapFolders - .ToDictionary(kv => makeMapDirectory(kv.Key), kv => Enum.Parse(kv.Value)); - - var mapPath = map.Package != null ? map.Package.Name : null; var directoryDropdown = widget.Get("DIRECTORY_DROPDOWN"); { - Func setupItem = (option, template) => + Func setupItem = (option, template) => { var item = ScrollItemWidget.Setup(template, - () => directoryDropdown.Text == option, - () => directoryDropdown.Text = option); - item.Get("LABEL").GetText = () => option; + () => selectedDirectory == option, + () => selectedDirectory = option); + item.Get("LABEL").GetText = () => option.DisplayName; return item; }; - // TODO: This won't work for maps inside oramod packages - var mapDirectory = mapPath != null ? Platform.UnresolvePath(Path.GetDirectoryName(mapPath)) : null; - var initialDirectory = mapDirectories.Keys.FirstOrDefault(f => f == mapDirectory); + foreach (var kv in modData.MapCache.MapLocations) + { + var folder = kv.Key as Folder; + if (folder == null) + continue; + + try + { + using (var fs = File.Create(Path.Combine(folder.Name, ".testwritable"), 1, FileOptions.DeleteOnClose)) + { + // Do nothing: we just want to test whether we can create the file + } + + writableDirectories.Add(new SaveDirectory(folder, kv.Value)); + } + catch + { + // Directory is not writable + } + } + + if (map.Package != null) + selectedDirectory = writableDirectories.FirstOrDefault(k => k.Folder.Contains(map.Package.Name)); // Prioritize MapClassification.User directories over system directories - if (initialDirectory == null) - initialDirectory = mapDirectories.OrderByDescending(kv => kv.Value).First().Key; + if (selectedDirectory == null) + selectedDirectory = writableDirectories.OrderByDescending(kv => kv.Classification).First(); - directoryDropdown.Text = initialDirectory; + directoryDropdown.GetText = () => selectedDirectory == null ? "" : selectedDirectory.DisplayName; directoryDropdown.OnClick = () => - directoryDropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 210, mapDirectories.Keys, setupItem); + directoryDropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 210, writableDirectories, setupItem); } - var mapIsUnpacked = false; - - // TODO: This won't work for maps inside oramod packages - if (mapPath != null) - { - var attr = File.GetAttributes(mapPath); - mapIsUnpacked = attr.HasFlag(FileAttributes.Directory); - } + var mapIsUnpacked = map.Package != null && map.Package is Folder; var filename = widget.Get("FILENAME"); - filename.Text = mapIsUnpacked ? Path.GetFileName(mapPath) : Path.GetFileNameWithoutExtension(mapPath); + filename.Text = map.Package == null ? "" : mapIsUnpacked ? Path.GetFileName(map.Package.Name) : Path.GetFileNameWithoutExtension(map.Package.Name); var fileType = mapIsUnpacked ? MapFileType.Unpacked : MapFileType.OraMap; var fileTypes = new Dictionary() @@ -159,22 +172,33 @@ namespace OpenRA.Mods.Common.Widgets.Logic map.RequiresMod = modData.Manifest.Mod.Id; - // Create the map directory if required - Directory.CreateDirectory(Platform.ResolvePath(directoryDropdown.Text)); - - // TODO: This won't work for maps inside oramod packages - var combinedPath = Platform.ResolvePath(Path.Combine(directoryDropdown.Text, filename.Text + fileTypes[fileType].Extension)); + var combinedPath = Platform.ResolvePath(Path.Combine(selectedDirectory.Folder.Name, filename.Text + fileTypes[fileType].Extension)); // Invalidate the old map metadata - if (map.Uid != null && combinedPath == mapPath) + if (map.Uid != null && map.Package != null && map.Package.Name == combinedPath) modData.MapCache[map.Uid].Invalidate(); - var package = modData.ModFiles.CreatePackage(combinedPath); - map.Save(package); + try + { + var package = map.Package as IReadWritePackage; + if (package == null || package.Name != combinedPath) + { + selectedDirectory.Folder.Delete(combinedPath); + if (fileType == MapFileType.OraMap) + package = new ZipFile(modData.DefaultFileSystem, combinedPath, true); + else + package = new Folder(combinedPath); + } + + map.Save(package); + } + catch + { + Console.WriteLine("Failed to save map at {0}", combinedPath); + } // Update the map cache so it can be loaded without restarting the game - var classification = mapDirectories[directoryDropdown.Text]; - modData.MapCache[map.Uid].UpdateFromMap(map.Package, classification, null, map.Grid.Type); + modData.MapCache[map.Uid].UpdateFromMap(map.Package, selectedDirectory.Folder, selectedDirectory.Classification, null, map.Grid.Type); Console.WriteLine("Saved current map at {0}", combinedPath); Ui.CloseWindow(); diff --git a/OpenRA.Mods.Common/Widgets/Logic/MapChooserLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/MapChooserLogic.cs index 9149a2fde1..a1ce1dd4a8 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/MapChooserLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/MapChooserLogic.cs @@ -288,22 +288,15 @@ namespace OpenRA.Mods.Common.Widgets.Logic string DeleteMap(string map) { - var path = modData.MapCache[map].Package.Name; try { - if (File.Exists(path)) - File.Delete(path); - else if (Directory.Exists(path)) - Directory.Delete(path, true); - - modData.MapCache[map].Invalidate(); - + modData.MapCache[map].Delete(); if (selectedUid == map) selectedUid = WidgetUtils.ChooseInitialMap(tabMaps[currentTab].Select(mp => mp.Uid).FirstOrDefault()); } catch (Exception ex) { - Game.Debug("Failed to delete map '{0}'. See the debug.log file for details.", path); + Game.Debug("Failed to delete map '{0}'. See the debug.log file for details.", map); Log.Write("debug", ex.ToString()); } diff --git a/OpenRA.Mods.D2k/UtilityCommands/ImportD2kMapCommand.cs b/OpenRA.Mods.D2k/UtilityCommands/ImportD2kMapCommand.cs index 387c2932b8..e57c34c162 100644 --- a/OpenRA.Mods.D2k/UtilityCommands/ImportD2kMapCommand.cs +++ b/OpenRA.Mods.D2k/UtilityCommands/ImportD2kMapCommand.cs @@ -11,6 +11,7 @@ using System; using System.IO; +using OpenRA.FileSystem; namespace OpenRA.Mods.D2k.UtilityCommands { @@ -37,7 +38,7 @@ namespace OpenRA.Mods.D2k.UtilityCommands return; var dest = Path.GetFileNameWithoutExtension(args[1]) + ".oramap"; - var package = modData.ModFiles.CreatePackage(dest); + var package = new ZipFile(modData.DefaultFileSystem, dest, true); map.Save(package); Console.WriteLine(dest + " saved."); } diff --git a/OpenRA.Mods.TS/UtilityCommands/ImportTSMapCommand.cs b/OpenRA.Mods.TS/UtilityCommands/ImportTSMapCommand.cs index 2b2c6efb75..0f42b7e616 100644 --- a/OpenRA.Mods.TS/UtilityCommands/ImportTSMapCommand.cs +++ b/OpenRA.Mods.TS/UtilityCommands/ImportTSMapCommand.cs @@ -179,7 +179,7 @@ namespace OpenRA.Mods.TS.UtilityCommands map.PlayerDefinitions = mapPlayers.ToMiniYaml(); var dest = Path.GetFileNameWithoutExtension(args[1]) + ".oramap"; - var package = modData.ModFiles.CreatePackage(dest); + var package = new ZipFile(modData.DefaultFileSystem, dest, true); map.Save(package); Console.WriteLine(dest + " saved."); }