Merge pull request #10530 from pchote/remove-external-hashes
Index filesystem contents by filename instead of hash.
This commit is contained in:
@@ -15,11 +15,14 @@ namespace OpenRA
|
|||||||
// Referenced from ModMetadata, so needs to be in OpenRA.Game :(
|
// Referenced from ModMetadata, so needs to be in OpenRA.Game :(
|
||||||
public class ContentInstaller : IGlobalModData
|
public class ContentInstaller : IGlobalModData
|
||||||
{
|
{
|
||||||
|
public enum FilenameCase { Input, ForceLower, ForceUpper }
|
||||||
|
|
||||||
public readonly string[] TestFiles = { };
|
public readonly string[] TestFiles = { };
|
||||||
public readonly string[] DiskTestFiles = { };
|
public readonly string[] DiskTestFiles = { };
|
||||||
public readonly string PackageToExtractFromCD = null;
|
public readonly string PackageToExtractFromCD = null;
|
||||||
public readonly bool OverwriteFiles = true;
|
public readonly bool OverwriteFiles = true;
|
||||||
|
|
||||||
|
public readonly FilenameCase OutputFilenameCase = FilenameCase.ForceLower;
|
||||||
public readonly Dictionary<string, string[]> CopyFilesFromCD = new Dictionary<string, string[]>();
|
public readonly Dictionary<string, string[]> CopyFilesFromCD = new Dictionary<string, string[]>();
|
||||||
public readonly Dictionary<string, string[]> ExtractFilesFromCD = new Dictionary<string, string[]>();
|
public readonly Dictionary<string, string[]> ExtractFilesFromCD = new Dictionary<string, string[]>();
|
||||||
|
|
||||||
|
|||||||
@@ -21,19 +21,15 @@ namespace OpenRA.FileSystem
|
|||||||
{
|
{
|
||||||
public sealed class BagFile : IReadOnlyPackage
|
public sealed class BagFile : IReadOnlyPackage
|
||||||
{
|
{
|
||||||
static readonly uint[] Nothing = { };
|
|
||||||
|
|
||||||
readonly string bagFilename;
|
readonly string bagFilename;
|
||||||
readonly Stream s;
|
readonly Stream s;
|
||||||
readonly int bagFilePriority;
|
readonly int bagFilePriority;
|
||||||
readonly Dictionary<uint, IdxEntry> index;
|
readonly Dictionary<string, IdxEntry> index;
|
||||||
readonly FileSystem context;
|
|
||||||
|
|
||||||
public BagFile(FileSystem context, string filename, int priority)
|
public BagFile(FileSystem context, string filename, int priority)
|
||||||
{
|
{
|
||||||
bagFilename = filename;
|
bagFilename = filename;
|
||||||
bagFilePriority = priority;
|
bagFilePriority = priority;
|
||||||
this.context = context;
|
|
||||||
|
|
||||||
// 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
|
||||||
@@ -44,7 +40,7 @@ namespace OpenRA.FileSystem
|
|||||||
using (var indexStream = context.Open(indexFilename))
|
using (var indexStream = context.Open(indexFilename))
|
||||||
entries = new IdxReader(indexStream).Entries;
|
entries = new IdxReader(indexStream).Entries;
|
||||||
|
|
||||||
index = entries.ToDictionaryWithConflictLog(x => x.Hash,
|
index = entries.ToDictionaryWithConflictLog(x => x.Filename,
|
||||||
"{0} (bag format)".F(filename),
|
"{0} (bag format)".F(filename),
|
||||||
null, x => "(offs={0}, len={1})".F(x.Offset, x.Length));
|
null, x => "(offs={0}, len={1})".F(x.Offset, x.Length));
|
||||||
|
|
||||||
@@ -54,10 +50,10 @@ namespace OpenRA.FileSystem
|
|||||||
public int Priority { get { return 1000 + bagFilePriority; } }
|
public int Priority { get { return 1000 + bagFilePriority; } }
|
||||||
public string Name { get { return bagFilename; } }
|
public string Name { get { return bagFilename; } }
|
||||||
|
|
||||||
public Stream GetContent(uint hash)
|
public Stream GetContent(string filename)
|
||||||
{
|
{
|
||||||
IdxEntry entry;
|
IdxEntry entry;
|
||||||
if (!index.TryGetValue(hash, out entry))
|
if (!index.TryGetValue(filename, out entry))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
s.Seek(entry.Offset, SeekOrigin.Begin);
|
s.Seek(entry.Offset, SeekOrigin.Begin);
|
||||||
@@ -120,59 +116,14 @@ namespace OpenRA.FileSystem
|
|||||||
return mergedStream;
|
return mergedStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint? FindMatchingHash(string filename)
|
|
||||||
{
|
|
||||||
var hash = IdxEntry.HashFilename(filename, PackageHashType.CRC32);
|
|
||||||
if (index.ContainsKey(hash))
|
|
||||||
return hash;
|
|
||||||
|
|
||||||
// Maybe we were given a raw hash?
|
|
||||||
uint raw;
|
|
||||||
if (!uint.TryParse(filename, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out raw))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if ("{0:X}".F(raw) == filename && index.ContainsKey(raw))
|
|
||||||
return raw;
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Stream GetContent(string filename)
|
|
||||||
{
|
|
||||||
var hash = FindMatchingHash(filename);
|
|
||||||
return hash.HasValue ? GetContent(hash.Value) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Exists(string filename)
|
public bool Exists(string filename)
|
||||||
{
|
{
|
||||||
return FindMatchingHash(filename).HasValue;
|
return index.ContainsKey(filename);
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<uint> ClassicHashes()
|
|
||||||
{
|
|
||||||
return Nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<uint> CrcHashes()
|
|
||||||
{
|
|
||||||
return index.Keys;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<string> AllFileNames()
|
public IEnumerable<string> AllFileNames()
|
||||||
{
|
{
|
||||||
var lookup = new Dictionary<uint, string>();
|
return index.Keys;
|
||||||
if (context.Exists("global mix database.dat"))
|
|
||||||
{
|
|
||||||
var db = new XccGlobalDatabase(context.Open("global mix database.dat"));
|
|
||||||
foreach (var e in db.Entries)
|
|
||||||
{
|
|
||||||
var hash = IdxEntry.HashFilename(e, PackageHashType.CRC32);
|
|
||||||
if (!lookup.ContainsKey(hash))
|
|
||||||
lookup.Add(hash, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return index.Keys.Select(k => lookup.ContainsKey(k) ? lookup[k] : "{0:X}".F(k));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|||||||
@@ -96,16 +96,6 @@ namespace OpenRA.FileSystem
|
|||||||
return entries.ContainsKey(filename);
|
return entries.ContainsKey(filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<uint> ClassicHashes()
|
|
||||||
{
|
|
||||||
return entries.Keys.Select(filename => PackageEntry.HashFilename(filename, PackageHashType.Classic));
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<uint> CrcHashes()
|
|
||||||
{
|
|
||||||
return Enumerable.Empty<uint>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<string> AllFileNames()
|
public IEnumerable<string> AllFileNames()
|
||||||
{
|
{
|
||||||
return entries.Keys;
|
return entries.Keys;
|
||||||
|
|||||||
@@ -10,18 +10,29 @@
|
|||||||
|
|
||||||
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 sealed class D2kSoundResources : IReadOnlyPackage
|
||||||
{
|
{
|
||||||
|
struct Entry
|
||||||
|
{
|
||||||
|
public readonly uint Offset;
|
||||||
|
public readonly uint Length;
|
||||||
|
|
||||||
|
public Entry(uint offset, uint length)
|
||||||
|
{
|
||||||
|
Offset = offset;
|
||||||
|
Length = length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
readonly Stream s;
|
readonly Stream s;
|
||||||
|
|
||||||
readonly string filename;
|
readonly string filename;
|
||||||
readonly List<string> filenames;
|
|
||||||
readonly int priority;
|
readonly int priority;
|
||||||
|
readonly Dictionary<string, Entry> index = new Dictionary<string, Entry>();
|
||||||
readonly Dictionary<uint, PackageEntry> index = new Dictionary<uint, PackageEntry>();
|
|
||||||
|
|
||||||
public D2kSoundResources(FileSystem context, string filename, int priority)
|
public D2kSoundResources(FileSystem context, string filename, int priority)
|
||||||
{
|
{
|
||||||
@@ -31,20 +42,13 @@ namespace OpenRA.FileSystem
|
|||||||
s = context.Open(filename);
|
s = context.Open(filename);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
filenames = new List<string>();
|
|
||||||
|
|
||||||
var headerLength = s.ReadUInt32();
|
var headerLength = s.ReadUInt32();
|
||||||
while (s.Position < headerLength + 4)
|
while (s.Position < headerLength + 4)
|
||||||
{
|
{
|
||||||
var name = s.ReadASCIIZ();
|
var name = s.ReadASCIIZ();
|
||||||
var offset = s.ReadUInt32();
|
var offset = s.ReadUInt32();
|
||||||
var length = s.ReadUInt32();
|
var length = s.ReadUInt32();
|
||||||
|
index.Add(name, new Entry(offset, length));
|
||||||
var hash = PackageEntry.HashFilename(name, PackageHashType.Classic);
|
|
||||||
if (!index.ContainsKey(hash))
|
|
||||||
index.Add(hash, new PackageEntry(hash, offset, length));
|
|
||||||
|
|
||||||
filenames.Add(name);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -54,45 +58,30 @@ namespace OpenRA.FileSystem
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Stream GetContent(uint hash)
|
public Stream GetContent(string filename)
|
||||||
{
|
{
|
||||||
PackageEntry e;
|
Entry e;
|
||||||
if (!index.TryGetValue(hash, out e))
|
if (!index.TryGetValue(filename, out e))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
s.Seek(e.Offset, SeekOrigin.Begin);
|
s.Seek(e.Offset, SeekOrigin.Begin);
|
||||||
return new MemoryStream(s.ReadBytes((int)e.Length));
|
return new MemoryStream(s.ReadBytes((int)e.Length));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Stream GetContent(string filename)
|
|
||||||
{
|
|
||||||
return GetContent(PackageEntry.HashFilename(filename, PackageHashType.Classic));
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Exists(string filename)
|
public bool Exists(string filename)
|
||||||
{
|
{
|
||||||
return index.ContainsKey(PackageEntry.HashFilename(filename, PackageHashType.Classic));
|
return index.ContainsKey(filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<string> AllFileNames()
|
public IEnumerable<string> AllFileNames()
|
||||||
{
|
{
|
||||||
return filenames;
|
return index.Keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Name { get { return filename; } }
|
public string Name { get { return filename; } }
|
||||||
|
|
||||||
public int Priority { get { return 1000 + priority; } }
|
public int Priority { get { return 1000 + priority; } }
|
||||||
|
|
||||||
public IEnumerable<uint> ClassicHashes()
|
|
||||||
{
|
|
||||||
return index.Keys;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<uint> CrcHashes()
|
|
||||||
{
|
|
||||||
yield break;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
s.Dispose();
|
s.Dispose();
|
||||||
|
|||||||
@@ -19,14 +19,12 @@ namespace OpenRA.FileSystem
|
|||||||
{
|
{
|
||||||
public class FileSystem
|
public class FileSystem
|
||||||
{
|
{
|
||||||
public readonly List<string> PackagePaths = new List<string>();
|
|
||||||
public readonly List<IReadOnlyPackage> MountedPackages = new List<IReadOnlyPackage>();
|
public readonly List<IReadOnlyPackage> MountedPackages = new List<IReadOnlyPackage>();
|
||||||
|
|
||||||
static readonly Dictionary<string, Assembly> AssemblyCache = new Dictionary<string, Assembly>();
|
static readonly Dictionary<string, Assembly> AssemblyCache = new Dictionary<string, Assembly>();
|
||||||
|
|
||||||
int order;
|
int order;
|
||||||
Cache<uint, List<IReadOnlyPackage>> crcHashIndex = new Cache<uint, List<IReadOnlyPackage>>(_ => new List<IReadOnlyPackage>());
|
Cache<string, List<IReadOnlyPackage>> fileIndex = new Cache<string, List<IReadOnlyPackage>>(_ => new List<IReadOnlyPackage>());
|
||||||
Cache<uint, List<IReadOnlyPackage>> classicHashIndex = new Cache<uint, List<IReadOnlyPackage>>(_ => new List<IReadOnlyPackage>());
|
|
||||||
|
|
||||||
public IReadWritePackage CreatePackage(string filename, int order, Dictionary<string, byte[]> content)
|
public IReadWritePackage CreatePackage(string filename, int order, Dictionary<string, byte[]> content)
|
||||||
{
|
{
|
||||||
@@ -93,7 +91,6 @@ namespace OpenRA.FileSystem
|
|||||||
|
|
||||||
name = Platform.ResolvePath(name);
|
name = Platform.ResolvePath(name);
|
||||||
|
|
||||||
PackagePaths.Add(name);
|
|
||||||
Action a = () => MountInner(OpenPackage(name, annotation, order++));
|
Action a = () => MountInner(OpenPackage(name, annotation, order++));
|
||||||
|
|
||||||
if (optional)
|
if (optional)
|
||||||
@@ -107,27 +104,23 @@ namespace OpenRA.FileSystem
|
|||||||
{
|
{
|
||||||
MountedPackages.Add(package);
|
MountedPackages.Add(package);
|
||||||
|
|
||||||
foreach (var hash in package.ClassicHashes())
|
foreach (var filename in package.AllFileNames())
|
||||||
{
|
{
|
||||||
var packageList = classicHashIndex[hash];
|
var packageList = fileIndex[filename];
|
||||||
if (!packageList.Contains(package))
|
|
||||||
packageList.Add(package);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var hash in package.CrcHashes())
|
|
||||||
{
|
|
||||||
var packageList = crcHashIndex[hash];
|
|
||||||
if (!packageList.Contains(package))
|
if (!packageList.Contains(package))
|
||||||
packageList.Add(package);
|
packageList.Add(package);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Unmount(IReadOnlyPackage mount)
|
public bool Unmount(IReadOnlyPackage package)
|
||||||
{
|
{
|
||||||
if (MountedPackages.Contains(mount))
|
foreach (var packagesForFile in fileIndex.Values)
|
||||||
mount.Dispose();
|
packagesForFile.RemoveAll(p => p == package);
|
||||||
|
|
||||||
return MountedPackages.RemoveAll(f => f == mount) > 0;
|
if (MountedPackages.Contains(package))
|
||||||
|
package.Dispose();
|
||||||
|
|
||||||
|
return MountedPackages.RemoveAll(p => p == package) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UnmountAll()
|
public void UnmountAll()
|
||||||
@@ -136,9 +129,7 @@ namespace OpenRA.FileSystem
|
|||||||
package.Dispose();
|
package.Dispose();
|
||||||
|
|
||||||
MountedPackages.Clear();
|
MountedPackages.Clear();
|
||||||
PackagePaths.Clear();
|
fileIndex = new Cache<string, List<IReadOnlyPackage>>(_ => new List<IReadOnlyPackage>());
|
||||||
classicHashIndex = new Cache<uint, List<IReadOnlyPackage>>(_ => new List<IReadOnlyPackage>());
|
|
||||||
crcHashIndex = new Cache<uint, List<IReadOnlyPackage>>(_ => new List<IReadOnlyPackage>());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadFromManifest(Manifest manifest)
|
public void LoadFromManifest(Manifest manifest)
|
||||||
@@ -151,10 +142,9 @@ namespace OpenRA.FileSystem
|
|||||||
Mount(pkg.Key, pkg.Value);
|
Mount(pkg.Key, pkg.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream GetFromCache(PackageHashType type, string filename)
|
Stream GetFromCache(string filename)
|
||||||
{
|
{
|
||||||
var index = type == PackageHashType.CRC32 ? crcHashIndex : classicHashIndex;
|
var package = fileIndex[filename]
|
||||||
var package = index[PackageEntry.HashFilename(filename, type)]
|
|
||||||
.Where(x => x.Exists(filename))
|
.Where(x => x.Exists(filename))
|
||||||
.MinByOrDefault(x => x.Priority);
|
.MinByOrDefault(x => x.Priority);
|
||||||
|
|
||||||
@@ -191,11 +181,7 @@ namespace OpenRA.FileSystem
|
|||||||
// TODO: This disables caching for explicit package requests
|
// TODO: This disables caching for explicit package requests
|
||||||
if (filename.IndexOfAny(new[] { '/', '\\' }) == -1 && !explicitPackage)
|
if (filename.IndexOfAny(new[] { '/', '\\' }) == -1 && !explicitPackage)
|
||||||
{
|
{
|
||||||
s = GetFromCache(PackageHashType.Classic, filename);
|
s = GetFromCache(filename);
|
||||||
if (s != null)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
s = GetFromCache(PackageHashType.CRC32, filename);
|
|
||||||
if (s != null)
|
if (s != null)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,17 +43,6 @@ namespace OpenRA.FileSystem
|
|||||||
catch { return null; }
|
catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<uint> ClassicHashes()
|
|
||||||
{
|
|
||||||
foreach (var filename in Directory.GetFiles(path, "*", SearchOption.TopDirectoryOnly))
|
|
||||||
yield return PackageEntry.HashFilename(Path.GetFileName(filename), PackageHashType.Classic);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<uint> CrcHashes()
|
|
||||||
{
|
|
||||||
yield break;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<string> AllFileNames()
|
public IEnumerable<string> AllFileNames()
|
||||||
{
|
{
|
||||||
foreach (var filename in Directory.GetFiles(path, "*", SearchOption.TopDirectoryOnly))
|
foreach (var filename in Directory.GetFiles(path, "*", SearchOption.TopDirectoryOnly))
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ namespace OpenRA.FileSystem
|
|||||||
{
|
{
|
||||||
Stream GetContent(string filename);
|
Stream GetContent(string filename);
|
||||||
bool Exists(string filename);
|
bool Exists(string filename);
|
||||||
IEnumerable<uint> ClassicHashes();
|
|
||||||
IEnumerable<uint> CrcHashes();
|
|
||||||
IEnumerable<string> AllFileNames();
|
IEnumerable<string> AllFileNames();
|
||||||
int Priority { get; }
|
int Priority { get; }
|
||||||
string Name { get; }
|
string Name { get; }
|
||||||
|
|||||||
@@ -15,80 +15,31 @@ namespace OpenRA.FileSystem
|
|||||||
{
|
{
|
||||||
public class IdxEntry
|
public class IdxEntry
|
||||||
{
|
{
|
||||||
public const string DefaultExtension = "wav";
|
public readonly string Filename;
|
||||||
|
|
||||||
public readonly uint Hash;
|
|
||||||
public readonly string Name;
|
|
||||||
public readonly string Extension;
|
|
||||||
public readonly uint Offset;
|
public readonly uint Offset;
|
||||||
public readonly uint Length;
|
public readonly uint Length;
|
||||||
public readonly uint SampleRate;
|
public readonly uint SampleRate;
|
||||||
public readonly uint Flags;
|
public readonly uint Flags;
|
||||||
public readonly uint ChunkSize;
|
public readonly uint ChunkSize;
|
||||||
|
|
||||||
public IdxEntry(uint hash, uint offset, uint length, uint sampleRate, uint flags, uint chuckSize)
|
|
||||||
{
|
|
||||||
Hash = hash;
|
|
||||||
Offset = offset;
|
|
||||||
Length = length;
|
|
||||||
SampleRate = sampleRate;
|
|
||||||
Flags = flags;
|
|
||||||
ChunkSize = chuckSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IdxEntry(Stream s)
|
public IdxEntry(Stream s)
|
||||||
{
|
{
|
||||||
var asciiname = s.ReadASCII(16);
|
var name = s.ReadASCII(16);
|
||||||
|
var pos = name.IndexOf('\0');
|
||||||
var pos = asciiname.IndexOf('\0');
|
|
||||||
if (pos != 0)
|
if (pos != 0)
|
||||||
asciiname = asciiname.Substring(0, pos);
|
name = name.Substring(0, pos);
|
||||||
|
|
||||||
Name = asciiname;
|
Filename = string.Concat(name, ".wav");
|
||||||
Extension = DefaultExtension;
|
|
||||||
Offset = s.ReadUInt32();
|
Offset = s.ReadUInt32();
|
||||||
Length = s.ReadUInt32();
|
Length = s.ReadUInt32();
|
||||||
SampleRate = s.ReadUInt32();
|
SampleRate = s.ReadUInt32();
|
||||||
Flags = s.ReadUInt32();
|
Flags = s.ReadUInt32();
|
||||||
ChunkSize = s.ReadUInt32();
|
ChunkSize = s.ReadUInt32();
|
||||||
Hash = HashFilename(string.Concat(Name, ".", Extension), PackageHashType.CRC32);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Write(BinaryWriter w)
|
|
||||||
{
|
|
||||||
w.Write(Name.PadRight(16, '\0'));
|
|
||||||
w.Write(Offset);
|
|
||||||
w.Write(Length);
|
|
||||||
w.Write(SampleRate);
|
|
||||||
w.Write(Flags);
|
|
||||||
w.Write(ChunkSize);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
string filename;
|
return "{0} - offset 0x{1:x8} - length 0x{2:x8}".F(Filename, Offset, Length);
|
||||||
if (names.TryGetValue(Hash, out filename))
|
|
||||||
return "{0} - offset 0x{1:x8} - length 0x{2:x8}".F(filename, Offset, Length);
|
|
||||||
else
|
|
||||||
return "0x{0:x8} - offset 0x{1:x8} - length 0x{2:x8}".F(Hash, Offset, Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static uint HashFilename(string name, PackageHashType type)
|
|
||||||
{
|
|
||||||
return PackageEntry.HashFilename(name, type);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Dictionary<uint, string> names = new Dictionary<uint, string>();
|
|
||||||
|
|
||||||
public static void AddStandardName(string s)
|
|
||||||
{
|
|
||||||
// RA1 and TD
|
|
||||||
var hash = HashFilename(s, PackageHashType.Classic);
|
|
||||||
names.Add(hash, s);
|
|
||||||
|
|
||||||
// TS
|
|
||||||
var crcHash = HashFilename(s, PackageHashType.CRC32);
|
|
||||||
names.Add(crcHash, s);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -451,11 +451,6 @@ namespace OpenRA.FileSystem
|
|||||||
GetContentById(index, destfile);
|
GetContentById(index, destfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<uint> ClassicHashes()
|
|
||||||
{
|
|
||||||
return fileLookup.Keys.Select(k => PackageEntry.HashFilename(k, PackageHashType.Classic));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Stream GetContentById(uint index)
|
public Stream GetContentById(uint index)
|
||||||
{
|
{
|
||||||
var fileDes = fileDescriptors[index];
|
var fileDes = fileDescriptors[index];
|
||||||
@@ -507,11 +502,6 @@ namespace OpenRA.FileSystem
|
|||||||
return GetContentById(fileLookup[fileName]);
|
return GetContentById(fileLookup[fileName]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<uint> CrcHashes()
|
|
||||||
{
|
|
||||||
yield break;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<string> AllFileNames()
|
public IEnumerable<string> AllFileNames()
|
||||||
{
|
{
|
||||||
return fileLookup.Keys;
|
return fileLookup.Keys;
|
||||||
|
|||||||
@@ -17,8 +17,19 @@ namespace OpenRA.FileSystem
|
|||||||
{
|
{
|
||||||
public sealed class InstallShieldPackage : IReadOnlyPackage
|
public sealed class InstallShieldPackage : IReadOnlyPackage
|
||||||
{
|
{
|
||||||
readonly Dictionary<uint, PackageEntry> index = new Dictionary<uint, PackageEntry>();
|
struct Entry
|
||||||
readonly List<string> filenames;
|
{
|
||||||
|
public readonly uint Offset;
|
||||||
|
public readonly uint Length;
|
||||||
|
|
||||||
|
public Entry(uint offset, uint length)
|
||||||
|
{
|
||||||
|
Offset = offset;
|
||||||
|
Length = length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly Dictionary<string, Entry> index = new Dictionary<string, Entry>();
|
||||||
readonly Stream s;
|
readonly Stream s;
|
||||||
readonly long dataStart = 255;
|
readonly long dataStart = 255;
|
||||||
readonly int priority;
|
readonly int priority;
|
||||||
@@ -29,40 +40,45 @@ namespace OpenRA.FileSystem
|
|||||||
this.filename = filename;
|
this.filename = filename;
|
||||||
this.priority = priority;
|
this.priority = priority;
|
||||||
|
|
||||||
filenames = new List<string>();
|
|
||||||
|
|
||||||
s = context.Open(filename);
|
s = context.Open(filename);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Parse package header
|
// Parse package header
|
||||||
var reader = new BinaryReader(s);
|
var signature = s.ReadUInt32();
|
||||||
var signature = reader.ReadUInt32();
|
|
||||||
if (signature != 0x8C655D13)
|
if (signature != 0x8C655D13)
|
||||||
throw new InvalidDataException("Not an Installshield package");
|
throw new InvalidDataException("Not an Installshield package");
|
||||||
|
|
||||||
reader.ReadBytes(8);
|
s.Position += 8;
|
||||||
/*var FileCount = */reader.ReadUInt16();
|
/*var FileCount = */s.ReadUInt16();
|
||||||
reader.ReadBytes(4);
|
s.Position += 4;
|
||||||
/*var ArchiveSize = */reader.ReadUInt32();
|
/*var ArchiveSize = */s.ReadUInt32();
|
||||||
reader.ReadBytes(19);
|
s.Position += 19;
|
||||||
var tocAddress = reader.ReadInt32();
|
var tocAddress = s.ReadInt32();
|
||||||
reader.ReadBytes(4);
|
s.Position += 4;
|
||||||
var dirCount = reader.ReadUInt16();
|
var dirCount = s.ReadUInt16();
|
||||||
|
|
||||||
// Parse the directory list
|
// Parse the directory list
|
||||||
s.Seek(tocAddress, SeekOrigin.Begin);
|
s.Position = tocAddress;
|
||||||
var tocReader = new BinaryReader(s);
|
|
||||||
|
|
||||||
var fileCountInDirs = new List<uint>();
|
|
||||||
|
|
||||||
// Parse directories
|
// Parse directories
|
||||||
|
var directories = new Dictionary<string, uint>();
|
||||||
for (var i = 0; i < dirCount; i++)
|
for (var i = 0; i < dirCount; i++)
|
||||||
fileCountInDirs.Add(ParseDirectory(tocReader));
|
{
|
||||||
|
// 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
|
// Parse files
|
||||||
foreach (var fileCount in fileCountInDirs)
|
foreach (var dir in directories)
|
||||||
for (var i = 0; i < fileCount; i++)
|
for (var i = 0; i < dir.Value; i++)
|
||||||
ParseFile(reader);
|
ParseFile(s, dir.Key);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -71,44 +87,29 @@ namespace OpenRA.FileSystem
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static uint ParseDirectory(BinaryReader reader)
|
|
||||||
{
|
|
||||||
// Parse directory header
|
|
||||||
var fileCount = reader.ReadUInt16();
|
|
||||||
var chunkSize = reader.ReadUInt16();
|
|
||||||
var nameLength = reader.ReadUInt16();
|
|
||||||
reader.ReadChars(nameLength); // var DirName = new String(reader.ReadChars(NameLength));
|
|
||||||
|
|
||||||
// Skip to the end of the chunk
|
|
||||||
reader.ReadBytes(chunkSize - nameLength - 6);
|
|
||||||
return fileCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint accumulatedData = 0;
|
uint accumulatedData = 0;
|
||||||
void ParseFile(BinaryReader reader)
|
void ParseFile(Stream s, string dirName)
|
||||||
{
|
{
|
||||||
reader.ReadBytes(7);
|
s.Position += 7;
|
||||||
var compressedSize = reader.ReadUInt32();
|
var compressedSize = s.ReadUInt32();
|
||||||
reader.ReadBytes(12);
|
s.Position += 12;
|
||||||
var chunkSize = reader.ReadUInt16();
|
var chunkSize = s.ReadUInt16();
|
||||||
reader.ReadBytes(4);
|
s.Position += 4;
|
||||||
var nameLength = reader.ReadByte();
|
var nameLength = s.ReadByte();
|
||||||
var fileName = new string(reader.ReadChars(nameLength));
|
var fileName = dirName + "\\" + s.ReadASCII(nameLength);
|
||||||
|
|
||||||
var hash = PackageEntry.HashFilename(fileName, PackageHashType.Classic);
|
// Use index syntax to overwrite any duplicate entries with the last value
|
||||||
if (!index.ContainsKey(hash))
|
index[fileName] = new Entry(accumulatedData, compressedSize);
|
||||||
index.Add(hash, new PackageEntry(hash, accumulatedData, compressedSize));
|
|
||||||
filenames.Add(fileName);
|
|
||||||
accumulatedData += compressedSize;
|
accumulatedData += compressedSize;
|
||||||
|
|
||||||
// Skip to the end of the chunk
|
// Skip to the end of the chunk
|
||||||
reader.ReadBytes(chunkSize - nameLength - 30);
|
s.Position += chunkSize - nameLength - 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Stream GetContent(uint hash)
|
public Stream GetContent(string filename)
|
||||||
{
|
{
|
||||||
PackageEntry e;
|
Entry e;
|
||||||
if (!index.TryGetValue(hash, out e))
|
if (!index.TryGetValue(filename, out e))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
s.Seek(dataStart + e.Offset, SeekOrigin.Begin);
|
s.Seek(dataStart + e.Offset, SeekOrigin.Begin);
|
||||||
@@ -117,39 +118,19 @@ namespace OpenRA.FileSystem
|
|||||||
return new MemoryStream(Blast.Decompress(data));
|
return new MemoryStream(Blast.Decompress(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Stream GetContent(string filename)
|
public IEnumerable<string> AllFileNames()
|
||||||
{
|
|
||||||
return GetContent(PackageEntry.HashFilename(filename, PackageHashType.Classic));
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<uint> ClassicHashes()
|
|
||||||
{
|
{
|
||||||
return index.Keys;
|
return index.Keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<uint> CrcHashes()
|
|
||||||
{
|
|
||||||
yield break;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<string> AllFileNames()
|
|
||||||
{
|
|
||||||
return filenames;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Exists(string filename)
|
public bool Exists(string filename)
|
||||||
{
|
{
|
||||||
return index.ContainsKey(PackageEntry.HashFilename(filename, PackageHashType.Classic));
|
return index.ContainsKey(filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int Priority { get { return 2000 + priority; } }
|
public int Priority { get { return 2000 + priority; } }
|
||||||
public string Name { get { return filename; } }
|
public string Name { get { return filename; } }
|
||||||
|
|
||||||
public void Write(Dictionary<string, byte[]> contents)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException("Cannot save InstallShieldPackages.");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
s.Dispose();
|
s.Dispose();
|
||||||
|
|||||||
@@ -170,23 +170,6 @@ namespace OpenRA.FileSystem
|
|||||||
return hash.HasValue ? GetContent(hash.Value) : null;
|
return hash.HasValue ? GetContent(hash.Value) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static readonly uint[] Nothing = { };
|
|
||||||
public IEnumerable<uint> ClassicHashes()
|
|
||||||
{
|
|
||||||
if (type == PackageHashType.Classic)
|
|
||||||
return index.Keys;
|
|
||||||
|
|
||||||
return Nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<uint> CrcHashes()
|
|
||||||
{
|
|
||||||
if (type == PackageHashType.CRC32)
|
|
||||||
return index.Keys;
|
|
||||||
|
|
||||||
return Nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<string> AllFileNames()
|
public IEnumerable<string> AllFileNames()
|
||||||
{
|
{
|
||||||
var lookup = new Dictionary<uint, string>();
|
var lookup = new Dictionary<uint, string>();
|
||||||
|
|||||||
@@ -70,17 +70,6 @@ namespace OpenRA.FileSystem
|
|||||||
return new MemoryStream(data);
|
return new MemoryStream(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<uint> ClassicHashes()
|
|
||||||
{
|
|
||||||
foreach (var filename in index.Keys)
|
|
||||||
yield return PackageEntry.HashFilename(filename, PackageHashType.Classic);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<uint> CrcHashes()
|
|
||||||
{
|
|
||||||
yield break;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<string> AllFileNames()
|
public IEnumerable<string> AllFileNames()
|
||||||
{
|
{
|
||||||
foreach (var filename in index.Keys)
|
foreach (var filename in index.Keys)
|
||||||
|
|||||||
@@ -71,17 +71,6 @@ namespace OpenRA.FileSystem
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<uint> ClassicHashes()
|
|
||||||
{
|
|
||||||
foreach (ZipEntry entry in pkg)
|
|
||||||
yield return PackageEntry.HashFilename(entry.Name, PackageHashType.Classic);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<uint> CrcHashes()
|
|
||||||
{
|
|
||||||
yield break;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<string> AllFileNames()
|
public IEnumerable<string> AllFileNames()
|
||||||
{
|
{
|
||||||
foreach (ZipEntry entry in pkg)
|
foreach (ZipEntry entry in pkg)
|
||||||
|
|||||||
@@ -37,9 +37,26 @@ namespace OpenRA.Mods.Common
|
|||||||
return volumes.FirstOrDefault(isValidDisk);
|
return volumes.FirstOrDefault(isValidDisk);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static string GetFileName(string path, ContentInstaller.FilenameCase caseModifier)
|
||||||
|
{
|
||||||
|
// Gets the file path, splitting on both / and \
|
||||||
|
var index = path.LastIndexOfAny(new[] { '\\', '/' });
|
||||||
|
var output = path.Substring(index + 1);
|
||||||
|
|
||||||
|
switch (caseModifier)
|
||||||
|
{
|
||||||
|
case ContentInstaller.FilenameCase.ForceLower:
|
||||||
|
return output.ToLowerInvariant();
|
||||||
|
case ContentInstaller.FilenameCase.ForceUpper:
|
||||||
|
return output.ToUpperInvariant();
|
||||||
|
default:
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: The package should be mounted into its own context to avoid name collisions with installed files
|
// TODO: The package should be mounted into its own context to avoid name collisions with installed files
|
||||||
public static bool ExtractFromPackage(string srcPath, string package, string annotation, Dictionary<string, string[]> filesByDirectory,
|
public static bool ExtractFromPackage(string srcPath, string package, string annotation, Dictionary<string, string[]> filesByDirectory,
|
||||||
string destPath, bool overwrite, Action<string> onProgress, Action<string> onError)
|
string destPath, bool overwrite, ContentInstaller.FilenameCase caseModifier, Action<string> onProgress, Action<string> onError)
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(destPath);
|
Directory.CreateDirectory(destPath);
|
||||||
|
|
||||||
@@ -55,7 +72,7 @@ namespace OpenRA.Mods.Common
|
|||||||
foreach (var file in directory.Value)
|
foreach (var file in directory.Value)
|
||||||
{
|
{
|
||||||
var containingDir = Path.Combine(destPath, targetDir);
|
var containingDir = Path.Combine(destPath, targetDir);
|
||||||
var dest = Path.Combine(containingDir, file.ToLowerInvariant());
|
var dest = Path.Combine(containingDir, GetFileName(file, caseModifier));
|
||||||
if (File.Exists(dest))
|
if (File.Exists(dest))
|
||||||
{
|
{
|
||||||
if (overwrite)
|
if (overwrite)
|
||||||
@@ -83,7 +100,7 @@ namespace OpenRA.Mods.Common
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static bool CopyFiles(string srcPath, Dictionary<string, string[]> files, string destPath,
|
public static bool CopyFiles(string srcPath, Dictionary<string, string[]> files, string destPath,
|
||||||
bool overwrite, Action<string> onProgress, Action<string> onError)
|
bool overwrite, ContentInstaller.FilenameCase caseModifier, Action<string> onProgress, Action<string> onError)
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(destPath);
|
Directory.CreateDirectory(destPath);
|
||||||
|
|
||||||
@@ -100,9 +117,9 @@ namespace OpenRA.Mods.Common
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var destFile = Path.GetFileName(file);
|
var destFile = GetFileName(file, caseModifier);
|
||||||
var containingDir = Path.Combine(destPath, targetDir);
|
var containingDir = Path.Combine(destPath, targetDir);
|
||||||
var dest = Path.Combine(containingDir, destFile.ToLowerInvariant());
|
var dest = Path.Combine(containingDir, destFile);
|
||||||
if (File.Exists(dest) && !overwrite)
|
if (File.Exists(dest) && !overwrite)
|
||||||
{
|
{
|
||||||
Log.Write("debug", "Skipping {0}".F(dest));
|
Log.Write("debug", "Skipping {0}".F(dest));
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
|||||||
var destFile = Platform.ResolvePath("^", "Content", modId, filename.ToLowerInvariant());
|
var destFile = Platform.ResolvePath("^", "Content", modId, filename.ToLowerInvariant());
|
||||||
cabExtractor.ExtractFile(uint.Parse(archive[0]), destFile);
|
cabExtractor.ExtractFile(uint.Parse(archive[0]), destFile);
|
||||||
var annotation = archive.Length > 1 ? archive[1] : null;
|
var annotation = archive.Length > 1 ? archive[1] : null;
|
||||||
InstallUtils.ExtractFromPackage(source, destFile, annotation, extractFiles, destDir, overwrite, onProgress, onError);
|
InstallUtils.ExtractFromPackage(source, destFile, annotation, extractFiles, destDir, overwrite, installData.OutputFilenameCase, onProgress, onError);
|
||||||
progressBar.Percentage += installPercent;
|
progressBar.Percentage += installPercent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,7 +183,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!InstallUtils.CopyFiles(source, copyFiles, dest, overwrite, onProgress, onError))
|
if (!InstallUtils.CopyFiles(source, copyFiles, dest, overwrite, installData.OutputFilenameCase, onProgress, onError))
|
||||||
{
|
{
|
||||||
onError("Copying files from CD failed.");
|
onError("Copying files from CD failed.");
|
||||||
return;
|
return;
|
||||||
@@ -191,7 +191,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
|||||||
|
|
||||||
if (!string.IsNullOrEmpty(extractPackage))
|
if (!string.IsNullOrEmpty(extractPackage))
|
||||||
{
|
{
|
||||||
if (!InstallUtils.ExtractFromPackage(source, extractPackage, annotation, extractFiles, dest, overwrite, onProgress, onError))
|
if (!InstallUtils.ExtractFromPackage(source, extractPackage, annotation, extractFiles, dest,
|
||||||
|
overwrite, installData.OutputFilenameCase, onProgress, onError))
|
||||||
{
|
{
|
||||||
onError("Extracting files from CD failed.");
|
onError("Extracting files from CD failed.");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -146,13 +146,11 @@ LoadScreen: CncLoadScreen
|
|||||||
|
|
||||||
ContentInstaller:
|
ContentInstaller:
|
||||||
TestFiles: ^Content/cnc/conquer.mix, ^Content/cnc/desert.mix, ^Content/cnc/sounds.mix, ^Content/cnc/speech.mix, ^Content/cnc/temperat.mix, ^Content/cnc/tempicnh.mix, ^Content/cnc/winter.mix
|
TestFiles: ^Content/cnc/conquer.mix, ^Content/cnc/desert.mix, ^Content/cnc/sounds.mix, ^Content/cnc/speech.mix, ^Content/cnc/temperat.mix, ^Content/cnc/tempicnh.mix, ^Content/cnc/winter.mix
|
||||||
FilesToCopy: CONQUER.MIX, DESERT.MIX, SCORES.MIX, SOUNDS.MIX, TEMPERAT.MIX, WINTER.MIX
|
|
||||||
FilesToExtract: speech.mix, tempicnh.mix, transit.mix
|
|
||||||
PackageMirrorList: http://www.openra.net/packages/cnc-mirrors.txt
|
PackageMirrorList: http://www.openra.net/packages/cnc-mirrors.txt
|
||||||
DiskTestFiles: conquer.mix, desert.mix, install/setup.z
|
DiskTestFiles: conquer.mix, desert.mix, install/setup.z
|
||||||
PackageToExtractFromCD: install/setup.z
|
PackageToExtractFromCD: install/setup.z
|
||||||
ExtractFilesFromCD:
|
ExtractFilesFromCD:
|
||||||
.: speech.mix, tempicnh.mix, transit.mix
|
.: C&C95\SPEECH.MIX, C&C95\TEMPICNH.MIX, C&C95\TRANSIT.MIX
|
||||||
CopyFilesFromCD:
|
CopyFilesFromCD:
|
||||||
.: conquer.mix, desert.mix, general.mix, scores.mix, sounds.mix, temperat.mix, winter.mix
|
.: conquer.mix, desert.mix, general.mix, scores.mix, sounds.mix, temperat.mix, winter.mix
|
||||||
ShippedSoundtracks: 4
|
ShippedSoundtracks: 4
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Visibility: MissionSelector
|
|||||||
Type: Campaign
|
Type: Campaign
|
||||||
|
|
||||||
Videos:
|
Videos:
|
||||||
Briefing: a_br01_e.vqa
|
Briefing: A_BR01_E.VQA
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
Crates: False
|
Crates: False
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Visibility: MissionSelector
|
|||||||
Type: Campaign
|
Type: Campaign
|
||||||
|
|
||||||
Videos:
|
Videos:
|
||||||
Briefing: a_br01_e.vqa
|
Briefing: A_BR01_E.VQA
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
Crates: False
|
Crates: False
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Visibility: MissionSelector
|
|||||||
Type: Campaign
|
Type: Campaign
|
||||||
|
|
||||||
Videos:
|
Videos:
|
||||||
Briefing: a_br02_e.vqa
|
Briefing: A_BR02_E.VQA
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
Crates: False
|
Crates: False
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Visibility: MissionSelector
|
|||||||
Type: Campaign
|
Type: Campaign
|
||||||
|
|
||||||
Videos:
|
Videos:
|
||||||
Briefing: a_br02_e.vqa
|
Briefing: A_BR02_E.VQA
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
Crates: False
|
Crates: False
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Visibility: MissionSelector
|
|||||||
Type: Campaign
|
Type: Campaign
|
||||||
|
|
||||||
Videos:
|
Videos:
|
||||||
Briefing: a_br03_e.vqa
|
Briefing: A_BR03_E.VQA
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
Crates: False
|
Crates: False
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user