Implement mod-defined package loaders.
This commit is contained in:
@@ -11,16 +11,16 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using OpenRA.FileFormats;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
namespace OpenRA.FileSystem
|
||||
{
|
||||
public sealed class BagFile : IReadOnlyPackage
|
||||
public class AudioBagLoader : IPackageLoader
|
||||
{
|
||||
sealed class BagFile : IReadOnlyPackage
|
||||
{
|
||||
public string Name { get; private set; }
|
||||
public IEnumerable<string> Contents { get { return index.Keys; } }
|
||||
@@ -28,24 +28,14 @@ namespace OpenRA.FileSystem
|
||||
readonly Stream s;
|
||||
readonly Dictionary<string, IdxEntry> index;
|
||||
|
||||
public BagFile(FileSystem context, string filename)
|
||||
public BagFile(Stream s, List<IdxEntry> entries, string filename)
|
||||
{
|
||||
Name = filename;
|
||||
|
||||
// A bag file is always accompanied with an .idx counterpart
|
||||
// For example: audio.bag requires the audio.idx file
|
||||
var indexFilename = Path.ChangeExtension(filename, ".idx");
|
||||
|
||||
// Build the index and dispose the stream, it is no longer needed after this
|
||||
List<IdxEntry> entries;
|
||||
using (var indexStream = context.Open(indexFilename))
|
||||
entries = new IdxReader(indexStream).Entries;
|
||||
this.s = s;
|
||||
|
||||
index = entries.ToDictionaryWithConflictLog(x => x.Filename,
|
||||
"{0} (bag format)".F(filename),
|
||||
null, x => "(offs={0}, len={1})".F(x.Offset, x.Length));
|
||||
|
||||
s = context.Open(filename);
|
||||
}
|
||||
|
||||
public Stream GetStream(string filename)
|
||||
@@ -119,9 +109,45 @@ namespace OpenRA.FileSystem
|
||||
return index.ContainsKey(filename);
|
||||
}
|
||||
|
||||
public IReadOnlyPackage OpenPackage(string filename, FileSystem context)
|
||||
{
|
||||
// Not implemented
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
s.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
bool IPackageLoader.TryParsePackage(Stream s, string filename, FileSystem context, out IReadOnlyPackage package)
|
||||
{
|
||||
if (!filename.EndsWith(".bag", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
package = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// A bag file is always accompanied with an .idx counterpart
|
||||
// For example: audio.bag requires the audio.idx file
|
||||
var indexFilename = Path.ChangeExtension(filename, ".idx");
|
||||
List<IdxEntry> entries = null;
|
||||
|
||||
try
|
||||
{
|
||||
// Build the index and dispose the stream, it is no longer needed after this
|
||||
using (var indexStream = context.Open(indexFilename))
|
||||
entries = new IdxReader(indexStream).Entries;
|
||||
}
|
||||
catch
|
||||
{
|
||||
package = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
package = new BagFile(s, entries, filename);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,11 +12,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenRA.FileSystem
|
||||
{
|
||||
public sealed class BigFile : IReadOnlyPackage
|
||||
public class BigLoader : IPackageLoader
|
||||
{
|
||||
sealed class BigFile : IReadOnlyPackage
|
||||
{
|
||||
public string Name { get; private set; }
|
||||
public IEnumerable<string> Contents { get { return index.Keys; } }
|
||||
@@ -24,15 +25,14 @@ namespace OpenRA.FileSystem
|
||||
readonly Dictionary<string, Entry> index = new Dictionary<string, Entry>();
|
||||
readonly Stream s;
|
||||
|
||||
public BigFile(FileSystem context, string filename)
|
||||
public BigFile(Stream s, string filename)
|
||||
{
|
||||
Name = filename;
|
||||
this.s = s;
|
||||
|
||||
s = context.Open(filename);
|
||||
try
|
||||
{
|
||||
if (s.ReadASCII(4) != "BIGF")
|
||||
throw new InvalidDataException("Header is not BIGF");
|
||||
/* var signature = */ s.ReadASCII(4);
|
||||
|
||||
// Total archive size.
|
||||
s.ReadUInt32();
|
||||
@@ -97,9 +97,32 @@ namespace OpenRA.FileSystem
|
||||
return index.ContainsKey(filename);
|
||||
}
|
||||
|
||||
public IReadOnlyPackage OpenPackage(string filename, FileSystem context)
|
||||
{
|
||||
// Not implemented
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
s.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
bool IPackageLoader.TryParsePackage(Stream s, string filename, FileSystem context, out IReadOnlyPackage package)
|
||||
{
|
||||
// Take a peek at the file signature
|
||||
var signature = s.ReadASCII(4);
|
||||
s.Position -= 4;
|
||||
|
||||
if (signature != "BIGF")
|
||||
{
|
||||
package = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
package = new BigFile(s, filename);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,15 @@
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
namespace OpenRA.FileSystem
|
||||
{
|
||||
public sealed class D2kSoundResources : IReadOnlyPackage
|
||||
public class D2kSoundResourcesLoader : IPackageLoader
|
||||
{
|
||||
sealed class D2kSoundResources : IReadOnlyPackage
|
||||
{
|
||||
struct Entry
|
||||
{
|
||||
@@ -35,11 +37,11 @@ namespace OpenRA.FileSystem
|
||||
readonly Stream s;
|
||||
readonly Dictionary<string, Entry> index = new Dictionary<string, Entry>();
|
||||
|
||||
public D2kSoundResources(FileSystem context, string filename)
|
||||
public D2kSoundResources(Stream s, string filename)
|
||||
{
|
||||
Name = filename;
|
||||
this.s = s;
|
||||
|
||||
s = context.Open(filename);
|
||||
try
|
||||
{
|
||||
var headerLength = s.ReadUInt32();
|
||||
@@ -68,6 +70,12 @@ namespace OpenRA.FileSystem
|
||||
return new MemoryStream(s.ReadBytes((int)e.Length));
|
||||
}
|
||||
|
||||
public IReadOnlyPackage OpenPackage(string filename, FileSystem context)
|
||||
{
|
||||
// Not implemented
|
||||
return null;
|
||||
}
|
||||
|
||||
public bool Contains(string filename)
|
||||
{
|
||||
return index.ContainsKey(filename);
|
||||
@@ -78,4 +86,17 @@ namespace OpenRA.FileSystem
|
||||
s.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
bool IPackageLoader.TryParsePackage(Stream s, string filename, FileSystem context, out IReadOnlyPackage package)
|
||||
{
|
||||
if (!filename.EndsWith(".rs", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
package = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
package = new D2kSoundResources(s, filename);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,80 +34,69 @@ namespace OpenRA.FileSystem
|
||||
// Mod packages that should not be disposed
|
||||
readonly List<IReadOnlyPackage> modPackages = new List<IReadOnlyPackage>();
|
||||
readonly IReadOnlyDictionary<string, Manifest> installedMods;
|
||||
readonly IPackageLoader[] packageLoaders;
|
||||
|
||||
Cache<string, List<IReadOnlyPackage>> fileIndex = new Cache<string, List<IReadOnlyPackage>>(_ => new List<IReadOnlyPackage>());
|
||||
|
||||
public FileSystem(IReadOnlyDictionary<string, Manifest> installedMods)
|
||||
public FileSystem(IReadOnlyDictionary<string, Manifest> installedMods, IPackageLoader[] packageLoaders)
|
||||
{
|
||||
this.installedMods = installedMods;
|
||||
this.packageLoaders = packageLoaders
|
||||
.Append(new ZipFileLoader())
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public bool TryParsePackage(Stream stream, string filename, out IReadOnlyPackage package)
|
||||
{
|
||||
package = null;
|
||||
foreach (var packageLoader in packageLoaders)
|
||||
if (packageLoader.TryParsePackage(stream, filename, this, out package))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public IReadOnlyPackage OpenPackage(string filename)
|
||||
{
|
||||
if (filename.EndsWith(".mix", StringComparison.InvariantCultureIgnoreCase))
|
||||
return new MixFile(this, filename);
|
||||
if (filename.EndsWith(".zip", StringComparison.InvariantCultureIgnoreCase))
|
||||
return new ZipFile(this, filename);
|
||||
if (filename.EndsWith(".oramap", StringComparison.InvariantCultureIgnoreCase))
|
||||
return new ZipFile(this, filename);
|
||||
if (filename.EndsWith(".RS", StringComparison.InvariantCultureIgnoreCase))
|
||||
return new D2kSoundResources(this, filename);
|
||||
if (filename.EndsWith(".Z", StringComparison.InvariantCultureIgnoreCase))
|
||||
return new InstallShieldPackage(this, filename);
|
||||
if (filename.EndsWith(".PAK", StringComparison.InvariantCultureIgnoreCase))
|
||||
return new PakFile(this, filename);
|
||||
if (filename.EndsWith(".big", StringComparison.InvariantCultureIgnoreCase))
|
||||
return new BigFile(this, filename);
|
||||
if (filename.EndsWith(".bag", StringComparison.InvariantCultureIgnoreCase))
|
||||
return new BagFile(this, filename);
|
||||
// Raw directories are the easiest and one of the most common cases, so try these first
|
||||
var resolvedPath = Platform.ResolvePath(filename);
|
||||
if (!filename.Contains("|") && Directory.Exists(resolvedPath))
|
||||
return new Folder(resolvedPath);
|
||||
|
||||
// Children of another package require special handling
|
||||
IReadOnlyPackage parent;
|
||||
string subPath = null;
|
||||
if (TryGetPackageContaining(filename, out parent, out subPath))
|
||||
return OpenPackage(subPath, parent);
|
||||
return parent.OpenPackage(subPath, this);
|
||||
|
||||
return new Folder(Platform.ResolvePath(filename));
|
||||
}
|
||||
// Try and open it normally
|
||||
IReadOnlyPackage package;
|
||||
var stream = Open(filename);
|
||||
if (TryParsePackage(stream, filename, out package))
|
||||
return package;
|
||||
|
||||
public IReadOnlyPackage OpenPackage(string filename, IReadOnlyPackage parent)
|
||||
{
|
||||
// HACK: limit support to zip and folder until we generalize the PackageLoader support
|
||||
if (filename.EndsWith(".zip", StringComparison.InvariantCultureIgnoreCase) ||
|
||||
filename.EndsWith(".oramap", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
using (var s = parent.GetStream(filename))
|
||||
return new ZipFile(s, filename, parent);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (parent is Folder)
|
||||
{
|
||||
var subFolder = Platform.ResolvePath(Path.Combine(parent.Name, filename));
|
||||
if (Directory.Exists(subFolder))
|
||||
return new Folder(subFolder);
|
||||
}
|
||||
// No package loaders took ownership of the stream, so clean it up
|
||||
stream.Dispose();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public IReadOnlyPackage OpenPackage(string filename, IReadOnlyPackage parent)
|
||||
{
|
||||
// TODO: Make legacy callers access the parent package directly
|
||||
return parent.OpenPackage(filename, this);
|
||||
}
|
||||
|
||||
public void Mount(string name, string explicitName = null)
|
||||
{
|
||||
var optional = name.StartsWith("~");
|
||||
var optional = name.StartsWith("~", StringComparison.Ordinal);
|
||||
if (optional)
|
||||
name = name.Substring(1);
|
||||
|
||||
try
|
||||
{
|
||||
IReadOnlyPackage package;
|
||||
if (name.StartsWith("$"))
|
||||
if (name.StartsWith("$", StringComparison.Ordinal))
|
||||
{
|
||||
name = name.Substring(1);
|
||||
|
||||
|
||||
@@ -60,6 +60,9 @@ namespace OpenRA.FileSystem
|
||||
// Other package types can be loaded normally
|
||||
IReadOnlyPackage package;
|
||||
var s = GetStream(filename);
|
||||
if (s == null)
|
||||
return null;
|
||||
|
||||
if (context.TryParsePackage(s, filename, out package))
|
||||
return package;
|
||||
|
||||
|
||||
@@ -15,12 +15,23 @@ using System.IO;
|
||||
|
||||
namespace OpenRA.FileSystem
|
||||
{
|
||||
public interface IPackageLoader
|
||||
{
|
||||
/// <summary>
|
||||
/// Attempt to parse a stream as this type of package.
|
||||
/// If successful, the loader is expected to take ownership of `s` and dispose it once done.
|
||||
/// If unsuccessful, the loader is expected to return the stream position to where it started.
|
||||
/// </summary>
|
||||
bool TryParsePackage(Stream s, string filename, FileSystem context, out IReadOnlyPackage package);
|
||||
}
|
||||
|
||||
public interface IReadOnlyPackage : IDisposable
|
||||
{
|
||||
string Name { get; }
|
||||
IEnumerable<string> Contents { get; }
|
||||
Stream GetStream(string filename);
|
||||
bool Contains(string filename);
|
||||
IReadOnlyPackage OpenPackage(string filename, FileSystem context);
|
||||
}
|
||||
|
||||
public interface IReadWritePackage : IReadOnlyPackage
|
||||
|
||||
@@ -16,6 +16,8 @@ using OpenRA.FileFormats;
|
||||
|
||||
namespace OpenRA.FileSystem
|
||||
{
|
||||
public class InstallShieldLoader : IPackageLoader
|
||||
{
|
||||
public sealed class InstallShieldPackage : IReadOnlyPackage
|
||||
{
|
||||
public struct Entry
|
||||
@@ -37,18 +39,15 @@ namespace OpenRA.FileSystem
|
||||
readonly Stream s;
|
||||
readonly long dataStart = 255;
|
||||
|
||||
public InstallShieldPackage(FileSystem context, string filename)
|
||||
public InstallShieldPackage(Stream s, string filename)
|
||||
{
|
||||
Name = filename;
|
||||
this.s = s;
|
||||
|
||||
s = context.Open(filename);
|
||||
try
|
||||
{
|
||||
// Parse package header
|
||||
var signature = s.ReadUInt32();
|
||||
if (signature != 0x8C655D13)
|
||||
throw new InvalidDataException("Not an Installshield package");
|
||||
|
||||
/*var signature = */s.ReadUInt32();
|
||||
s.Position += 8;
|
||||
/*var FileCount = */s.ReadUInt16();
|
||||
s.Position += 4;
|
||||
@@ -79,7 +78,7 @@ namespace OpenRA.FileSystem
|
||||
// Parse files
|
||||
foreach (var dir in directories)
|
||||
for (var i = 0; i < dir.Value; i++)
|
||||
ParseFile(s, dir.Key);
|
||||
ParseFile(dir.Key);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -89,7 +88,7 @@ namespace OpenRA.FileSystem
|
||||
}
|
||||
|
||||
uint accumulatedData = 0;
|
||||
void ParseFile(Stream s, string dirName)
|
||||
void ParseFile(string dirName)
|
||||
{
|
||||
s.Position += 7;
|
||||
var compressedSize = s.ReadUInt32();
|
||||
@@ -122,6 +121,12 @@ namespace OpenRA.FileSystem
|
||||
return ret;
|
||||
}
|
||||
|
||||
public IReadOnlyPackage OpenPackage(string filename, FileSystem context)
|
||||
{
|
||||
// Not implemented
|
||||
return null;
|
||||
}
|
||||
|
||||
public bool Contains(string filename)
|
||||
{
|
||||
return index.ContainsKey(filename);
|
||||
@@ -134,4 +139,21 @@ namespace OpenRA.FileSystem
|
||||
s.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
bool IPackageLoader.TryParsePackage(Stream s, string filename, FileSystem context, out IReadOnlyPackage package)
|
||||
{
|
||||
// Take a peek at the file signature
|
||||
var signature = s.ReadUInt32();
|
||||
s.Position -= 4;
|
||||
|
||||
if (signature != 0x8C655D13)
|
||||
{
|
||||
package = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
package = new InstallShieldPackage(s, filename);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using OpenRA.FileFormats;
|
||||
@@ -19,6 +18,8 @@ using OpenRA.Primitives;
|
||||
|
||||
namespace OpenRA.FileSystem
|
||||
{
|
||||
public class MixLoader : IPackageLoader
|
||||
{
|
||||
public sealed class MixFile : IReadOnlyPackage
|
||||
{
|
||||
public string Name { get; private set; }
|
||||
@@ -27,14 +28,12 @@ namespace OpenRA.FileSystem
|
||||
readonly Dictionary<string, PackageEntry> index;
|
||||
readonly long dataStart;
|
||||
readonly Stream s;
|
||||
readonly FileSystem context;
|
||||
|
||||
public MixFile(FileSystem context, string filename)
|
||||
public MixFile(Stream s, string filename, HashSet<string> allPossibleFilenames)
|
||||
{
|
||||
Name = filename;
|
||||
this.context = context;
|
||||
this.s = s;
|
||||
|
||||
s = context.Open(filename);
|
||||
try
|
||||
{
|
||||
// Detect format type
|
||||
@@ -56,7 +55,7 @@ namespace OpenRA.FileSystem
|
||||
|
||||
index = ParseIndex(entries.ToDictionaryWithConflictLog(x => x.Hash,
|
||||
"{0} ({1} format, Encrypted: {2}, DataStart: {3})".F(filename, isCncMix ? "C&C" : "RA/TS/RA2", isEncrypted, dataStart),
|
||||
null, x => "(offs={0}, len={1})".F(x.Offset, x.Length)));
|
||||
null, x => "(offs={0}, len={1})".F(x.Offset, x.Length)), allPossibleFilenames);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@@ -65,11 +64,10 @@ namespace OpenRA.FileSystem
|
||||
}
|
||||
}
|
||||
|
||||
Dictionary<string, PackageEntry> ParseIndex(Dictionary<uint, PackageEntry> entries)
|
||||
Dictionary<string, PackageEntry> ParseIndex(Dictionary<uint, PackageEntry> entries, HashSet<string> allPossibleFilenames)
|
||||
{
|
||||
var classicIndex = new Dictionary<string, PackageEntry>();
|
||||
var crcIndex = new Dictionary<string, PackageEntry>();
|
||||
var allPossibleFilenames = new HashSet<string>();
|
||||
|
||||
// Try and find a local mix database
|
||||
var dbNameClassic = PackageEntry.HashFilename("local mix database.dat", PackageHashType.Classic);
|
||||
@@ -89,17 +87,6 @@ namespace OpenRA.FileSystem
|
||||
}
|
||||
}
|
||||
|
||||
// Load the global mix database
|
||||
// TODO: This should be passed to the mix file ctor
|
||||
if (context.Exists("global mix database.dat"))
|
||||
{
|
||||
using (var db = new XccGlobalDatabase(context.Open("global mix database.dat")))
|
||||
{
|
||||
foreach (var e in db.Entries)
|
||||
allPossibleFilenames.Add(e);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var filename in allPossibleFilenames)
|
||||
{
|
||||
var classicHash = PackageEntry.HashFilename(filename, PackageHashType.Classic);
|
||||
@@ -235,9 +222,44 @@ namespace OpenRA.FileSystem
|
||||
return index.ContainsKey(filename);
|
||||
}
|
||||
|
||||
public IReadOnlyPackage OpenPackage(string filename, FileSystem context)
|
||||
{
|
||||
IReadOnlyPackage package;
|
||||
var childStream = GetStream(filename);
|
||||
if (childStream == null)
|
||||
return null;
|
||||
|
||||
if (context.TryParsePackage(childStream, filename, out package))
|
||||
return package;
|
||||
|
||||
childStream.Dispose();
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
s.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
bool IPackageLoader.TryParsePackage(Stream s, string filename, FileSystem context, out IReadOnlyPackage package)
|
||||
{
|
||||
if (!filename.EndsWith(".mix", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
package = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load the global mix database
|
||||
Stream mixDatabase;
|
||||
var allPossibleFilenames = new HashSet<string>();
|
||||
if (context.TryOpen("global mix database.dat", out mixDatabase))
|
||||
using (var db = new XccGlobalDatabase(mixDatabase))
|
||||
foreach (var e in db.Entries)
|
||||
allPossibleFilenames.Add(e);
|
||||
|
||||
package = new MixFile(s, filename, allPossibleFilenames);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,14 @@
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace OpenRA.FileSystem
|
||||
{
|
||||
public class PakFileLoader : IPackageLoader
|
||||
{
|
||||
struct Entry
|
||||
{
|
||||
public uint Offset;
|
||||
@@ -21,23 +24,21 @@ namespace OpenRA.FileSystem
|
||||
public string Filename;
|
||||
}
|
||||
|
||||
public sealed class PakFile : IReadOnlyPackage
|
||||
sealed class PakFile : IReadOnlyPackage
|
||||
{
|
||||
public string Name { get; private set; }
|
||||
public IEnumerable<string> Contents { get { return index.Keys; } }
|
||||
|
||||
readonly Dictionary<string, Entry> index;
|
||||
readonly Dictionary<string, Entry> index = new Dictionary<string, Entry>();
|
||||
readonly Stream stream;
|
||||
|
||||
public PakFile(FileSystem context, string filename)
|
||||
public PakFile(Stream stream, string filename)
|
||||
{
|
||||
Name = filename;
|
||||
index = new Dictionary<string, Entry>();
|
||||
this.stream = stream;
|
||||
|
||||
stream = context.Open(filename);
|
||||
try
|
||||
{
|
||||
index = new Dictionary<string, Entry>();
|
||||
var offset = stream.ReadUInt32();
|
||||
while (offset != 0)
|
||||
{
|
||||
@@ -76,9 +77,28 @@ namespace OpenRA.FileSystem
|
||||
return index.ContainsKey(filename);
|
||||
}
|
||||
|
||||
public IReadOnlyPackage OpenPackage(string filename, FileSystem context)
|
||||
{
|
||||
// Not implemented
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
stream.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
bool IPackageLoader.TryParsePackage(Stream s, string filename, FileSystem context, out IReadOnlyPackage package)
|
||||
{
|
||||
if (!filename.EndsWith(".pak", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
package = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
package = new PakFile(s, filename);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System.Collections;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -19,7 +19,11 @@ using SZipFile = ICSharpCode.SharpZipLib.Zip.ZipFile;
|
||||
|
||||
namespace OpenRA.FileSystem
|
||||
{
|
||||
public sealed class ZipFile : IReadWritePackage
|
||||
public class ZipFileLoader : IPackageLoader
|
||||
{
|
||||
static readonly string[] Extensions = { ".zip", ".oramap" };
|
||||
|
||||
sealed class ZipFile : IReadWritePackage
|
||||
{
|
||||
public IReadWritePackage Parent { get; private set; }
|
||||
public string Name { get; private set; }
|
||||
@@ -44,27 +48,7 @@ namespace OpenRA.FileSystem
|
||||
pkg = new SZipFile(pkgStream);
|
||||
}
|
||||
|
||||
public ZipFile(IReadOnlyFileSystem context, string filename)
|
||||
{
|
||||
string name;
|
||||
IReadOnlyPackage p;
|
||||
if (!context.TryGetPackageContaining(filename, out p, out name))
|
||||
throw new FileNotFoundException("Unable to find parent package for " + filename);
|
||||
|
||||
Name = name;
|
||||
Parent = p as IReadWritePackage;
|
||||
|
||||
// SharpZipLib breaks when asked to update archives loaded from outside streams or files
|
||||
// We can work around this by creating a clean in-memory-only file, cutting all outside references
|
||||
pkgStream = new MemoryStream();
|
||||
using (var sourceStream = p.GetStream(name))
|
||||
sourceStream.CopyTo(pkgStream);
|
||||
pkgStream.Position = 0;
|
||||
|
||||
pkg = new SZipFile(pkgStream);
|
||||
}
|
||||
|
||||
ZipFile(string filename, IReadWritePackage parent)
|
||||
public ZipFile(string filename, IReadWritePackage parent)
|
||||
{
|
||||
pkgStream = new MemoryStream();
|
||||
|
||||
@@ -73,11 +57,6 @@ namespace OpenRA.FileSystem
|
||||
pkg = SZipFile.Create(pkgStream);
|
||||
}
|
||||
|
||||
public static ZipFile Create(string filename, IReadWritePackage parent)
|
||||
{
|
||||
return new ZipFile(filename, parent);
|
||||
}
|
||||
|
||||
public Stream GetStream(string filename)
|
||||
{
|
||||
var entry = pkg.GetEntry(filename);
|
||||
@@ -142,6 +121,88 @@ namespace OpenRA.FileSystem
|
||||
if (pkgStream != null)
|
||||
pkgStream.Dispose();
|
||||
}
|
||||
|
||||
public IReadOnlyPackage OpenPackage(string filename, FileSystem context)
|
||||
{
|
||||
// Directories are stored with a trailing "/" in the index
|
||||
var entry = pkg.GetEntry(filename) ?? pkg.GetEntry(filename + "/");
|
||||
if (entry == null)
|
||||
return null;
|
||||
|
||||
if (entry.IsDirectory)
|
||||
return new ZipFolder(this, filename);
|
||||
|
||||
if (Extensions.Any(e => filename.EndsWith(e, StringComparison.InvariantCultureIgnoreCase)))
|
||||
return new ZipFile(GetStream(filename), filename, this);
|
||||
|
||||
// Other package types can be loaded normally
|
||||
IReadOnlyPackage package;
|
||||
var s = GetStream(filename);
|
||||
if (s == null)
|
||||
return null;
|
||||
|
||||
if (context.TryParsePackage(s, filename, out package))
|
||||
return package;
|
||||
|
||||
s.Dispose();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ZipFolder : IReadOnlyPackage
|
||||
{
|
||||
public string Name { get { return path; } }
|
||||
public ZipFile Parent { get; private set; }
|
||||
readonly string path;
|
||||
|
||||
static ZipFolder()
|
||||
{
|
||||
ZipConstants.DefaultCodePage = Encoding.UTF8.CodePage;
|
||||
}
|
||||
|
||||
public ZipFolder(ZipFile parent, string path)
|
||||
{
|
||||
if (path.EndsWith("/", StringComparison.Ordinal))
|
||||
path = path.Substring(0, path.Length - 1);
|
||||
|
||||
Parent = parent;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public Stream GetStream(string filename)
|
||||
{
|
||||
// Zip files use '/' as a path separator
|
||||
return Parent.GetStream(path + '/' + filename);
|
||||
}
|
||||
|
||||
public IEnumerable<string> Contents
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach (var entry in Parent.Contents)
|
||||
{
|
||||
if (entry.StartsWith(path, StringComparison.Ordinal) && 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 IReadOnlyPackage OpenPackage(string filename, FileSystem context)
|
||||
{
|
||||
return Parent.OpenPackage(path + '/' + filename, context);
|
||||
}
|
||||
|
||||
public void Dispose() { /* nothing to do */ }
|
||||
}
|
||||
|
||||
class StaticStreamDataSource : IStaticDataSource
|
||||
@@ -157,4 +218,28 @@ namespace OpenRA.FileSystem
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryParsePackage(Stream s, string filename, FileSystem context, out IReadOnlyPackage package)
|
||||
{
|
||||
if (!Extensions.Any(e => filename.EndsWith(e, StringComparison.InvariantCultureIgnoreCase)))
|
||||
{
|
||||
package = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
string name;
|
||||
IReadOnlyPackage p;
|
||||
if (context.TryGetPackageContaining(filename, out p, out name))
|
||||
package = new ZipFile(p.GetStream(name), name, p);
|
||||
else
|
||||
package = new ZipFile(s, filename, null);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static IReadWritePackage Create(string filename, IReadWritePackage parent)
|
||||
{
|
||||
return new ZipFile(filename, parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
#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.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<string> 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 */ }
|
||||
}
|
||||
}
|
||||
@@ -57,12 +57,13 @@ namespace OpenRA
|
||||
|
||||
public readonly string[] SoundFormats = { };
|
||||
public readonly string[] SpriteFormats = { };
|
||||
public readonly string[] PackageFormats = { };
|
||||
|
||||
readonly string[] reservedModuleNames = { "Metadata", "Folders", "MapFolders", "Packages", "Rules",
|
||||
"Sequences", "VoxelSequences", "Cursors", "Chrome", "Assemblies", "ChromeLayout", "Weapons",
|
||||
"Voices", "Notifications", "Music", "Translations", "TileSets", "ChromeMetrics", "Missions",
|
||||
"ServerTraits", "LoadScreen", "Fonts", "SupportsMapsFrom", "SoundFormats", "SpriteFormats",
|
||||
"RequiresMods" };
|
||||
"RequiresMods", "PackageFormats" };
|
||||
|
||||
readonly TypeDictionary modules = new TypeDictionary();
|
||||
readonly Dictionary<string, MiniYaml> yaml;
|
||||
@@ -119,6 +120,9 @@ namespace OpenRA
|
||||
|
||||
MapCompatibility = compat.ToArray();
|
||||
|
||||
if (yaml.ContainsKey("PackageFormats"))
|
||||
PackageFormats = FieldLoader.GetValue<string[]>("PackageFormats", yaml["PackageFormats"].Value);
|
||||
|
||||
if (yaml.ContainsKey("SoundFormats"))
|
||||
SoundFormats = FieldLoader.GetValue<string[]>("SoundFormats", yaml["SoundFormats"].Value);
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using OpenRA.FileSystem;
|
||||
using OpenRA.Graphics;
|
||||
using OpenRA.Widgets;
|
||||
@@ -27,6 +26,7 @@ namespace OpenRA
|
||||
public readonly ObjectCreator ObjectCreator;
|
||||
public readonly WidgetLoader WidgetLoader;
|
||||
public readonly MapCache MapCache;
|
||||
public readonly IPackageLoader[] PackageLoaders;
|
||||
public readonly ISoundLoader[] SoundLoaders;
|
||||
public readonly ISpriteLoader[] SpriteLoaders;
|
||||
public readonly ISpriteSequenceLoader SpriteSequenceLoader;
|
||||
@@ -49,12 +49,12 @@ namespace OpenRA
|
||||
{
|
||||
Languages = new string[0];
|
||||
|
||||
|
||||
// Take a local copy of the manifest
|
||||
Manifest = new Manifest(mod.Id, mod.Package);
|
||||
ObjectCreator = new ObjectCreator(Manifest, mods);
|
||||
PackageLoaders = ObjectCreator.GetLoaders<IPackageLoader>(Manifest.PackageFormats, "package");
|
||||
|
||||
ModFiles = new FS(mods);
|
||||
ModFiles = new FS(mods, PackageLoaders);
|
||||
ModFiles.LoadFromManifest(Manifest);
|
||||
Manifest.LoadCustomData(ObjectCreator);
|
||||
|
||||
|
||||
@@ -254,7 +254,6 @@
|
||||
<Compile Include="Graphics\RgbaColorRenderer.cs" />
|
||||
<Compile Include="Traits\Player\IndexedPlayerPalette.cs" />
|
||||
<Compile Include="Traits\ActivityUtils.cs" />
|
||||
<Compile Include="FileSystem\ZipFolder.cs" />
|
||||
<Compile Include="Primitives\float3.cs" />
|
||||
<Compile Include="InstalledMods.cs" />
|
||||
<Compile Include="CryptoUtil.cs" />
|
||||
|
||||
@@ -271,7 +271,7 @@ namespace OpenRA.Mods.Cnc.UtilityCommands
|
||||
map.PlayerDefinitions = mapPlayers.ToMiniYaml();
|
||||
|
||||
var dest = Path.GetFileNameWithoutExtension(args[1]) + ".oramap";
|
||||
map.Save(ZipFile.Create(dest, new Folder(".")));
|
||||
map.Save(ZipFileLoader.Create(dest, new Folder(".")));
|
||||
Console.WriteLine(dest + " saved.");
|
||||
}
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ namespace OpenRA.Mods.Common.UtilityCommands
|
||||
|
||||
var dest = Path.GetFileNameWithoutExtension(args[1]) + ".oramap";
|
||||
|
||||
Map.Save(ZipFile.Create(dest, new Folder(".")));
|
||||
Map.Save(ZipFileLoader.Create(dest, new Folder(".")));
|
||||
Console.WriteLine(dest + " saved.");
|
||||
}
|
||||
|
||||
|
||||
@@ -27,13 +27,7 @@ namespace OpenRA.Mods.Common.UtilityCommands
|
||||
[Desc("ARCHIVE.Z", "Lists the content ranges for a InstallShield V3 file")]
|
||||
void IUtilityCommand.Run(Utility utility, string[] args)
|
||||
{
|
||||
var filename = Path.GetFileName(args[1]);
|
||||
var path = Path.GetDirectoryName(args[1]);
|
||||
|
||||
var fs = new FileSystem.FileSystem(utility.Mods);
|
||||
fs.Mount(path, "parent");
|
||||
var package = new InstallShieldPackage(fs, "parent|" + filename);
|
||||
|
||||
var package = new InstallShieldLoader.InstallShieldPackage(File.OpenRead(args[1]), args[1]);
|
||||
foreach (var kv in package.Index)
|
||||
{
|
||||
Console.WriteLine("{0}:", kv.Key);
|
||||
|
||||
@@ -10,8 +10,10 @@
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using OpenRA.FileFormats;
|
||||
using OpenRA.FileSystem;
|
||||
|
||||
namespace OpenRA.Mods.Common.UtilityCommands
|
||||
@@ -22,23 +24,18 @@ namespace OpenRA.Mods.Common.UtilityCommands
|
||||
|
||||
bool IUtilityCommand.ValidateArguments(string[] args)
|
||||
{
|
||||
return args.Length == 2;
|
||||
return args.Length == 3;
|
||||
}
|
||||
|
||||
[Desc("ARCHIVE.MIX", "Lists the content ranges for a mix file")]
|
||||
[Desc("ARCHIVE.MIX", "MIXDATABASE.DAT", "Lists the content ranges for a mix file")]
|
||||
void IUtilityCommand.Run(Utility utility, string[] args)
|
||||
{
|
||||
var filename = Path.GetFileName(args[1]);
|
||||
var path = Path.GetDirectoryName(args[1]);
|
||||
|
||||
var fs = new FileSystem.FileSystem(utility.Mods);
|
||||
|
||||
// Needed to access the global mix database
|
||||
fs.LoadFromManifest(utility.ModData.Manifest);
|
||||
|
||||
fs.Mount(path, "parent");
|
||||
var package = new MixFile(fs, "parent|" + filename);
|
||||
var allPossibleFilenames = new HashSet<string>();
|
||||
using (var db = new XccGlobalDatabase(File.OpenRead(args[2])))
|
||||
foreach (var e in db.Entries)
|
||||
allPossibleFilenames.Add(e);
|
||||
|
||||
var package = new MixLoader.MixFile(File.OpenRead(args[1]), args[1], allPossibleFilenames);
|
||||
foreach (var kv in package.Index.OrderBy(kv => kv.Value.Offset))
|
||||
{
|
||||
Console.WriteLine("{0}:", kv.Key);
|
||||
|
||||
@@ -123,7 +123,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
||||
directoryDropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 210, writableDirectories, setupItem);
|
||||
}
|
||||
|
||||
var mapIsUnpacked = map.Package != null && (map.Package is Folder || map.Package is ZipFolder);
|
||||
var mapIsUnpacked = map.Package != null && map.Package is Folder;
|
||||
|
||||
var filename = widget.Get<TextFieldWidget>("FILENAME");
|
||||
filename.Text = map.Package == null ? "" : mapIsUnpacked ? Path.GetFileName(map.Package.Name) : Path.GetFileNameWithoutExtension(map.Package.Name);
|
||||
@@ -185,7 +185,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
||||
{
|
||||
selectedDirectory.Folder.Delete(combinedPath);
|
||||
if (fileType == MapFileType.OraMap)
|
||||
package = ZipFile.Create(combinedPath, selectedDirectory.Folder);
|
||||
package = ZipFileLoader.Create(combinedPath, selectedDirectory.Folder);
|
||||
else
|
||||
package = new Folder(combinedPath);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenRA.FileSystem;
|
||||
using OpenRA.Widgets;
|
||||
|
||||
namespace OpenRA.Mods.Common.Widgets.Logic
|
||||
@@ -28,13 +29,15 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
||||
bool discAvailable;
|
||||
|
||||
[ObjectCreator.UseCtor]
|
||||
public ModContentLogic(Widget widget, Manifest mod, ModContent content, Action onCancel)
|
||||
public ModContentLogic(Widget widget, ModData modData, Manifest mod, ModContent content, Action onCancel)
|
||||
{
|
||||
this.content = content;
|
||||
|
||||
var panel = widget.Get("CONTENT_PANEL");
|
||||
|
||||
var modFileSystem = new FileSystem.FileSystem(Game.Mods);
|
||||
var modObjectCreator = new ObjectCreator(mod, Game.Mods);
|
||||
var modPackageLoaders = modObjectCreator.GetLoaders<IPackageLoader>(mod.PackageFormats, "package");
|
||||
var modFileSystem = new FileSystem.FileSystem(Game.Mods, modPackageLoaders);
|
||||
modFileSystem.LoadFromManifest(mod);
|
||||
|
||||
var sourceYaml = MiniYaml.Load(modFileSystem, content.Sources, null);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using OpenRA.FileSystem;
|
||||
using OpenRA.Widgets;
|
||||
|
||||
namespace OpenRA.Mods.Common.Widgets.Logic
|
||||
@@ -18,7 +19,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
||||
public class ModContentPromptLogic : ChromeLogic
|
||||
{
|
||||
[ObjectCreator.UseCtor]
|
||||
public ModContentPromptLogic(Widget widget, Manifest mod, ModContent content, Action continueLoading)
|
||||
public ModContentPromptLogic(Widget widget, ModData modData, Manifest mod, ModContent content, Action continueLoading)
|
||||
{
|
||||
var panel = widget.Get("CONTENT_PROMPT_PANEL");
|
||||
|
||||
@@ -55,8 +56,11 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
||||
quickButton.Bounds.Y += headerHeight;
|
||||
quickButton.OnClick = () =>
|
||||
{
|
||||
var modFileSystem = new FileSystem.FileSystem(Game.Mods);
|
||||
var modObjectCreator = new ObjectCreator(mod, Game.Mods);
|
||||
var modPackageLoaders = modObjectCreator.GetLoaders<IPackageLoader>(mod.PackageFormats, "package");
|
||||
var modFileSystem = new FileSystem.FileSystem(Game.Mods, modPackageLoaders);
|
||||
modFileSystem.LoadFromManifest(mod);
|
||||
|
||||
var downloadYaml = MiniYaml.Load(modFileSystem, content.Downloads, null);
|
||||
modFileSystem.UnmountAll();
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace OpenRA.Mods.D2k.UtilityCommands
|
||||
return;
|
||||
|
||||
var dest = Path.GetFileNameWithoutExtension(args[1]) + ".oramap";
|
||||
map.Save(ZipFile.Create(dest, new Folder(".")));
|
||||
map.Save(ZipFileLoader.Create(dest, new Folder(".")));
|
||||
Console.WriteLine(dest + " saved.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ Metadata:
|
||||
Title: Tiberian Dawn
|
||||
Version: {DEV_VERSION}
|
||||
|
||||
PackageFormats: Mix
|
||||
|
||||
Packages:
|
||||
~^Content/cnc
|
||||
~^Content/cnc/movies
|
||||
|
||||
@@ -2,6 +2,8 @@ Metadata:
|
||||
Title: Dune 2000
|
||||
Version: {DEV_VERSION}
|
||||
|
||||
PackageFormats: D2kSoundResources
|
||||
|
||||
Packages:
|
||||
~^Content/d2k/v2/
|
||||
~^Content/d2k/v2/GAMESFX
|
||||
|
||||
@@ -2,6 +2,8 @@ Metadata:
|
||||
Title: Red Alert
|
||||
Version: {DEV_VERSION}
|
||||
|
||||
PackageFormats: Mix
|
||||
|
||||
Packages:
|
||||
~^Content/ra/v2/
|
||||
~^Content/ra/v2/expand
|
||||
|
||||
@@ -2,6 +2,8 @@ Metadata:
|
||||
Title: Tiberian Sun
|
||||
Version: {DEV_VERSION}
|
||||
|
||||
PackageFormats: Mix
|
||||
|
||||
Packages:
|
||||
~^Content/ts
|
||||
~^Content/ts/firestorm
|
||||
|
||||
Reference in New Issue
Block a user