diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj
index 1762a14c..adb204e3 100644
--- a/ArcFormats/ArcFormats.csproj
+++ b/ArcFormats/ArcFormats.csproj
@@ -68,8 +68,10 @@
+
+
diff --git a/ArcFormats/Cri/ArcSPC.cs b/ArcFormats/Cri/ArcSPC.cs
new file mode 100644
index 00000000..4a05060f
--- /dev/null
+++ b/ArcFormats/Cri/ArcSPC.cs
@@ -0,0 +1,176 @@
+//! \file ArcSPC.cs
+//! \date Wed Mar 09 09:01:11 2016
+//! \brief CRI container for multiple textures.
+//
+// Copyright (C) 2016 by morkt
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.IO;
+using GameRes.Compression;
+
+namespace GameRes.Formats.Cri
+{
+ [Export(typeof(ArchiveFormat))]
+ public class SpcOpener : ArchiveFormat
+ {
+ public override string Tag { get { return "SPC/CRI"; } }
+ public override string Description { get { return "CRI MiddleWare texture container"; } }
+ public override uint Signature { get { return 0; } }
+ public override bool IsHierarchic { get { return true; } }
+ public override bool CanCreate { get { return false; } }
+
+ public SpcOpener ()
+ {
+ Extensions = new string[] { "spc" };
+ }
+
+ public override ArcFile TryOpen (ArcView file)
+ {
+ if (!file.Name.EndsWith (".spc", StringComparison.InvariantCultureIgnoreCase))
+ return null;
+ uint unpacked_size = file.View.ReadUInt32 (0);
+ if (unpacked_size <= 0x20 || unpacked_size > 0x5000000)
+ return null;
+
+ var backend = file.CreateStream();
+ backend.Position = 4;
+ var lzss = new LzssStream (backend);
+ var input = new SeekableStream (lzss);
+ try
+ {
+ var base_name = Path.GetFileNameWithoutExtension (file.Name);
+ using (var spc = new XtxIndexBuilder (input, base_name))
+ {
+ spc.ReadIndex (0);
+ if (spc.Dir.Count > 0)
+ return new SpcArchive (file, this, spc.Dir, input);
+ else
+ throw new InvalidFormatException();
+ }
+ }
+ catch
+ {
+ input.Dispose();
+ throw;
+ }
+ }
+
+ public override Stream OpenEntry (ArcFile arc, Entry entry)
+ {
+ return new StreamRegion (((SpcArchive)arc).Source, entry.Offset, entry.Size, true);
+ }
+ }
+
+ internal sealed class XtxIndexBuilder : IDisposable
+ {
+ BinaryReader m_input;
+ string m_base_name;
+ List m_dir = new List();
+ int m_subdir_count = 0;
+
+ public List Dir { get { return m_dir; } }
+
+ public XtxIndexBuilder (Stream input, string base_name)
+ {
+ m_input = new ArcView.Reader (input);
+ m_base_name = base_name;
+ }
+
+ public void ReadIndex (uint base_offset, string dir_name = "")
+ {
+ m_input.BaseStream.Position = base_offset;
+ uint first_offset = m_input.ReadUInt32();
+ if (0 != (first_offset & 0xF))
+ throw new InvalidFormatException();
+ int count = (int)(first_offset / 0x10u);
+ m_input.BaseStream.Position = base_offset;
+ var subdir = new List (count);
+ for (int i = 0; i < count; ++i)
+ {
+ uint offset = m_input.ReadUInt32();
+ uint size = m_input.ReadUInt32();
+ if (offset < first_offset || size < 0x20)
+ throw new InvalidFormatException();
+ m_input.BaseStream.Seek (8, SeekOrigin.Current);
+ var entry = new Entry { Offset = base_offset + offset, Size = size };
+ subdir.Add (entry);
+ }
+ foreach (var entry in subdir)
+ {
+ m_input.BaseStream.Position = entry.Offset;
+ uint signature = m_input.ReadUInt32();
+ if (0x787478 == signature) // 'xtx'
+ {
+ var file_name = string.Format ("{0}#{1:D4}.xtx", m_base_name, m_dir.Count);
+ entry.Name = Path.Combine (dir_name, file_name);
+ entry.Type = "image";
+ m_dir.Add (entry);
+ }
+ else
+ {
+ var subdir_name = m_subdir_count++.ToString ("D4");
+ ReadIndex ((uint)entry.Offset, Path.Combine (dir_name, subdir_name));
+ }
+ }
+ }
+
+ #region IDisposable Members
+ bool _disposed = false;
+ public void Dispose ()
+ {
+ if (!_disposed)
+ {
+ m_input.Dispose();
+ _disposed = true;
+ }
+ }
+ #endregion
+ }
+
+ internal class SpcArchive : ArcFile
+ {
+ public readonly Stream Source;
+
+ public SpcArchive (ArcView arc, ArchiveFormat impl, ICollection dir, Stream input)
+ : base (arc, impl, dir)
+ {
+ Source = input;
+ }
+
+ #region IDisposable Members
+ bool _spc_disposed = false;
+ protected override void Dispose (bool disposing)
+ {
+ if (_spc_disposed)
+ return;
+ if (disposing)
+ {
+ Source.Dispose();
+ }
+ _spc_disposed = true;
+ base.Dispose (disposing);
+ }
+ #endregion
+ }
+}
diff --git a/ArcFormats/Cri/ImageSPC.cs b/ArcFormats/Cri/ImageSPC.cs
new file mode 100644
index 00000000..b26f8f6e
--- /dev/null
+++ b/ArcFormats/Cri/ImageSPC.cs
@@ -0,0 +1,209 @@
+//! \file ImageSPC.cs
+//! \date Tue Mar 08 19:05:11 2016
+//! \brief CRI MiddleWare compressed multi-frame image.
+//
+// Copyright (C) 2016 by morkt
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//
+
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using GameRes.Compression;
+
+namespace GameRes.Formats.Cri
+{
+ [Export(typeof(ImageFormat))]
+ public class SpcFormat : XtxFormat
+ {
+ public override string Tag { get { return "SPC"; } }
+ public override string Description { get { return "CRI MiddleWare compressed texture format"; } }
+ public override uint Signature { get { return 0; } }
+
+ public SpcFormat ()
+ {
+ Signatures = new uint[] { 0 };
+ }
+
+ public override ImageMetaData ReadMetaData (Stream stream)
+ {
+ uint unpacked_size = FormatCatalog.ReadSignature (stream);
+ if (unpacked_size <= 0x20 || unpacked_size > 0x5000000) // ~83MB
+ return null;
+ using (var lzss = new LzssStream (stream, LzssMode.Decompress, true))
+ using (var input = new SeekableStream (lzss))
+ return base.ReadMetaData (input);
+ }
+
+ public override ImageData Read (Stream stream, ImageMetaData info)
+ {
+ stream.Position = 4;
+ using (var lzss = new LzssStream (stream, LzssMode.Decompress, true))
+ using (var input = new SeekableStream (lzss))
+ return base.Read (input, info);
+ }
+
+ public override void Write (Stream file, ImageData image)
+ {
+ throw new System.NotImplementedException ("SpcFormat.Write not implemented");
+ }
+ }
+
+ public class SeekableStream : Stream
+ {
+ Stream m_source;
+ Stream m_buffer;
+ bool m_should_dispose;
+ bool m_source_depleted;
+ long m_read_pos;
+
+ public SeekableStream (Stream input, bool leave_open = false)
+ {
+ m_source = input;
+ m_should_dispose = !leave_open;
+ m_read_pos = 0;
+ if (m_source.CanSeek)
+ {
+ m_buffer = m_source;
+ m_source_depleted = true;
+ }
+ else
+ {
+ m_buffer = new MemoryStream();
+ m_source_depleted = false;
+ }
+ }
+
+ #region IO.Stream Members
+ public override bool CanRead { get { return m_buffer.CanRead; } }
+ public override bool CanSeek { get { return true; } }
+ public override bool CanWrite { get { return false; } }
+ public override long Length
+ {
+ get
+ {
+ if (!m_source_depleted)
+ {
+ m_buffer.Seek (0, SeekOrigin.End);
+ m_source.CopyTo (m_buffer);
+ m_source_depleted = true;
+ }
+ return m_buffer.Length;
+ }
+ }
+ public override long Position
+ {
+ get { return m_read_pos; }
+ set { m_read_pos = value; }
+ }
+
+ public override int Read (byte[] buffer, int offset, int count)
+ {
+ int total_read = 0;
+ if (m_source_depleted)
+ {
+ m_buffer.Position = m_read_pos;
+ total_read = m_buffer.Read (buffer, offset, count);
+ m_read_pos += total_read;
+ return total_read;
+ }
+ if (m_read_pos < m_buffer.Length)
+ {
+ int available = (int)Math.Min (m_buffer.Length-m_read_pos, count);
+ m_buffer.Position = m_read_pos;
+ total_read = m_buffer.Read (buffer, offset, available);
+ m_read_pos += total_read;
+ count -= total_read;
+ if (0 == count)
+ return total_read;
+ offset += total_read;
+ }
+ else
+ {
+ m_buffer.Seek (0, SeekOrigin.End);
+ while (m_read_pos > m_buffer.Length)
+ {
+ int b = m_source.ReadByte();
+ if (-1 == b)
+ {
+ m_source_depleted = true;
+ return 0;
+ }
+ m_buffer.WriteByte ((byte)b);
+ }
+ }
+ int read = m_source.Read (buffer, offset, count);
+ m_read_pos += read;
+ m_buffer.Write (buffer, offset, read);
+ return total_read + read;
+ }
+
+ public override void Flush()
+ {
+ }
+
+ public override long Seek (long offset, SeekOrigin origin)
+ {
+ if (SeekOrigin.Begin == origin)
+ m_read_pos = offset;
+ else if (SeekOrigin.Current == origin)
+ m_read_pos += offset;
+ else
+ m_read_pos = Length + offset;
+
+ return m_read_pos;
+ }
+
+ public override void SetLength (long length)
+ {
+ throw new NotSupportedException ("SeekableStream.SetLength method is not supported");
+ }
+
+ public override void Write (byte[] buffer, int offset, int count)
+ {
+ throw new NotSupportedException ("SeekableStream.Write method is not supported");
+ }
+
+ public override void WriteByte (byte value)
+ {
+ throw new NotSupportedException ("SeekableStream.WriteByte method is not supported");
+ }
+ #endregion
+
+ #region IDisposable Members
+ bool _disposed = false;
+ protected override void Dispose (bool disposing)
+ {
+ if (_disposed)
+ return;
+
+ if (disposing)
+ {
+ if (m_should_dispose)
+ m_source.Dispose();
+ if (m_buffer != m_source)
+ m_buffer.Dispose();
+ }
+ _disposed = true;
+ base.Dispose (disposing);
+ }
+ #endregion
+ }
+}
diff --git a/ArcFormats/Cri/ImageXTX.cs b/ArcFormats/Cri/ImageXTX.cs
index 07fb230e..a40848b7 100644
--- a/ArcFormats/Cri/ImageXTX.cs
+++ b/ArcFormats/Cri/ImageXTX.cs
@@ -59,7 +59,7 @@ namespace GameRes.Formats.Cri
if (!Binary.AsciiEqual (header, 0, "xtx\0"))
{
var header_size = LittleEndian.ToUInt32 (header, 0);
- if (header_size >= stream.Length)
+ if (header_size >= 0x1000) // XXX use some arbitrary "large" value to avoid call to Stream.Length
return null;
stream.Position = header_size;
if (0x20 != stream.Read (header, 0, 0x20))