Implement mod-defined package loaders.
This commit is contained in:
@@ -11,117 +11,143 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using OpenRA.FileFormats;
|
using OpenRA.FileFormats;
|
||||||
using OpenRA.Primitives;
|
using OpenRA.Primitives;
|
||||||
|
|
||||||
namespace OpenRA.FileSystem
|
namespace OpenRA.FileSystem
|
||||||
{
|
{
|
||||||
public sealed class BagFile : IReadOnlyPackage
|
public class AudioBagLoader : IPackageLoader
|
||||||
{
|
{
|
||||||
public string Name { get; private set; }
|
sealed class BagFile : IReadOnlyPackage
|
||||||
public IEnumerable<string> Contents { get { return index.Keys; } }
|
|
||||||
|
|
||||||
readonly Stream s;
|
|
||||||
readonly Dictionary<string, IdxEntry> index;
|
|
||||||
|
|
||||||
public BagFile(FileSystem context, string filename)
|
|
||||||
{
|
{
|
||||||
Name = filename;
|
public string Name { get; private set; }
|
||||||
|
public IEnumerable<string> Contents { get { return index.Keys; } }
|
||||||
|
|
||||||
|
readonly Stream s;
|
||||||
|
readonly Dictionary<string, IdxEntry> index;
|
||||||
|
|
||||||
|
public BagFile(Stream s, List<IdxEntry> entries, string filename)
|
||||||
|
{
|
||||||
|
Name = filename;
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stream GetStream(string filename)
|
||||||
|
{
|
||||||
|
IdxEntry entry;
|
||||||
|
if (!index.TryGetValue(filename, out entry))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
s.Seek(entry.Offset, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
var waveHeaderMemoryStream = new MemoryStream();
|
||||||
|
|
||||||
|
var channels = (entry.Flags & 1) > 0 ? 2 : 1;
|
||||||
|
|
||||||
|
if ((entry.Flags & 2) > 0)
|
||||||
|
{
|
||||||
|
// PCM
|
||||||
|
waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("RIFF"));
|
||||||
|
waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.Length + 36));
|
||||||
|
waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("WAVE"));
|
||||||
|
waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("fmt "));
|
||||||
|
waveHeaderMemoryStream.Write(BitConverter.GetBytes(16));
|
||||||
|
waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)1));
|
||||||
|
waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)channels));
|
||||||
|
waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.SampleRate));
|
||||||
|
waveHeaderMemoryStream.Write(BitConverter.GetBytes(2 * channels * entry.SampleRate));
|
||||||
|
waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)(2 * channels)));
|
||||||
|
waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)16));
|
||||||
|
waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("data"));
|
||||||
|
waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.Length));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((entry.Flags & 8) > 0)
|
||||||
|
{
|
||||||
|
// IMA ADPCM
|
||||||
|
var samplesPerChunk = (2 * (entry.ChunkSize - 4)) + 1;
|
||||||
|
var bytesPerSec = (int)Math.Floor(((double)(2 * entry.ChunkSize) / samplesPerChunk) * ((double)entry.SampleRate / 2));
|
||||||
|
var chunkSize = entry.ChunkSize > entry.Length ? entry.Length : entry.ChunkSize;
|
||||||
|
|
||||||
|
waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("RIFF"));
|
||||||
|
waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.Length + 52));
|
||||||
|
waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("WAVE"));
|
||||||
|
waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("fmt "));
|
||||||
|
waveHeaderMemoryStream.Write(BitConverter.GetBytes(20));
|
||||||
|
waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)17));
|
||||||
|
waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)channels));
|
||||||
|
waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.SampleRate));
|
||||||
|
waveHeaderMemoryStream.Write(BitConverter.GetBytes(bytesPerSec));
|
||||||
|
waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)chunkSize));
|
||||||
|
waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)4));
|
||||||
|
waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)2));
|
||||||
|
waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)samplesPerChunk));
|
||||||
|
waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("fact"));
|
||||||
|
waveHeaderMemoryStream.Write(BitConverter.GetBytes(4));
|
||||||
|
waveHeaderMemoryStream.Write(BitConverter.GetBytes(4 * entry.Length));
|
||||||
|
waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("data"));
|
||||||
|
waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.Length));
|
||||||
|
}
|
||||||
|
|
||||||
|
waveHeaderMemoryStream.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
// Construct a merged stream
|
||||||
|
var mergedStream = new MergedStream(waveHeaderMemoryStream, s);
|
||||||
|
mergedStream.SetLength(waveHeaderMemoryStream.Length + entry.Length);
|
||||||
|
|
||||||
|
return mergedStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Contains(string filename)
|
||||||
|
{
|
||||||
|
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
|
// A bag file is always accompanied with an .idx counterpart
|
||||||
// For example: audio.bag requires the audio.idx file
|
// For example: audio.bag requires the audio.idx file
|
||||||
var indexFilename = Path.ChangeExtension(filename, ".idx");
|
var indexFilename = Path.ChangeExtension(filename, ".idx");
|
||||||
|
List<IdxEntry> entries = null;
|
||||||
|
|
||||||
// Build the index and dispose the stream, it is no longer needed after this
|
try
|
||||||
List<IdxEntry> entries;
|
|
||||||
using (var indexStream = context.Open(indexFilename))
|
|
||||||
entries = new IdxReader(indexStream).Entries;
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
IdxEntry entry;
|
|
||||||
if (!index.TryGetValue(filename, out entry))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
s.Seek(entry.Offset, SeekOrigin.Begin);
|
|
||||||
|
|
||||||
var waveHeaderMemoryStream = new MemoryStream();
|
|
||||||
|
|
||||||
var channels = (entry.Flags & 1) > 0 ? 2 : 1;
|
|
||||||
|
|
||||||
if ((entry.Flags & 2) > 0)
|
|
||||||
{
|
{
|
||||||
// PCM
|
// Build the index and dispose the stream, it is no longer needed after this
|
||||||
waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("RIFF"));
|
using (var indexStream = context.Open(indexFilename))
|
||||||
waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.Length + 36));
|
entries = new IdxReader(indexStream).Entries;
|
||||||
waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("WAVE"));
|
}
|
||||||
waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("fmt "));
|
catch
|
||||||
waveHeaderMemoryStream.Write(BitConverter.GetBytes(16));
|
{
|
||||||
waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)1));
|
package = null;
|
||||||
waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)channels));
|
return false;
|
||||||
waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.SampleRate));
|
|
||||||
waveHeaderMemoryStream.Write(BitConverter.GetBytes(2 * channels * entry.SampleRate));
|
|
||||||
waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)(2 * channels)));
|
|
||||||
waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)16));
|
|
||||||
waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("data"));
|
|
||||||
waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.Length));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((entry.Flags & 8) > 0)
|
package = new BagFile(s, entries, filename);
|
||||||
{
|
return true;
|
||||||
// IMA ADPCM
|
|
||||||
var samplesPerChunk = (2 * (entry.ChunkSize - 4)) + 1;
|
|
||||||
var bytesPerSec = (int)Math.Floor(((double)(2 * entry.ChunkSize) / samplesPerChunk) * ((double)entry.SampleRate / 2));
|
|
||||||
var chunkSize = entry.ChunkSize > entry.Length ? entry.Length : entry.ChunkSize;
|
|
||||||
|
|
||||||
waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("RIFF"));
|
|
||||||
waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.Length + 52));
|
|
||||||
waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("WAVE"));
|
|
||||||
waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("fmt "));
|
|
||||||
waveHeaderMemoryStream.Write(BitConverter.GetBytes(20));
|
|
||||||
waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)17));
|
|
||||||
waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)channels));
|
|
||||||
waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.SampleRate));
|
|
||||||
waveHeaderMemoryStream.Write(BitConverter.GetBytes(bytesPerSec));
|
|
||||||
waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)chunkSize));
|
|
||||||
waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)4));
|
|
||||||
waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)2));
|
|
||||||
waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)samplesPerChunk));
|
|
||||||
waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("fact"));
|
|
||||||
waveHeaderMemoryStream.Write(BitConverter.GetBytes(4));
|
|
||||||
waveHeaderMemoryStream.Write(BitConverter.GetBytes(4 * entry.Length));
|
|
||||||
waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("data"));
|
|
||||||
waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.Length));
|
|
||||||
}
|
|
||||||
|
|
||||||
waveHeaderMemoryStream.Seek(0, SeekOrigin.Begin);
|
|
||||||
|
|
||||||
// Construct a merged stream
|
|
||||||
var mergedStream = new MergedStream(waveHeaderMemoryStream, s);
|
|
||||||
mergedStream.SetLength(waveHeaderMemoryStream.Length + entry.Length);
|
|
||||||
|
|
||||||
return mergedStream;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Contains(string filename)
|
|
||||||
{
|
|
||||||
return index.ContainsKey(filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
s.Dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,94 +12,117 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace OpenRA.FileSystem
|
namespace OpenRA.FileSystem
|
||||||
{
|
{
|
||||||
public sealed class BigFile : IReadOnlyPackage
|
public class BigLoader : IPackageLoader
|
||||||
{
|
{
|
||||||
public string Name { get; private set; }
|
sealed class BigFile : IReadOnlyPackage
|
||||||
public IEnumerable<string> Contents { get { return index.Keys; } }
|
|
||||||
|
|
||||||
readonly Dictionary<string, Entry> index = new Dictionary<string, Entry>();
|
|
||||||
readonly Stream s;
|
|
||||||
|
|
||||||
public BigFile(FileSystem context, string filename)
|
|
||||||
{
|
{
|
||||||
Name = filename;
|
public string Name { get; private set; }
|
||||||
|
public IEnumerable<string> Contents { get { return index.Keys; } }
|
||||||
|
|
||||||
s = context.Open(filename);
|
readonly Dictionary<string, Entry> index = new Dictionary<string, Entry>();
|
||||||
try
|
|
||||||
{
|
|
||||||
if (s.ReadASCII(4) != "BIGF")
|
|
||||||
throw new InvalidDataException("Header is not BIGF");
|
|
||||||
|
|
||||||
// Total archive size.
|
|
||||||
s.ReadUInt32();
|
|
||||||
|
|
||||||
var entryCount = s.ReadUInt32();
|
|
||||||
if (BitConverter.IsLittleEndian)
|
|
||||||
entryCount = int2.Swap(entryCount);
|
|
||||||
|
|
||||||
// First entry offset? This is apparently bogus for EA's .big files
|
|
||||||
// and we don't have to try seeking there since the entries typically start next in EA's .big files.
|
|
||||||
s.ReadUInt32();
|
|
||||||
|
|
||||||
for (var i = 0; i < entryCount; i++)
|
|
||||||
{
|
|
||||||
var entry = new Entry(s);
|
|
||||||
index.Add(entry.Path, entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
Dispose();
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Entry
|
|
||||||
{
|
|
||||||
readonly Stream s;
|
readonly Stream s;
|
||||||
readonly uint offset;
|
|
||||||
readonly uint size;
|
|
||||||
public readonly string Path;
|
|
||||||
|
|
||||||
public Entry(Stream s)
|
public BigFile(Stream s, string filename)
|
||||||
{
|
{
|
||||||
|
Name = filename;
|
||||||
this.s = s;
|
this.s = s;
|
||||||
|
|
||||||
offset = s.ReadUInt32();
|
try
|
||||||
size = s.ReadUInt32();
|
|
||||||
if (BitConverter.IsLittleEndian)
|
|
||||||
{
|
{
|
||||||
offset = int2.Swap(offset);
|
/* var signature = */ s.ReadASCII(4);
|
||||||
size = int2.Swap(size);
|
|
||||||
|
// Total archive size.
|
||||||
|
s.ReadUInt32();
|
||||||
|
|
||||||
|
var entryCount = s.ReadUInt32();
|
||||||
|
if (BitConverter.IsLittleEndian)
|
||||||
|
entryCount = int2.Swap(entryCount);
|
||||||
|
|
||||||
|
// First entry offset? This is apparently bogus for EA's .big files
|
||||||
|
// and we don't have to try seeking there since the entries typically start next in EA's .big files.
|
||||||
|
s.ReadUInt32();
|
||||||
|
|
||||||
|
for (var i = 0; i < entryCount; i++)
|
||||||
|
{
|
||||||
|
var entry = new Entry(s);
|
||||||
|
index.Add(entry.Path, entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Dispose();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Entry
|
||||||
|
{
|
||||||
|
readonly Stream s;
|
||||||
|
readonly uint offset;
|
||||||
|
readonly uint size;
|
||||||
|
public readonly string Path;
|
||||||
|
|
||||||
|
public Entry(Stream s)
|
||||||
|
{
|
||||||
|
this.s = s;
|
||||||
|
|
||||||
|
offset = s.ReadUInt32();
|
||||||
|
size = s.ReadUInt32();
|
||||||
|
if (BitConverter.IsLittleEndian)
|
||||||
|
{
|
||||||
|
offset = int2.Swap(offset);
|
||||||
|
size = int2.Swap(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
Path = s.ReadASCIIZ();
|
||||||
}
|
}
|
||||||
|
|
||||||
Path = s.ReadASCIIZ();
|
public Stream GetData()
|
||||||
|
{
|
||||||
|
s.Position = offset;
|
||||||
|
return new MemoryStream(s.ReadBytes((int)size));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Stream GetData()
|
public Stream GetStream(string filename)
|
||||||
{
|
{
|
||||||
s.Position = offset;
|
return index[filename].GetData();
|
||||||
return new MemoryStream(s.ReadBytes((int)size));
|
}
|
||||||
|
|
||||||
|
public bool Contains(string filename)
|
||||||
|
{
|
||||||
|
return index.ContainsKey(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyPackage OpenPackage(string filename, FileSystem context)
|
||||||
|
{
|
||||||
|
// Not implemented
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
s.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Stream GetStream(string filename)
|
bool IPackageLoader.TryParsePackage(Stream s, string filename, FileSystem context, out IReadOnlyPackage package)
|
||||||
{
|
{
|
||||||
return index[filename].GetData();
|
// Take a peek at the file signature
|
||||||
}
|
var signature = s.ReadASCII(4);
|
||||||
|
s.Position -= 4;
|
||||||
|
|
||||||
public bool Contains(string filename)
|
if (signature != "BIGF")
|
||||||
{
|
{
|
||||||
return index.ContainsKey(filename);
|
package = null;
|
||||||
}
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
package = new BigFile(s, filename);
|
||||||
{
|
return true;
|
||||||
s.Dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,73 +9,94 @@
|
|||||||
*/
|
*/
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using OpenRA.Primitives;
|
|
||||||
|
|
||||||
namespace OpenRA.FileSystem
|
namespace OpenRA.FileSystem
|
||||||
{
|
{
|
||||||
public sealed class D2kSoundResources : IReadOnlyPackage
|
public class D2kSoundResourcesLoader : IPackageLoader
|
||||||
{
|
{
|
||||||
struct Entry
|
sealed class D2kSoundResources : IReadOnlyPackage
|
||||||
{
|
{
|
||||||
public readonly uint Offset;
|
struct Entry
|
||||||
public readonly uint Length;
|
|
||||||
|
|
||||||
public Entry(uint offset, uint length)
|
|
||||||
{
|
{
|
||||||
Offset = offset;
|
public readonly uint Offset;
|
||||||
Length = length;
|
public readonly uint Length;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Name { get; private set; }
|
public Entry(uint offset, uint length)
|
||||||
public IEnumerable<string> Contents { get { return index.Keys; } }
|
|
||||||
|
|
||||||
readonly Stream s;
|
|
||||||
readonly Dictionary<string, Entry> index = new Dictionary<string, Entry>();
|
|
||||||
|
|
||||||
public D2kSoundResources(FileSystem context, string filename)
|
|
||||||
{
|
|
||||||
Name = filename;
|
|
||||||
|
|
||||||
s = context.Open(filename);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var headerLength = s.ReadUInt32();
|
|
||||||
while (s.Position < headerLength + 4)
|
|
||||||
{
|
{
|
||||||
var name = s.ReadASCIIZ();
|
Offset = offset;
|
||||||
var offset = s.ReadUInt32();
|
Length = length;
|
||||||
var length = s.ReadUInt32();
|
|
||||||
index.Add(name, new Entry(offset, length));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
|
||||||
|
public string Name { get; private set; }
|
||||||
|
public IEnumerable<string> Contents { get { return index.Keys; } }
|
||||||
|
|
||||||
|
readonly Stream s;
|
||||||
|
readonly Dictionary<string, Entry> index = new Dictionary<string, Entry>();
|
||||||
|
|
||||||
|
public D2kSoundResources(Stream s, string filename)
|
||||||
{
|
{
|
||||||
Dispose();
|
Name = filename;
|
||||||
throw;
|
this.s = s;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var headerLength = s.ReadUInt32();
|
||||||
|
while (s.Position < headerLength + 4)
|
||||||
|
{
|
||||||
|
var name = s.ReadASCIIZ();
|
||||||
|
var offset = s.ReadUInt32();
|
||||||
|
var length = s.ReadUInt32();
|
||||||
|
index.Add(name, new Entry(offset, length));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Dispose();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stream GetStream(string filename)
|
||||||
|
{
|
||||||
|
Entry e;
|
||||||
|
if (!index.TryGetValue(filename, out e))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
s.Seek(e.Offset, SeekOrigin.Begin);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
s.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Stream GetStream(string filename)
|
bool IPackageLoader.TryParsePackage(Stream s, string filename, FileSystem context, out IReadOnlyPackage package)
|
||||||
{
|
{
|
||||||
Entry e;
|
if (!filename.EndsWith(".rs", StringComparison.InvariantCultureIgnoreCase))
|
||||||
if (!index.TryGetValue(filename, out e))
|
{
|
||||||
return null;
|
package = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
s.Seek(e.Offset, SeekOrigin.Begin);
|
package = new D2kSoundResources(s, filename);
|
||||||
return new MemoryStream(s.ReadBytes((int)e.Length));
|
return true;
|
||||||
}
|
|
||||||
|
|
||||||
public bool Contains(string filename)
|
|
||||||
{
|
|
||||||
return index.ContainsKey(filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
s.Dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,80 +34,69 @@ namespace OpenRA.FileSystem
|
|||||||
// Mod packages that should not be disposed
|
// Mod packages that should not be disposed
|
||||||
readonly List<IReadOnlyPackage> modPackages = new List<IReadOnlyPackage>();
|
readonly List<IReadOnlyPackage> modPackages = new List<IReadOnlyPackage>();
|
||||||
readonly IReadOnlyDictionary<string, Manifest> installedMods;
|
readonly IReadOnlyDictionary<string, Manifest> installedMods;
|
||||||
|
readonly IPackageLoader[] packageLoaders;
|
||||||
|
|
||||||
Cache<string, List<IReadOnlyPackage>> fileIndex = new Cache<string, List<IReadOnlyPackage>>(_ => new List<IReadOnlyPackage>());
|
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.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)
|
public IReadOnlyPackage OpenPackage(string filename)
|
||||||
{
|
{
|
||||||
if (filename.EndsWith(".mix", StringComparison.InvariantCultureIgnoreCase))
|
// Raw directories are the easiest and one of the most common cases, so try these first
|
||||||
return new MixFile(this, filename);
|
var resolvedPath = Platform.ResolvePath(filename);
|
||||||
if (filename.EndsWith(".zip", StringComparison.InvariantCultureIgnoreCase))
|
if (!filename.Contains("|") && Directory.Exists(resolvedPath))
|
||||||
return new ZipFile(this, filename);
|
return new Folder(resolvedPath);
|
||||||
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);
|
|
||||||
|
|
||||||
|
// Children of another package require special handling
|
||||||
IReadOnlyPackage parent;
|
IReadOnlyPackage parent;
|
||||||
string subPath = null;
|
string subPath = null;
|
||||||
if (TryGetPackageContaining(filename, out parent, out subPath))
|
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)
|
// No package loaders took ownership of the stream, so clean it up
|
||||||
{
|
stream.Dispose();
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
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)
|
public void Mount(string name, string explicitName = null)
|
||||||
{
|
{
|
||||||
var optional = name.StartsWith("~");
|
var optional = name.StartsWith("~", StringComparison.Ordinal);
|
||||||
if (optional)
|
if (optional)
|
||||||
name = name.Substring(1);
|
name = name.Substring(1);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
IReadOnlyPackage package;
|
IReadOnlyPackage package;
|
||||||
if (name.StartsWith("$"))
|
if (name.StartsWith("$", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
name = name.Substring(1);
|
name = name.Substring(1);
|
||||||
|
|
||||||
|
|||||||
@@ -60,13 +60,16 @@ namespace OpenRA.FileSystem
|
|||||||
// Other package types can be loaded normally
|
// Other package types can be loaded normally
|
||||||
IReadOnlyPackage package;
|
IReadOnlyPackage package;
|
||||||
var s = GetStream(filename);
|
var s = GetStream(filename);
|
||||||
|
if (s == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
if (context.TryParsePackage(s, filename, out package))
|
if (context.TryParsePackage(s, filename, out package))
|
||||||
return package;
|
return package;
|
||||||
|
|
||||||
s.Dispose();
|
s.Dispose();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Update(string filename, byte[] contents)
|
public void Update(string filename, byte[] contents)
|
||||||
{
|
{
|
||||||
// HACK: ZipFiles can't be loaded as read-write from a stream, so we are
|
// HACK: ZipFiles can't be loaded as read-write from a stream, so we are
|
||||||
|
|||||||
@@ -15,12 +15,23 @@ using System.IO;
|
|||||||
|
|
||||||
namespace OpenRA.FileSystem
|
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
|
public interface IReadOnlyPackage : IDisposable
|
||||||
{
|
{
|
||||||
string Name { get; }
|
string Name { get; }
|
||||||
IEnumerable<string> Contents { get; }
|
IEnumerable<string> Contents { get; }
|
||||||
Stream GetStream(string filename);
|
Stream GetStream(string filename);
|
||||||
bool Contains(string filename);
|
bool Contains(string filename);
|
||||||
|
IReadOnlyPackage OpenPackage(string filename, FileSystem context);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IReadWritePackage : IReadOnlyPackage
|
public interface IReadWritePackage : IReadOnlyPackage
|
||||||
|
|||||||
@@ -16,122 +16,144 @@ using OpenRA.FileFormats;
|
|||||||
|
|
||||||
namespace OpenRA.FileSystem
|
namespace OpenRA.FileSystem
|
||||||
{
|
{
|
||||||
public sealed class InstallShieldPackage : IReadOnlyPackage
|
public class InstallShieldLoader : IPackageLoader
|
||||||
{
|
{
|
||||||
public struct Entry
|
public sealed class InstallShieldPackage : IReadOnlyPackage
|
||||||
{
|
{
|
||||||
public readonly uint Offset;
|
public struct Entry
|
||||||
public readonly uint Length;
|
|
||||||
|
|
||||||
public Entry(uint offset, uint length)
|
|
||||||
{
|
{
|
||||||
Offset = offset;
|
public readonly uint Offset;
|
||||||
Length = length;
|
public readonly uint Length;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Name { get; private set; }
|
public Entry(uint offset, uint length)
|
||||||
public IEnumerable<string> Contents { get { return index.Keys; } }
|
|
||||||
|
|
||||||
readonly Dictionary<string, Entry> index = new Dictionary<string, Entry>();
|
|
||||||
readonly Stream s;
|
|
||||||
readonly long dataStart = 255;
|
|
||||||
|
|
||||||
public InstallShieldPackage(FileSystem context, string filename)
|
|
||||||
{
|
|
||||||
Name = filename;
|
|
||||||
|
|
||||||
s = context.Open(filename);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Parse package header
|
|
||||||
var signature = s.ReadUInt32();
|
|
||||||
if (signature != 0x8C655D13)
|
|
||||||
throw new InvalidDataException("Not an Installshield package");
|
|
||||||
|
|
||||||
s.Position += 8;
|
|
||||||
/*var FileCount = */s.ReadUInt16();
|
|
||||||
s.Position += 4;
|
|
||||||
/*var ArchiveSize = */s.ReadUInt32();
|
|
||||||
s.Position += 19;
|
|
||||||
var tocAddress = s.ReadInt32();
|
|
||||||
s.Position += 4;
|
|
||||||
var dirCount = s.ReadUInt16();
|
|
||||||
|
|
||||||
// Parse the directory list
|
|
||||||
s.Position = tocAddress;
|
|
||||||
|
|
||||||
// Parse directories
|
|
||||||
var directories = new Dictionary<string, uint>();
|
|
||||||
for (var i = 0; i < dirCount; i++)
|
|
||||||
{
|
{
|
||||||
// Parse directory header
|
Offset = offset;
|
||||||
var fileCount = s.ReadUInt16();
|
Length = length;
|
||||||
var chunkSize = s.ReadUInt16();
|
|
||||||
var nameLength = s.ReadUInt16();
|
|
||||||
var dirName = s.ReadASCII(nameLength);
|
|
||||||
|
|
||||||
// Skip to the end of the chunk
|
|
||||||
s.ReadBytes(chunkSize - nameLength - 6);
|
|
||||||
directories.Add(dirName, fileCount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse files
|
|
||||||
foreach (var dir in directories)
|
|
||||||
for (var i = 0; i < dir.Value; i++)
|
|
||||||
ParseFile(s, dir.Key);
|
|
||||||
}
|
}
|
||||||
catch
|
|
||||||
|
public string Name { get; private set; }
|
||||||
|
public IEnumerable<string> Contents { get { return index.Keys; } }
|
||||||
|
|
||||||
|
readonly Dictionary<string, Entry> index = new Dictionary<string, Entry>();
|
||||||
|
readonly Stream s;
|
||||||
|
readonly long dataStart = 255;
|
||||||
|
|
||||||
|
public InstallShieldPackage(Stream s, string filename)
|
||||||
{
|
{
|
||||||
Dispose();
|
Name = filename;
|
||||||
throw;
|
this.s = s;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Parse package header
|
||||||
|
/*var signature = */s.ReadUInt32();
|
||||||
|
s.Position += 8;
|
||||||
|
/*var FileCount = */s.ReadUInt16();
|
||||||
|
s.Position += 4;
|
||||||
|
/*var ArchiveSize = */s.ReadUInt32();
|
||||||
|
s.Position += 19;
|
||||||
|
var tocAddress = s.ReadInt32();
|
||||||
|
s.Position += 4;
|
||||||
|
var dirCount = s.ReadUInt16();
|
||||||
|
|
||||||
|
// Parse the directory list
|
||||||
|
s.Position = tocAddress;
|
||||||
|
|
||||||
|
// Parse directories
|
||||||
|
var directories = new Dictionary<string, uint>();
|
||||||
|
for (var i = 0; i < dirCount; i++)
|
||||||
|
{
|
||||||
|
// Parse directory header
|
||||||
|
var fileCount = s.ReadUInt16();
|
||||||
|
var chunkSize = s.ReadUInt16();
|
||||||
|
var nameLength = s.ReadUInt16();
|
||||||
|
var dirName = s.ReadASCII(nameLength);
|
||||||
|
|
||||||
|
// Skip to the end of the chunk
|
||||||
|
s.ReadBytes(chunkSize - nameLength - 6);
|
||||||
|
directories.Add(dirName, fileCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse files
|
||||||
|
foreach (var dir in directories)
|
||||||
|
for (var i = 0; i < dir.Value; i++)
|
||||||
|
ParseFile(dir.Key);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Dispose();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint accumulatedData = 0;
|
||||||
|
void ParseFile(string dirName)
|
||||||
|
{
|
||||||
|
s.Position += 7;
|
||||||
|
var compressedSize = s.ReadUInt32();
|
||||||
|
s.Position += 12;
|
||||||
|
var chunkSize = s.ReadUInt16();
|
||||||
|
s.Position += 4;
|
||||||
|
var nameLength = s.ReadByte();
|
||||||
|
var fileName = dirName + "\\" + s.ReadASCII(nameLength);
|
||||||
|
|
||||||
|
// Use index syntax to overwrite any duplicate entries with the last value
|
||||||
|
index[fileName] = new Entry(accumulatedData, compressedSize);
|
||||||
|
accumulatedData += compressedSize;
|
||||||
|
|
||||||
|
// Skip to the end of the chunk
|
||||||
|
s.Position += chunkSize - nameLength - 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stream GetStream(string filename)
|
||||||
|
{
|
||||||
|
Entry e;
|
||||||
|
if (!index.TryGetValue(filename, out e))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
s.Seek(dataStart + e.Offset, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
var ret = new MemoryStream();
|
||||||
|
Blast.Decompress(s, ret);
|
||||||
|
ret.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyPackage OpenPackage(string filename, FileSystem context)
|
||||||
|
{
|
||||||
|
// Not implemented
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Contains(string filename)
|
||||||
|
{
|
||||||
|
return index.ContainsKey(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, Entry> Index { get { return new ReadOnlyDictionary<string, Entry>(index); } }
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
s.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uint accumulatedData = 0;
|
bool IPackageLoader.TryParsePackage(Stream s, string filename, FileSystem context, out IReadOnlyPackage package)
|
||||||
void ParseFile(Stream s, string dirName)
|
|
||||||
{
|
{
|
||||||
s.Position += 7;
|
// Take a peek at the file signature
|
||||||
var compressedSize = s.ReadUInt32();
|
var signature = s.ReadUInt32();
|
||||||
s.Position += 12;
|
s.Position -= 4;
|
||||||
var chunkSize = s.ReadUInt16();
|
|
||||||
s.Position += 4;
|
|
||||||
var nameLength = s.ReadByte();
|
|
||||||
var fileName = dirName + "\\" + s.ReadASCII(nameLength);
|
|
||||||
|
|
||||||
// Use index syntax to overwrite any duplicate entries with the last value
|
if (signature != 0x8C655D13)
|
||||||
index[fileName] = new Entry(accumulatedData, compressedSize);
|
{
|
||||||
accumulatedData += compressedSize;
|
package = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Skip to the end of the chunk
|
package = new InstallShieldPackage(s, filename);
|
||||||
s.Position += chunkSize - nameLength - 30;
|
return true;
|
||||||
}
|
|
||||||
|
|
||||||
public Stream GetStream(string filename)
|
|
||||||
{
|
|
||||||
Entry e;
|
|
||||||
if (!index.TryGetValue(filename, out e))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
s.Seek(dataStart + e.Offset, SeekOrigin.Begin);
|
|
||||||
|
|
||||||
var ret = new MemoryStream();
|
|
||||||
Blast.Decompress(s, ret);
|
|
||||||
ret.Seek(0, SeekOrigin.Begin);
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Contains(string filename)
|
|
||||||
{
|
|
||||||
return index.ContainsKey(filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IReadOnlyDictionary<string, Entry> Index { get { return new ReadOnlyDictionary<string, Entry>(index); } }
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
s.Dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using OpenRA.FileFormats;
|
using OpenRA.FileFormats;
|
||||||
@@ -19,225 +18,248 @@ using OpenRA.Primitives;
|
|||||||
|
|
||||||
namespace OpenRA.FileSystem
|
namespace OpenRA.FileSystem
|
||||||
{
|
{
|
||||||
public sealed class MixFile : IReadOnlyPackage
|
public class MixLoader : IPackageLoader
|
||||||
{
|
{
|
||||||
public string Name { get; private set; }
|
public sealed class MixFile : IReadOnlyPackage
|
||||||
public IEnumerable<string> Contents { get { return index.Keys; } }
|
|
||||||
|
|
||||||
readonly Dictionary<string, PackageEntry> index;
|
|
||||||
readonly long dataStart;
|
|
||||||
readonly Stream s;
|
|
||||||
readonly FileSystem context;
|
|
||||||
|
|
||||||
public MixFile(FileSystem context, string filename)
|
|
||||||
{
|
{
|
||||||
Name = filename;
|
public string Name { get; private set; }
|
||||||
this.context = context;
|
public IEnumerable<string> Contents { get { return index.Keys; } }
|
||||||
|
|
||||||
s = context.Open(filename);
|
readonly Dictionary<string, PackageEntry> index;
|
||||||
try
|
readonly long dataStart;
|
||||||
|
readonly Stream s;
|
||||||
|
|
||||||
|
public MixFile(Stream s, string filename, HashSet<string> allPossibleFilenames)
|
||||||
{
|
{
|
||||||
// Detect format type
|
Name = filename;
|
||||||
var isCncMix = s.ReadUInt16() != 0;
|
this.s = s;
|
||||||
|
|
||||||
// The C&C mix format doesn't contain any flags or encryption
|
try
|
||||||
var isEncrypted = false;
|
|
||||||
if (!isCncMix)
|
|
||||||
isEncrypted = (s.ReadUInt16() & 0x2) != 0;
|
|
||||||
|
|
||||||
List<PackageEntry> entries;
|
|
||||||
if (isEncrypted)
|
|
||||||
{
|
{
|
||||||
long unused;
|
// Detect format type
|
||||||
entries = ParseHeader(DecryptHeader(s, 4, out dataStart), 0, out unused);
|
var isCncMix = s.ReadUInt16() != 0;
|
||||||
}
|
|
||||||
else
|
|
||||||
entries = ParseHeader(s, isCncMix ? 0 : 4, out dataStart);
|
|
||||||
|
|
||||||
index = ParseIndex(entries.ToDictionaryWithConflictLog(x => x.Hash,
|
// The C&C mix format doesn't contain any flags or encryption
|
||||||
"{0} ({1} format, Encrypted: {2}, DataStart: {3})".F(filename, isCncMix ? "C&C" : "RA/TS/RA2", isEncrypted, dataStart),
|
var isEncrypted = false;
|
||||||
null, x => "(offs={0}, len={1})".F(x.Offset, x.Length)));
|
if (!isCncMix)
|
||||||
|
isEncrypted = (s.ReadUInt16() & 0x2) != 0;
|
||||||
|
|
||||||
|
List<PackageEntry> entries;
|
||||||
|
if (isEncrypted)
|
||||||
|
{
|
||||||
|
long unused;
|
||||||
|
entries = ParseHeader(DecryptHeader(s, 4, out dataStart), 0, out unused);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
entries = ParseHeader(s, isCncMix ? 0 : 4, out dataStart);
|
||||||
|
|
||||||
|
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)), allPossibleFilenames);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
Dispose();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception)
|
|
||||||
|
Dictionary<string, PackageEntry> ParseIndex(Dictionary<uint, PackageEntry> entries, HashSet<string> allPossibleFilenames)
|
||||||
{
|
{
|
||||||
Dispose();
|
var classicIndex = new Dictionary<string, PackageEntry>();
|
||||||
throw;
|
var crcIndex = new Dictionary<string, PackageEntry>();
|
||||||
|
|
||||||
|
// Try and find a local mix database
|
||||||
|
var dbNameClassic = PackageEntry.HashFilename("local mix database.dat", PackageHashType.Classic);
|
||||||
|
var dbNameCRC = PackageEntry.HashFilename("local mix database.dat", PackageHashType.CRC32);
|
||||||
|
foreach (var kv in entries)
|
||||||
|
{
|
||||||
|
if (kv.Key == dbNameClassic || kv.Key == dbNameCRC)
|
||||||
|
{
|
||||||
|
using (var content = GetContent(kv.Value))
|
||||||
|
{
|
||||||
|
var db = new XccLocalDatabase(content);
|
||||||
|
foreach (var e in db.Entries)
|
||||||
|
allPossibleFilenames.Add(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var filename in allPossibleFilenames)
|
||||||
|
{
|
||||||
|
var classicHash = PackageEntry.HashFilename(filename, PackageHashType.Classic);
|
||||||
|
var crcHash = PackageEntry.HashFilename(filename, PackageHashType.CRC32);
|
||||||
|
PackageEntry e;
|
||||||
|
|
||||||
|
if (entries.TryGetValue(classicHash, out e))
|
||||||
|
classicIndex.Add(filename, e);
|
||||||
|
|
||||||
|
if (entries.TryGetValue(crcHash, out e))
|
||||||
|
crcIndex.Add(filename, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
var bestIndex = crcIndex.Count > classicIndex.Count ? crcIndex : classicIndex;
|
||||||
|
|
||||||
|
var unknown = entries.Count - bestIndex.Count;
|
||||||
|
if (unknown > 0)
|
||||||
|
Log.Write("debug", "{0}: failed to resolve filenames for {1} unknown hashes".F(Name, unknown));
|
||||||
|
|
||||||
|
return bestIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<PackageEntry> ParseHeader(Stream s, long offset, out long headerEnd)
|
||||||
|
{
|
||||||
|
s.Seek(offset, SeekOrigin.Begin);
|
||||||
|
var numFiles = s.ReadUInt16();
|
||||||
|
/*uint dataSize = */s.ReadUInt32();
|
||||||
|
|
||||||
|
var items = new List<PackageEntry>();
|
||||||
|
for (var i = 0; i < numFiles; i++)
|
||||||
|
items.Add(new PackageEntry(s));
|
||||||
|
|
||||||
|
headerEnd = offset + 6 + numFiles * PackageEntry.Size;
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
static MemoryStream DecryptHeader(Stream s, long offset, out long headerEnd)
|
||||||
|
{
|
||||||
|
s.Seek(offset, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
// Decrypt blowfish key
|
||||||
|
var keyblock = s.ReadBytes(80);
|
||||||
|
var blowfishKey = new BlowfishKeyProvider().DecryptKey(keyblock);
|
||||||
|
var fish = new Blowfish(blowfishKey);
|
||||||
|
|
||||||
|
// Decrypt first block to work out the header length
|
||||||
|
var ms = Decrypt(ReadBlocks(s, offset + 80, 1), fish);
|
||||||
|
var numFiles = ms.ReadUInt16();
|
||||||
|
|
||||||
|
// Decrypt the full header - round bytes up to a full block
|
||||||
|
var blockCount = (13 + numFiles * PackageEntry.Size) / 8;
|
||||||
|
headerEnd = offset + 80 + blockCount * 8;
|
||||||
|
|
||||||
|
return Decrypt(ReadBlocks(s, offset + 80, blockCount), fish);
|
||||||
|
}
|
||||||
|
|
||||||
|
static MemoryStream Decrypt(uint[] h, Blowfish fish)
|
||||||
|
{
|
||||||
|
var decrypted = fish.Decrypt(h);
|
||||||
|
|
||||||
|
var ms = new MemoryStream();
|
||||||
|
var writer = new BinaryWriter(ms);
|
||||||
|
foreach (var t in decrypted)
|
||||||
|
writer.Write(t);
|
||||||
|
writer.Flush();
|
||||||
|
|
||||||
|
ms.Seek(0, SeekOrigin.Begin);
|
||||||
|
return ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint[] ReadBlocks(Stream s, long offset, int count)
|
||||||
|
{
|
||||||
|
if (offset < 0)
|
||||||
|
throw new ArgumentOutOfRangeException("offset", "Non-negative number required.");
|
||||||
|
|
||||||
|
if (count < 0)
|
||||||
|
throw new ArgumentOutOfRangeException("count", "Non-negative number required.");
|
||||||
|
|
||||||
|
if (offset + (count * 2) > s.Length)
|
||||||
|
throw new ArgumentException("Bytes to read {0} and offset {1} greater than stream length {2}.".F(count * 2, offset, s.Length));
|
||||||
|
|
||||||
|
s.Seek(offset, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
// A block is a single encryption unit (represented as two 32-bit integers)
|
||||||
|
var ret = new uint[2 * count];
|
||||||
|
for (var i = 0; i < ret.Length; i++)
|
||||||
|
ret[i] = s.ReadUInt32();
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stream GetContent(PackageEntry entry)
|
||||||
|
{
|
||||||
|
Stream parentStream;
|
||||||
|
var baseOffset = dataStart + entry.Offset;
|
||||||
|
var nestedOffset = baseOffset + SegmentStream.GetOverallNestedOffset(s, out parentStream);
|
||||||
|
|
||||||
|
// Special case FileStream - instead of creating an in-memory copy,
|
||||||
|
// just reference the portion of the on-disk file that we need to save memory.
|
||||||
|
// We use GetType instead of 'is' here since we can't handle any derived classes of FileStream.
|
||||||
|
if (parentStream.GetType() == typeof(FileStream))
|
||||||
|
{
|
||||||
|
var path = ((FileStream)parentStream).Name;
|
||||||
|
return new SegmentStream(File.OpenRead(path), nestedOffset, entry.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For all other streams, create a copy in memory.
|
||||||
|
// This uses more memory but is the only way in general to ensure the returned streams won't clash.
|
||||||
|
s.Seek(baseOffset, SeekOrigin.Begin);
|
||||||
|
return new MemoryStream(s.ReadBytes((int)entry.Length));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stream GetStream(string filename)
|
||||||
|
{
|
||||||
|
PackageEntry e;
|
||||||
|
if (!index.TryGetValue(filename, out e))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return GetContent(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, PackageEntry> Index
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var absoluteIndex = index.ToDictionary(e => e.Key, e => new PackageEntry(e.Value.Hash, (uint)(e.Value.Offset + dataStart), e.Value.Length));
|
||||||
|
return new ReadOnlyDictionary<string, PackageEntry>(absoluteIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Contains(string filename)
|
||||||
|
{
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Dictionary<string, PackageEntry> ParseIndex(Dictionary<uint, PackageEntry> entries)
|
bool IPackageLoader.TryParsePackage(Stream s, string filename, FileSystem context, out IReadOnlyPackage package)
|
||||||
{
|
{
|
||||||
var classicIndex = new Dictionary<string, PackageEntry>();
|
if (!filename.EndsWith(".mix", StringComparison.InvariantCultureIgnoreCase))
|
||||||
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);
|
|
||||||
var dbNameCRC = PackageEntry.HashFilename("local mix database.dat", PackageHashType.CRC32);
|
|
||||||
foreach (var kv in entries)
|
|
||||||
{
|
{
|
||||||
if (kv.Key == dbNameClassic || kv.Key == dbNameCRC)
|
package = null;
|
||||||
{
|
return false;
|
||||||
using (var content = GetContent(kv.Value))
|
|
||||||
{
|
|
||||||
var db = new XccLocalDatabase(content);
|
|
||||||
foreach (var e in db.Entries)
|
|
||||||
allPossibleFilenames.Add(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the global mix database
|
// Load the global mix database
|
||||||
// TODO: This should be passed to the mix file ctor
|
Stream mixDatabase;
|
||||||
if (context.Exists("global mix database.dat"))
|
var allPossibleFilenames = new HashSet<string>();
|
||||||
{
|
if (context.TryOpen("global mix database.dat", out mixDatabase))
|
||||||
using (var db = new XccGlobalDatabase(context.Open("global mix database.dat")))
|
using (var db = new XccGlobalDatabase(mixDatabase))
|
||||||
{
|
|
||||||
foreach (var e in db.Entries)
|
foreach (var e in db.Entries)
|
||||||
allPossibleFilenames.Add(e);
|
allPossibleFilenames.Add(e);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var filename in allPossibleFilenames)
|
package = new MixFile(s, filename, allPossibleFilenames);
|
||||||
{
|
return true;
|
||||||
var classicHash = PackageEntry.HashFilename(filename, PackageHashType.Classic);
|
|
||||||
var crcHash = PackageEntry.HashFilename(filename, PackageHashType.CRC32);
|
|
||||||
PackageEntry e;
|
|
||||||
|
|
||||||
if (entries.TryGetValue(classicHash, out e))
|
|
||||||
classicIndex.Add(filename, e);
|
|
||||||
|
|
||||||
if (entries.TryGetValue(crcHash, out e))
|
|
||||||
crcIndex.Add(filename, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
var bestIndex = crcIndex.Count > classicIndex.Count ? crcIndex : classicIndex;
|
|
||||||
|
|
||||||
var unknown = entries.Count - bestIndex.Count;
|
|
||||||
if (unknown > 0)
|
|
||||||
Log.Write("debug", "{0}: failed to resolve filenames for {1} unknown hashes".F(Name, unknown));
|
|
||||||
|
|
||||||
return bestIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
static List<PackageEntry> ParseHeader(Stream s, long offset, out long headerEnd)
|
|
||||||
{
|
|
||||||
s.Seek(offset, SeekOrigin.Begin);
|
|
||||||
var numFiles = s.ReadUInt16();
|
|
||||||
/*uint dataSize = */s.ReadUInt32();
|
|
||||||
|
|
||||||
var items = new List<PackageEntry>();
|
|
||||||
for (var i = 0; i < numFiles; i++)
|
|
||||||
items.Add(new PackageEntry(s));
|
|
||||||
|
|
||||||
headerEnd = offset + 6 + numFiles * PackageEntry.Size;
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
static MemoryStream DecryptHeader(Stream s, long offset, out long headerEnd)
|
|
||||||
{
|
|
||||||
s.Seek(offset, SeekOrigin.Begin);
|
|
||||||
|
|
||||||
// Decrypt blowfish key
|
|
||||||
var keyblock = s.ReadBytes(80);
|
|
||||||
var blowfishKey = new BlowfishKeyProvider().DecryptKey(keyblock);
|
|
||||||
var fish = new Blowfish(blowfishKey);
|
|
||||||
|
|
||||||
// Decrypt first block to work out the header length
|
|
||||||
var ms = Decrypt(ReadBlocks(s, offset + 80, 1), fish);
|
|
||||||
var numFiles = ms.ReadUInt16();
|
|
||||||
|
|
||||||
// Decrypt the full header - round bytes up to a full block
|
|
||||||
var blockCount = (13 + numFiles * PackageEntry.Size) / 8;
|
|
||||||
headerEnd = offset + 80 + blockCount * 8;
|
|
||||||
|
|
||||||
return Decrypt(ReadBlocks(s, offset + 80, blockCount), fish);
|
|
||||||
}
|
|
||||||
|
|
||||||
static MemoryStream Decrypt(uint[] h, Blowfish fish)
|
|
||||||
{
|
|
||||||
var decrypted = fish.Decrypt(h);
|
|
||||||
|
|
||||||
var ms = new MemoryStream();
|
|
||||||
var writer = new BinaryWriter(ms);
|
|
||||||
foreach (var t in decrypted)
|
|
||||||
writer.Write(t);
|
|
||||||
writer.Flush();
|
|
||||||
|
|
||||||
ms.Seek(0, SeekOrigin.Begin);
|
|
||||||
return ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
static uint[] ReadBlocks(Stream s, long offset, int count)
|
|
||||||
{
|
|
||||||
if (offset < 0)
|
|
||||||
throw new ArgumentOutOfRangeException("offset", "Non-negative number required.");
|
|
||||||
|
|
||||||
if (count < 0)
|
|
||||||
throw new ArgumentOutOfRangeException("count", "Non-negative number required.");
|
|
||||||
|
|
||||||
if (offset + (count * 2) > s.Length)
|
|
||||||
throw new ArgumentException("Bytes to read {0} and offset {1} greater than stream length {2}.".F(count * 2, offset, s.Length));
|
|
||||||
|
|
||||||
s.Seek(offset, SeekOrigin.Begin);
|
|
||||||
|
|
||||||
// A block is a single encryption unit (represented as two 32-bit integers)
|
|
||||||
var ret = new uint[2 * count];
|
|
||||||
for (var i = 0; i < ret.Length; i++)
|
|
||||||
ret[i] = s.ReadUInt32();
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Stream GetContent(PackageEntry entry)
|
|
||||||
{
|
|
||||||
Stream parentStream;
|
|
||||||
var baseOffset = dataStart + entry.Offset;
|
|
||||||
var nestedOffset = baseOffset + SegmentStream.GetOverallNestedOffset(s, out parentStream);
|
|
||||||
|
|
||||||
// Special case FileStream - instead of creating an in-memory copy,
|
|
||||||
// just reference the portion of the on-disk file that we need to save memory.
|
|
||||||
// We use GetType instead of 'is' here since we can't handle any derived classes of FileStream.
|
|
||||||
if (parentStream.GetType() == typeof(FileStream))
|
|
||||||
{
|
|
||||||
var path = ((FileStream)parentStream).Name;
|
|
||||||
return new SegmentStream(File.OpenRead(path), nestedOffset, entry.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
// For all other streams, create a copy in memory.
|
|
||||||
// This uses more memory but is the only way in general to ensure the returned streams won't clash.
|
|
||||||
s.Seek(baseOffset, SeekOrigin.Begin);
|
|
||||||
return new MemoryStream(s.ReadBytes((int)entry.Length));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Stream GetStream(string filename)
|
|
||||||
{
|
|
||||||
PackageEntry e;
|
|
||||||
if (!index.TryGetValue(filename, out e))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return GetContent(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IReadOnlyDictionary<string, PackageEntry> Index
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var absoluteIndex = index.ToDictionary(e => e.Key, e => new PackageEntry(e.Value.Hash, (uint)(e.Value.Offset + dataStart), e.Value.Length));
|
|
||||||
return new ReadOnlyDictionary<string, PackageEntry>(absoluteIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Contains(string filename)
|
|
||||||
{
|
|
||||||
return index.ContainsKey(filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
s.Dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,76 +9,96 @@
|
|||||||
*/
|
*/
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
namespace OpenRA.FileSystem
|
namespace OpenRA.FileSystem
|
||||||
{
|
{
|
||||||
struct Entry
|
public class PakFileLoader : IPackageLoader
|
||||||
{
|
{
|
||||||
public uint Offset;
|
struct Entry
|
||||||
public uint Length;
|
|
||||||
public string Filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class PakFile : IReadOnlyPackage
|
|
||||||
{
|
|
||||||
public string Name { get; private set; }
|
|
||||||
public IEnumerable<string> Contents { get { return index.Keys; } }
|
|
||||||
|
|
||||||
readonly Dictionary<string, Entry> index;
|
|
||||||
readonly Stream stream;
|
|
||||||
|
|
||||||
public PakFile(FileSystem context, string filename)
|
|
||||||
{
|
{
|
||||||
Name = filename;
|
public uint Offset;
|
||||||
index = new Dictionary<string, Entry>();
|
public uint Length;
|
||||||
|
public string Filename;
|
||||||
|
}
|
||||||
|
|
||||||
stream = context.Open(filename);
|
sealed class PakFile : IReadOnlyPackage
|
||||||
try
|
{
|
||||||
|
public string Name { get; private set; }
|
||||||
|
public IEnumerable<string> Contents { get { return index.Keys; } }
|
||||||
|
|
||||||
|
readonly Dictionary<string, Entry> index = new Dictionary<string, Entry>();
|
||||||
|
readonly Stream stream;
|
||||||
|
|
||||||
|
public PakFile(Stream stream, string filename)
|
||||||
{
|
{
|
||||||
index = new Dictionary<string, Entry>();
|
Name = filename;
|
||||||
var offset = stream.ReadUInt32();
|
this.stream = stream;
|
||||||
while (offset != 0)
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var file = stream.ReadASCIIZ();
|
var offset = stream.ReadUInt32();
|
||||||
var next = stream.ReadUInt32();
|
while (offset != 0)
|
||||||
var length = (next == 0 ? (uint)stream.Length : next) - offset;
|
{
|
||||||
|
var file = stream.ReadASCIIZ();
|
||||||
|
var next = stream.ReadUInt32();
|
||||||
|
var length = (next == 0 ? (uint)stream.Length : next) - offset;
|
||||||
|
|
||||||
// Ignore duplicate files
|
// Ignore duplicate files
|
||||||
if (index.ContainsKey(file))
|
if (index.ContainsKey(file))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
index.Add(file, new Entry { Offset = offset, Length = length, Filename = file });
|
index.Add(file, new Entry { Offset = offset, Length = length, Filename = file });
|
||||||
offset = next;
|
offset = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Dispose();
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
|
||||||
|
public Stream GetStream(string filename)
|
||||||
{
|
{
|
||||||
Dispose();
|
Entry entry;
|
||||||
throw;
|
if (!index.TryGetValue(filename, out entry))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
stream.Seek(entry.Offset, SeekOrigin.Begin);
|
||||||
|
var data = stream.ReadBytes((int)entry.Length);
|
||||||
|
return new MemoryStream(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Contains(string filename)
|
||||||
|
{
|
||||||
|
return index.ContainsKey(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyPackage OpenPackage(string filename, FileSystem context)
|
||||||
|
{
|
||||||
|
// Not implemented
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
stream.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Stream GetStream(string filename)
|
bool IPackageLoader.TryParsePackage(Stream s, string filename, FileSystem context, out IReadOnlyPackage package)
|
||||||
{
|
{
|
||||||
Entry entry;
|
if (!filename.EndsWith(".pak", StringComparison.InvariantCultureIgnoreCase))
|
||||||
if (!index.TryGetValue(filename, out entry))
|
{
|
||||||
return null;
|
package = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
stream.Seek(entry.Offset, SeekOrigin.Begin);
|
package = new PakFile(s, filename);
|
||||||
var data = stream.ReadBytes((int)entry.Length);
|
return true;
|
||||||
return new MemoryStream(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Contains(string filename)
|
|
||||||
{
|
|
||||||
return index.ContainsKey(filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
stream.Dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
*/
|
*/
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
using System.Collections;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -19,142 +19,227 @@ using SZipFile = ICSharpCode.SharpZipLib.Zip.ZipFile;
|
|||||||
|
|
||||||
namespace OpenRA.FileSystem
|
namespace OpenRA.FileSystem
|
||||||
{
|
{
|
||||||
public sealed class ZipFile : IReadWritePackage
|
public class ZipFileLoader : IPackageLoader
|
||||||
{
|
{
|
||||||
public IReadWritePackage Parent { get; private set; }
|
static readonly string[] Extensions = { ".zip", ".oramap" };
|
||||||
public string Name { get; private set; }
|
|
||||||
readonly Stream pkgStream;
|
|
||||||
readonly SZipFile pkg;
|
|
||||||
|
|
||||||
static ZipFile()
|
sealed class ZipFile : IReadWritePackage
|
||||||
{
|
{
|
||||||
ZipConstants.DefaultCodePage = Encoding.UTF8.CodePage;
|
public IReadWritePackage Parent { get; private set; }
|
||||||
|
public string Name { get; private set; }
|
||||||
|
readonly Stream pkgStream;
|
||||||
|
readonly SZipFile pkg;
|
||||||
|
|
||||||
|
static ZipFile()
|
||||||
|
{
|
||||||
|
ZipConstants.DefaultCodePage = Encoding.UTF8.CodePage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ZipFile(Stream stream, string name, IReadOnlyPackage parent = null)
|
||||||
|
{
|
||||||
|
// 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();
|
||||||
|
stream.CopyTo(pkgStream);
|
||||||
|
pkgStream.Position = 0;
|
||||||
|
|
||||||
|
Name = name;
|
||||||
|
Parent = parent as IReadWritePackage;
|
||||||
|
pkg = new SZipFile(pkgStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ZipFile(string filename, IReadWritePackage parent)
|
||||||
|
{
|
||||||
|
pkgStream = new MemoryStream();
|
||||||
|
|
||||||
|
Name = filename;
|
||||||
|
Parent = parent;
|
||||||
|
pkg = SZipFile.Create(pkgStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stream GetStream(string filename)
|
||||||
|
{
|
||||||
|
var entry = pkg.GetEntry(filename);
|
||||||
|
if (entry == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
using (var z = pkg.GetInputStream(entry))
|
||||||
|
{
|
||||||
|
var ms = new MemoryStream();
|
||||||
|
z.CopyTo(ms);
|
||||||
|
ms.Seek(0, SeekOrigin.Begin);
|
||||||
|
return ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<string> Contents
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
foreach (ZipEntry entry in pkg)
|
||||||
|
yield return entry.Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Contains(string filename)
|
||||||
|
{
|
||||||
|
return pkg.GetEntry(filename) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Commit()
|
||||||
|
{
|
||||||
|
if (Parent == null)
|
||||||
|
throw new InvalidDataException("Cannot update ZipFile without writable parent");
|
||||||
|
|
||||||
|
var pos = pkgStream.Position;
|
||||||
|
pkgStream.Position = 0;
|
||||||
|
Parent.Update(Name, pkgStream.ReadBytes((int)pkgStream.Length));
|
||||||
|
pkgStream.Position = pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Update(string filename, byte[] contents)
|
||||||
|
{
|
||||||
|
pkg.BeginUpdate();
|
||||||
|
pkg.Add(new StaticStreamDataSource(new MemoryStream(contents)), filename);
|
||||||
|
pkg.CommitUpdate();
|
||||||
|
Commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Delete(string filename)
|
||||||
|
{
|
||||||
|
pkg.BeginUpdate();
|
||||||
|
pkg.Delete(filename);
|
||||||
|
pkg.CommitUpdate();
|
||||||
|
Commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (pkg != null)
|
||||||
|
pkg.Close();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ZipFile(Stream stream, string name, IReadOnlyPackage parent = null)
|
sealed class ZipFolder : IReadOnlyPackage
|
||||||
{
|
{
|
||||||
// SharpZipLib breaks when asked to update archives loaded from outside streams or files
|
public string Name { get { return path; } }
|
||||||
// We can work around this by creating a clean in-memory-only file, cutting all outside references
|
public ZipFile Parent { get; private set; }
|
||||||
pkgStream = new MemoryStream();
|
readonly string path;
|
||||||
stream.CopyTo(pkgStream);
|
|
||||||
pkgStream.Position = 0;
|
|
||||||
|
|
||||||
Name = name;
|
static ZipFolder()
|
||||||
Parent = parent as IReadWritePackage;
|
{
|
||||||
pkg = new SZipFile(pkgStream);
|
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 */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
public ZipFile(IReadOnlyFileSystem context, string filename)
|
class StaticStreamDataSource : IStaticDataSource
|
||||||
{
|
{
|
||||||
|
readonly Stream s;
|
||||||
|
public StaticStreamDataSource(Stream s)
|
||||||
|
{
|
||||||
|
this.s = s;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stream GetSource()
|
||||||
|
{
|
||||||
|
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;
|
string name;
|
||||||
IReadOnlyPackage p;
|
IReadOnlyPackage p;
|
||||||
if (!context.TryGetPackageContaining(filename, out p, out name))
|
if (context.TryGetPackageContaining(filename, out p, out name))
|
||||||
throw new FileNotFoundException("Unable to find parent package for " + filename);
|
package = new ZipFile(p.GetStream(name), name, p);
|
||||||
|
else
|
||||||
|
package = new ZipFile(s, filename, null);
|
||||||
|
|
||||||
Name = name;
|
return true;
|
||||||
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 static IReadWritePackage Create(string filename, IReadWritePackage parent)
|
||||||
{
|
|
||||||
pkgStream = new MemoryStream();
|
|
||||||
|
|
||||||
Name = filename;
|
|
||||||
Parent = parent;
|
|
||||||
pkg = SZipFile.Create(pkgStream);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ZipFile Create(string filename, IReadWritePackage parent)
|
|
||||||
{
|
{
|
||||||
return new ZipFile(filename, parent);
|
return new ZipFile(filename, parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Stream GetStream(string filename)
|
|
||||||
{
|
|
||||||
var entry = pkg.GetEntry(filename);
|
|
||||||
if (entry == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
using (var z = pkg.GetInputStream(entry))
|
|
||||||
{
|
|
||||||
var ms = new MemoryStream();
|
|
||||||
z.CopyTo(ms);
|
|
||||||
ms.Seek(0, SeekOrigin.Begin);
|
|
||||||
return ms;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<string> Contents
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
foreach (ZipEntry entry in pkg)
|
|
||||||
yield return entry.Name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Contains(string filename)
|
|
||||||
{
|
|
||||||
return pkg.GetEntry(filename) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Commit()
|
|
||||||
{
|
|
||||||
if (Parent == null)
|
|
||||||
throw new InvalidDataException("Cannot update ZipFile without writable parent");
|
|
||||||
|
|
||||||
var pos = pkgStream.Position;
|
|
||||||
pkgStream.Position = 0;
|
|
||||||
Parent.Update(Name, pkgStream.ReadBytes((int)pkgStream.Length));
|
|
||||||
pkgStream.Position = pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Update(string filename, byte[] contents)
|
|
||||||
{
|
|
||||||
pkg.BeginUpdate();
|
|
||||||
pkg.Add(new StaticStreamDataSource(new MemoryStream(contents)), filename);
|
|
||||||
pkg.CommitUpdate();
|
|
||||||
Commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Delete(string filename)
|
|
||||||
{
|
|
||||||
pkg.BeginUpdate();
|
|
||||||
pkg.Delete(filename);
|
|
||||||
pkg.CommitUpdate();
|
|
||||||
Commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (pkg != null)
|
|
||||||
pkg.Close();
|
|
||||||
|
|
||||||
if (pkgStream != null)
|
|
||||||
pkgStream.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class StaticStreamDataSource : IStaticDataSource
|
|
||||||
{
|
|
||||||
readonly Stream s;
|
|
||||||
public StaticStreamDataSource(Stream s)
|
|
||||||
{
|
|
||||||
this.s = s;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Stream GetSource()
|
|
||||||
{
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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[] SoundFormats = { };
|
||||||
public readonly string[] SpriteFormats = { };
|
public readonly string[] SpriteFormats = { };
|
||||||
|
public readonly string[] PackageFormats = { };
|
||||||
|
|
||||||
readonly string[] reservedModuleNames = { "Metadata", "Folders", "MapFolders", "Packages", "Rules",
|
readonly string[] reservedModuleNames = { "Metadata", "Folders", "MapFolders", "Packages", "Rules",
|
||||||
"Sequences", "VoxelSequences", "Cursors", "Chrome", "Assemblies", "ChromeLayout", "Weapons",
|
"Sequences", "VoxelSequences", "Cursors", "Chrome", "Assemblies", "ChromeLayout", "Weapons",
|
||||||
"Voices", "Notifications", "Music", "Translations", "TileSets", "ChromeMetrics", "Missions",
|
"Voices", "Notifications", "Music", "Translations", "TileSets", "ChromeMetrics", "Missions",
|
||||||
"ServerTraits", "LoadScreen", "Fonts", "SupportsMapsFrom", "SoundFormats", "SpriteFormats",
|
"ServerTraits", "LoadScreen", "Fonts", "SupportsMapsFrom", "SoundFormats", "SpriteFormats",
|
||||||
"RequiresMods" };
|
"RequiresMods", "PackageFormats" };
|
||||||
|
|
||||||
readonly TypeDictionary modules = new TypeDictionary();
|
readonly TypeDictionary modules = new TypeDictionary();
|
||||||
readonly Dictionary<string, MiniYaml> yaml;
|
readonly Dictionary<string, MiniYaml> yaml;
|
||||||
@@ -119,6 +120,9 @@ namespace OpenRA
|
|||||||
|
|
||||||
MapCompatibility = compat.ToArray();
|
MapCompatibility = compat.ToArray();
|
||||||
|
|
||||||
|
if (yaml.ContainsKey("PackageFormats"))
|
||||||
|
PackageFormats = FieldLoader.GetValue<string[]>("PackageFormats", yaml["PackageFormats"].Value);
|
||||||
|
|
||||||
if (yaml.ContainsKey("SoundFormats"))
|
if (yaml.ContainsKey("SoundFormats"))
|
||||||
SoundFormats = FieldLoader.GetValue<string[]>("SoundFormats", yaml["SoundFormats"].Value);
|
SoundFormats = FieldLoader.GetValue<string[]>("SoundFormats", yaml["SoundFormats"].Value);
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
|
||||||
using OpenRA.FileSystem;
|
using OpenRA.FileSystem;
|
||||||
using OpenRA.Graphics;
|
using OpenRA.Graphics;
|
||||||
using OpenRA.Widgets;
|
using OpenRA.Widgets;
|
||||||
@@ -27,6 +26,7 @@ namespace OpenRA
|
|||||||
public readonly ObjectCreator ObjectCreator;
|
public readonly ObjectCreator ObjectCreator;
|
||||||
public readonly WidgetLoader WidgetLoader;
|
public readonly WidgetLoader WidgetLoader;
|
||||||
public readonly MapCache MapCache;
|
public readonly MapCache MapCache;
|
||||||
|
public readonly IPackageLoader[] PackageLoaders;
|
||||||
public readonly ISoundLoader[] SoundLoaders;
|
public readonly ISoundLoader[] SoundLoaders;
|
||||||
public readonly ISpriteLoader[] SpriteLoaders;
|
public readonly ISpriteLoader[] SpriteLoaders;
|
||||||
public readonly ISpriteSequenceLoader SpriteSequenceLoader;
|
public readonly ISpriteSequenceLoader SpriteSequenceLoader;
|
||||||
@@ -49,12 +49,12 @@ namespace OpenRA
|
|||||||
{
|
{
|
||||||
Languages = new string[0];
|
Languages = new string[0];
|
||||||
|
|
||||||
|
|
||||||
// Take a local copy of the manifest
|
// Take a local copy of the manifest
|
||||||
Manifest = new Manifest(mod.Id, mod.Package);
|
Manifest = new Manifest(mod.Id, mod.Package);
|
||||||
ObjectCreator = new ObjectCreator(Manifest, mods);
|
ObjectCreator = new ObjectCreator(Manifest, mods);
|
||||||
|
PackageLoaders = ObjectCreator.GetLoaders<IPackageLoader>(Manifest.PackageFormats, "package");
|
||||||
|
|
||||||
ModFiles = new FS(mods);
|
ModFiles = new FS(mods, PackageLoaders);
|
||||||
ModFiles.LoadFromManifest(Manifest);
|
ModFiles.LoadFromManifest(Manifest);
|
||||||
Manifest.LoadCustomData(ObjectCreator);
|
Manifest.LoadCustomData(ObjectCreator);
|
||||||
|
|
||||||
|
|||||||
@@ -254,7 +254,6 @@
|
|||||||
<Compile Include="Graphics\RgbaColorRenderer.cs" />
|
<Compile Include="Graphics\RgbaColorRenderer.cs" />
|
||||||
<Compile Include="Traits\Player\IndexedPlayerPalette.cs" />
|
<Compile Include="Traits\Player\IndexedPlayerPalette.cs" />
|
||||||
<Compile Include="Traits\ActivityUtils.cs" />
|
<Compile Include="Traits\ActivityUtils.cs" />
|
||||||
<Compile Include="FileSystem\ZipFolder.cs" />
|
|
||||||
<Compile Include="Primitives\float3.cs" />
|
<Compile Include="Primitives\float3.cs" />
|
||||||
<Compile Include="InstalledMods.cs" />
|
<Compile Include="InstalledMods.cs" />
|
||||||
<Compile Include="CryptoUtil.cs" />
|
<Compile Include="CryptoUtil.cs" />
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ namespace OpenRA.Mods.Cnc.UtilityCommands
|
|||||||
map.PlayerDefinitions = mapPlayers.ToMiniYaml();
|
map.PlayerDefinitions = mapPlayers.ToMiniYaml();
|
||||||
|
|
||||||
var dest = Path.GetFileNameWithoutExtension(args[1]) + ".oramap";
|
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.");
|
Console.WriteLine(dest + " saved.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ namespace OpenRA.Mods.Common.UtilityCommands
|
|||||||
|
|
||||||
var dest = Path.GetFileNameWithoutExtension(args[1]) + ".oramap";
|
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.");
|
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")]
|
[Desc("ARCHIVE.Z", "Lists the content ranges for a InstallShield V3 file")]
|
||||||
void IUtilityCommand.Run(Utility utility, string[] args)
|
void IUtilityCommand.Run(Utility utility, string[] args)
|
||||||
{
|
{
|
||||||
var filename = Path.GetFileName(args[1]);
|
var package = new InstallShieldLoader.InstallShieldPackage(File.OpenRead(args[1]), 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);
|
|
||||||
|
|
||||||
foreach (var kv in package.Index)
|
foreach (var kv in package.Index)
|
||||||
{
|
{
|
||||||
Console.WriteLine("{0}:", kv.Key);
|
Console.WriteLine("{0}:", kv.Key);
|
||||||
|
|||||||
@@ -10,8 +10,10 @@
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using OpenRA.FileFormats;
|
||||||
using OpenRA.FileSystem;
|
using OpenRA.FileSystem;
|
||||||
|
|
||||||
namespace OpenRA.Mods.Common.UtilityCommands
|
namespace OpenRA.Mods.Common.UtilityCommands
|
||||||
@@ -22,23 +24,18 @@ namespace OpenRA.Mods.Common.UtilityCommands
|
|||||||
|
|
||||||
bool IUtilityCommand.ValidateArguments(string[] args)
|
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)
|
void IUtilityCommand.Run(Utility utility, string[] args)
|
||||||
{
|
{
|
||||||
var filename = Path.GetFileName(args[1]);
|
var allPossibleFilenames = new HashSet<string>();
|
||||||
var path = Path.GetDirectoryName(args[1]);
|
using (var db = new XccGlobalDatabase(File.OpenRead(args[2])))
|
||||||
|
foreach (var e in db.Entries)
|
||||||
var fs = new FileSystem.FileSystem(utility.Mods);
|
allPossibleFilenames.Add(e);
|
||||||
|
|
||||||
// Needed to access the global mix database
|
|
||||||
fs.LoadFromManifest(utility.ModData.Manifest);
|
|
||||||
|
|
||||||
fs.Mount(path, "parent");
|
|
||||||
var package = new MixFile(fs, "parent|" + filename);
|
|
||||||
|
|
||||||
|
var package = new MixLoader.MixFile(File.OpenRead(args[1]), args[1], allPossibleFilenames);
|
||||||
foreach (var kv in package.Index.OrderBy(kv => kv.Value.Offset))
|
foreach (var kv in package.Index.OrderBy(kv => kv.Value.Offset))
|
||||||
{
|
{
|
||||||
Console.WriteLine("{0}:", kv.Key);
|
Console.WriteLine("{0}:", kv.Key);
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
|||||||
directoryDropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 210, writableDirectories, setupItem);
|
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");
|
var filename = widget.Get<TextFieldWidget>("FILENAME");
|
||||||
filename.Text = map.Package == null ? "" : mapIsUnpacked ? Path.GetFileName(map.Package.Name) : Path.GetFileNameWithoutExtension(map.Package.Name);
|
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);
|
selectedDirectory.Folder.Delete(combinedPath);
|
||||||
if (fileType == MapFileType.OraMap)
|
if (fileType == MapFileType.OraMap)
|
||||||
package = ZipFile.Create(combinedPath, selectedDirectory.Folder);
|
package = ZipFileLoader.Create(combinedPath, selectedDirectory.Folder);
|
||||||
else
|
else
|
||||||
package = new Folder(combinedPath);
|
package = new Folder(combinedPath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using OpenRA.FileSystem;
|
||||||
using OpenRA.Widgets;
|
using OpenRA.Widgets;
|
||||||
|
|
||||||
namespace OpenRA.Mods.Common.Widgets.Logic
|
namespace OpenRA.Mods.Common.Widgets.Logic
|
||||||
@@ -28,13 +29,15 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
|||||||
bool discAvailable;
|
bool discAvailable;
|
||||||
|
|
||||||
[ObjectCreator.UseCtor]
|
[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;
|
this.content = content;
|
||||||
|
|
||||||
var panel = widget.Get("CONTENT_PANEL");
|
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);
|
modFileSystem.LoadFromManifest(mod);
|
||||||
|
|
||||||
var sourceYaml = MiniYaml.Load(modFileSystem, content.Sources, null);
|
var sourceYaml = MiniYaml.Load(modFileSystem, content.Sources, null);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using OpenRA.FileSystem;
|
||||||
using OpenRA.Widgets;
|
using OpenRA.Widgets;
|
||||||
|
|
||||||
namespace OpenRA.Mods.Common.Widgets.Logic
|
namespace OpenRA.Mods.Common.Widgets.Logic
|
||||||
@@ -18,7 +19,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
|||||||
public class ModContentPromptLogic : ChromeLogic
|
public class ModContentPromptLogic : ChromeLogic
|
||||||
{
|
{
|
||||||
[ObjectCreator.UseCtor]
|
[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");
|
var panel = widget.Get("CONTENT_PROMPT_PANEL");
|
||||||
|
|
||||||
@@ -55,8 +56,11 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
|||||||
quickButton.Bounds.Y += headerHeight;
|
quickButton.Bounds.Y += headerHeight;
|
||||||
quickButton.OnClick = () =>
|
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);
|
modFileSystem.LoadFromManifest(mod);
|
||||||
|
|
||||||
var downloadYaml = MiniYaml.Load(modFileSystem, content.Downloads, null);
|
var downloadYaml = MiniYaml.Load(modFileSystem, content.Downloads, null);
|
||||||
modFileSystem.UnmountAll();
|
modFileSystem.UnmountAll();
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ namespace OpenRA.Mods.D2k.UtilityCommands
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var dest = Path.GetFileNameWithoutExtension(args[1]) + ".oramap";
|
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.");
|
Console.WriteLine(dest + " saved.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ Metadata:
|
|||||||
Title: Tiberian Dawn
|
Title: Tiberian Dawn
|
||||||
Version: {DEV_VERSION}
|
Version: {DEV_VERSION}
|
||||||
|
|
||||||
|
PackageFormats: Mix
|
||||||
|
|
||||||
Packages:
|
Packages:
|
||||||
~^Content/cnc
|
~^Content/cnc
|
||||||
~^Content/cnc/movies
|
~^Content/cnc/movies
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ Metadata:
|
|||||||
Title: Dune 2000
|
Title: Dune 2000
|
||||||
Version: {DEV_VERSION}
|
Version: {DEV_VERSION}
|
||||||
|
|
||||||
|
PackageFormats: D2kSoundResources
|
||||||
|
|
||||||
Packages:
|
Packages:
|
||||||
~^Content/d2k/v2/
|
~^Content/d2k/v2/
|
||||||
~^Content/d2k/v2/GAMESFX
|
~^Content/d2k/v2/GAMESFX
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ Metadata:
|
|||||||
Title: Red Alert
|
Title: Red Alert
|
||||||
Version: {DEV_VERSION}
|
Version: {DEV_VERSION}
|
||||||
|
|
||||||
|
PackageFormats: Mix
|
||||||
|
|
||||||
Packages:
|
Packages:
|
||||||
~^Content/ra/v2/
|
~^Content/ra/v2/
|
||||||
~^Content/ra/v2/expand
|
~^Content/ra/v2/expand
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ Metadata:
|
|||||||
Title: Tiberian Sun
|
Title: Tiberian Sun
|
||||||
Version: {DEV_VERSION}
|
Version: {DEV_VERSION}
|
||||||
|
|
||||||
|
PackageFormats: Mix
|
||||||
|
|
||||||
Packages:
|
Packages:
|
||||||
~^Content/ts
|
~^Content/ts
|
||||||
~^Content/ts/firestorm
|
~^Content/ts/firestorm
|
||||||
|
|||||||
Reference in New Issue
Block a user