Merge pull request #7573 from bvandenbogaard/bleed
Added audio.bag/audio.idx support used in RA2
This commit is contained in:
44
OpenRA.Game/FileFormats/IdxReader.cs
Normal file
44
OpenRA.Game/FileFormats/IdxReader.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
#region Copyright & License Information
|
||||||
|
/*
|
||||||
|
* Copyright 2007-2015 The OpenRA Developers (see AUTHORS)
|
||||||
|
* This file is part of OpenRA, which is free software. It is made
|
||||||
|
* available to you under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation. For more information,
|
||||||
|
* see COPYING.
|
||||||
|
*/
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using OpenRA.FileSystem;
|
||||||
|
|
||||||
|
namespace OpenRA.FileFormats
|
||||||
|
{
|
||||||
|
public class IdxReader
|
||||||
|
{
|
||||||
|
public readonly int SoundCount;
|
||||||
|
public List<IdxEntry> Entries;
|
||||||
|
|
||||||
|
public IdxReader(Stream s)
|
||||||
|
{
|
||||||
|
s.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
var id = s.ReadASCII(4);
|
||||||
|
|
||||||
|
if (id != "GABA")
|
||||||
|
throw new InvalidDataException("Unable to load Idx file, did not find magic id, found {0} instead".F(id));
|
||||||
|
|
||||||
|
var two = s.ReadInt32();
|
||||||
|
|
||||||
|
if (two != 2)
|
||||||
|
throw new InvalidDataException("Unable to load Idx file, did not find magic number 2, found {0} instead".F(two));
|
||||||
|
|
||||||
|
SoundCount = s.ReadInt32();
|
||||||
|
|
||||||
|
Entries = new List<IdxEntry>();
|
||||||
|
|
||||||
|
for (var i = 0; i < SoundCount; i++)
|
||||||
|
Entries.Add(new IdxEntry(s));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
192
OpenRA.Game/FileSystem/BagFile.cs
Normal file
192
OpenRA.Game/FileSystem/BagFile.cs
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
#region Copyright & License Information
|
||||||
|
/*
|
||||||
|
* Copyright 2007-2015 The OpenRA Developers (see AUTHORS)
|
||||||
|
* This file is part of OpenRA, which is free software. It is made
|
||||||
|
* available to you under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation. For more information,
|
||||||
|
* see COPYING.
|
||||||
|
*/
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using OpenRA.FileFormats;
|
||||||
|
using OpenRA.Primitives;
|
||||||
|
|
||||||
|
namespace OpenRA.FileSystem
|
||||||
|
{
|
||||||
|
public sealed class BagFile : IFolder, IDisposable
|
||||||
|
{
|
||||||
|
static readonly uint[] Nothing = { };
|
||||||
|
|
||||||
|
readonly string bagFilename;
|
||||||
|
readonly Stream s;
|
||||||
|
readonly int bagFilePriority;
|
||||||
|
readonly Dictionary<uint, IdxEntry> index;
|
||||||
|
|
||||||
|
public BagFile(string filename, int priority)
|
||||||
|
{
|
||||||
|
bagFilename = filename;
|
||||||
|
bagFilePriority = priority;
|
||||||
|
|
||||||
|
// A bag file is always accompanied with an .idx counterpart
|
||||||
|
// For example: audio.bag requires the audio.idx file
|
||||||
|
var indexFilename = Path.ChangeExtension(filename, ".idx");
|
||||||
|
|
||||||
|
s = GlobalFileSystem.Open(filename);
|
||||||
|
|
||||||
|
// Build the index and dispose the stream, it is no longer needed after this
|
||||||
|
List<IdxEntry> entries;
|
||||||
|
using (var indexStream = GlobalFileSystem.Open(indexFilename))
|
||||||
|
{
|
||||||
|
var reader = new IdxReader(indexStream);
|
||||||
|
|
||||||
|
entries = reader.Entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
index = entries.ToDictionaryWithConflictLog(x => x.Hash,
|
||||||
|
"{0} (bag format)".F(filename),
|
||||||
|
null, x => "(offs={0}, len={1})".F(x.Offset, x.Length));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Priority { get { return 1000 + bagFilePriority; } }
|
||||||
|
public string Name { get { return bagFilename; } }
|
||||||
|
|
||||||
|
public Stream GetContent(uint hash)
|
||||||
|
{
|
||||||
|
IdxEntry entry;
|
||||||
|
if (!index.TryGetValue(hash, 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)WavLoader.WaveType.Pcm));
|
||||||
|
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)WavLoader.WaveType.ImaAdpcm));
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
return FindMatchingHash(filename).HasValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<uint> ClassicHashes()
|
||||||
|
{
|
||||||
|
return Nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<uint> CrcHashes()
|
||||||
|
{
|
||||||
|
return index.Keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<string> AllFileNames()
|
||||||
|
{
|
||||||
|
var lookup = new Dictionary<uint, string>();
|
||||||
|
if (GlobalFileSystem.Exists("global mix database.dat"))
|
||||||
|
{
|
||||||
|
var db = new XccGlobalDatabase(GlobalFileSystem.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 Write(Dictionary<string, byte[]> contents)
|
||||||
|
{
|
||||||
|
GlobalFileSystem.Unmount(this);
|
||||||
|
throw new NotImplementedException("Updating bag files unsupported");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (s != null)
|
||||||
|
s.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -101,6 +101,8 @@ namespace OpenRA.FileSystem
|
|||||||
return new PakFile(filename, order);
|
return new PakFile(filename, order);
|
||||||
if (filename.EndsWith(".big", StringComparison.InvariantCultureIgnoreCase))
|
if (filename.EndsWith(".big", StringComparison.InvariantCultureIgnoreCase))
|
||||||
return new BigFile(filename, order);
|
return new BigFile(filename, order);
|
||||||
|
if (filename.EndsWith(".bag", StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
return new BagFile(filename, order);
|
||||||
|
|
||||||
return new Folder(filename, order);
|
return new Folder(filename, order);
|
||||||
}
|
}
|
||||||
|
|||||||
94
OpenRA.Game/FileSystem/IdxEntry.cs
Normal file
94
OpenRA.Game/FileSystem/IdxEntry.cs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
#region Copyright & License Information
|
||||||
|
/*
|
||||||
|
* Copyright 2007-2015 The OpenRA Developers (see AUTHORS)
|
||||||
|
* This file is part of OpenRA, which is free software. It is made
|
||||||
|
* available to you under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation. For more information,
|
||||||
|
* see COPYING.
|
||||||
|
*/
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace OpenRA.FileSystem
|
||||||
|
{
|
||||||
|
public class IdxEntry
|
||||||
|
{
|
||||||
|
public const string DefaultExtension = "wav";
|
||||||
|
|
||||||
|
public readonly uint Hash;
|
||||||
|
public readonly string Name;
|
||||||
|
public readonly string Extension;
|
||||||
|
public readonly uint Offset;
|
||||||
|
public readonly uint Length;
|
||||||
|
public readonly uint SampleRate;
|
||||||
|
public readonly uint Flags;
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
var asciiname = s.ReadASCII(16);
|
||||||
|
|
||||||
|
var pos = asciiname.IndexOf('\0');
|
||||||
|
if (pos != 0)
|
||||||
|
asciiname = asciiname.Substring(0, pos);
|
||||||
|
|
||||||
|
Name = asciiname;
|
||||||
|
Extension = DefaultExtension;
|
||||||
|
Offset = s.ReadUInt32();
|
||||||
|
Length = s.ReadUInt32();
|
||||||
|
SampleRate = s.ReadUInt32();
|
||||||
|
Flags = 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()
|
||||||
|
{
|
||||||
|
string filename;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,7 +85,10 @@
|
|||||||
<Compile Include="Activities\CallFunc.cs" />
|
<Compile Include="Activities\CallFunc.cs" />
|
||||||
<Compile Include="Actor.cs" />
|
<Compile Include="Actor.cs" />
|
||||||
<Compile Include="CacheStorage.cs" />
|
<Compile Include="CacheStorage.cs" />
|
||||||
|
<Compile Include="FileSystem\IdxEntry.cs" />
|
||||||
<Compile Include="LogProxy.cs" />
|
<Compile Include="LogProxy.cs" />
|
||||||
|
<Compile Include="FileFormats\IdxReader.cs" />
|
||||||
|
<Compile Include="FileSystem\BagFile.cs" />
|
||||||
<Compile Include="MPos.cs" />
|
<Compile Include="MPos.cs" />
|
||||||
<Compile Include="GameRules\Warhead.cs" />
|
<Compile Include="GameRules\Warhead.cs" />
|
||||||
<Compile Include="Graphics\QuadRenderer.cs" />
|
<Compile Include="Graphics\QuadRenderer.cs" />
|
||||||
@@ -139,6 +142,7 @@
|
|||||||
<Compile Include="Orders\IOrderGenerator.cs" />
|
<Compile Include="Orders\IOrderGenerator.cs" />
|
||||||
<Compile Include="Orders\UnitOrderGenerator.cs" />
|
<Compile Include="Orders\UnitOrderGenerator.cs" />
|
||||||
<Compile Include="Player.cs" />
|
<Compile Include="Player.cs" />
|
||||||
|
<Compile Include="Primitives\MergedStream.cs" />
|
||||||
<Compile Include="Selection.cs" />
|
<Compile Include="Selection.cs" />
|
||||||
<Compile Include="Server\Connection.cs" />
|
<Compile Include="Server\Connection.cs" />
|
||||||
<Compile Include="Server\Exts.cs" />
|
<Compile Include="Server\Exts.cs" />
|
||||||
|
|||||||
116
OpenRA.Game/Primitives/MergedStream.cs
Normal file
116
OpenRA.Game/Primitives/MergedStream.cs
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
#region Copyright & License Information
|
||||||
|
/*
|
||||||
|
* Copyright 2007-2015 The OpenRA Developers (see AUTHORS)
|
||||||
|
* This file is part of OpenRA, which is free software. It is made
|
||||||
|
* available to you under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation. For more information,
|
||||||
|
* see COPYING.
|
||||||
|
*/
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace OpenRA.Primitives
|
||||||
|
{
|
||||||
|
public class MergedStream : Stream
|
||||||
|
{
|
||||||
|
public Stream Stream1 { get; set; }
|
||||||
|
public Stream Stream2 { get; set; }
|
||||||
|
|
||||||
|
long VirtualLength { get; set; }
|
||||||
|
|
||||||
|
public MergedStream(Stream stream1, Stream stream2)
|
||||||
|
{
|
||||||
|
Stream1 = stream1;
|
||||||
|
Stream2 = stream2;
|
||||||
|
|
||||||
|
VirtualLength = Stream1.Length + Stream2.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Flush()
|
||||||
|
{
|
||||||
|
Stream1.Flush();
|
||||||
|
Stream2.Flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override long Seek(long offset, SeekOrigin origin)
|
||||||
|
{
|
||||||
|
var position = Position;
|
||||||
|
|
||||||
|
switch (origin)
|
||||||
|
{
|
||||||
|
case SeekOrigin.Begin:
|
||||||
|
position = offset;
|
||||||
|
break;
|
||||||
|
case SeekOrigin.Current:
|
||||||
|
position += offset;
|
||||||
|
break;
|
||||||
|
case SeekOrigin.End:
|
||||||
|
position = Length;
|
||||||
|
position += offset;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (position >= Stream1.Length)
|
||||||
|
Position = Stream1.Length + Stream2.Seek(offset - Stream1.Length, SeekOrigin.Begin);
|
||||||
|
else
|
||||||
|
Position = Stream1.Seek(offset, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void SetLength(long value)
|
||||||
|
{
|
||||||
|
VirtualLength = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int Read(byte[] buffer, int offset, int count)
|
||||||
|
{
|
||||||
|
int bytesRead;
|
||||||
|
|
||||||
|
if (Position >= Stream1.Length)
|
||||||
|
bytesRead = Stream2.Read(buffer, offset, count);
|
||||||
|
else if (count > Stream1.Length)
|
||||||
|
{
|
||||||
|
bytesRead = Stream1.Read(buffer, offset, (int)Stream1.Length);
|
||||||
|
bytesRead += Stream2.Read(buffer, (int)Stream1.Length, count - (int)Stream1.Length);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
bytesRead = Stream1.Read(buffer, offset, count);
|
||||||
|
|
||||||
|
Position += bytesRead;
|
||||||
|
|
||||||
|
return bytesRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(byte[] buffer, int offset, int count)
|
||||||
|
{
|
||||||
|
if (Position >= Stream1.Length)
|
||||||
|
Stream2.Write(buffer, offset - (int)Stream1.Length, count);
|
||||||
|
else
|
||||||
|
Stream1.Write(buffer, offset, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanRead
|
||||||
|
{
|
||||||
|
get { return Stream1.CanRead && Stream2.CanRead; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanSeek
|
||||||
|
{
|
||||||
|
get { return Stream1.CanSeek && Stream2.CanSeek; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanWrite
|
||||||
|
{
|
||||||
|
get { return Stream1.CanWrite && Stream2.CanWrite; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public override long Length
|
||||||
|
{
|
||||||
|
get { return VirtualLength; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public override long Position { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user