From 5e15fb5091886714fdf42b33a1f238e278447b5b Mon Sep 17 00:00:00 2001 From: morkt Date: Sat, 16 Aug 2014 10:38:26 +0400 Subject: [PATCH] implemented Ren'Py game engine archives support. --- ArcFormats/ArcFormats.csproj | 1 + ArcFormats/ArcRPA.cs | 546 ++++++++++++++++++++++ ArcFormats/Strings/arcStrings.Designer.cs | 9 + ArcFormats/Strings/arcStrings.resx | 3 + 4 files changed, 559 insertions(+) create mode 100644 ArcFormats/ArcRPA.cs diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj index fa2b1948..1ab9825a 100644 --- a/ArcFormats/ArcFormats.csproj +++ b/ArcFormats/ArcFormats.csproj @@ -62,6 +62,7 @@ + diff --git a/ArcFormats/ArcRPA.cs b/ArcFormats/ArcRPA.cs new file mode 100644 index 00000000..be385df8 --- /dev/null +++ b/ArcFormats/ArcRPA.cs @@ -0,0 +1,546 @@ +//! \file ArcRPA.cs +//! \date Sat Aug 16 05:26:13 2014 +//! \brief Ren'Py game engine archive implementation. +// +// Copyright (C) 2014 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; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Text; +using ZLibNet; + +namespace GameRes.Formats.RenPy +{ + internal class RpaEntry : Entry + { + public byte[] Header = null; + } + + [Export(typeof(ArchiveFormat))] + public class RpaOpener : ArchiveFormat + { + public override string Tag { get { return "RPA"; } } + public override string Description { get { return Strings.arcStrings.RPADescription; } } + public override uint Signature { get { return 0x2d415052; } } // "RPA-" + public override bool IsHierarchic { get { return true; } } + + public override ArcFile TryOpen (ArcView file) + { + if (0x20302e33 != file.View.ReadUInt32 (4)) + return null; + string index_offset_str = file.View.ReadString (8, 16, Encoding.ASCII); + long index_offset; + if (!long.TryParse (index_offset_str, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out index_offset)) + return null; + if (index_offset >= file.MaxOffset) + return null; + uint key; + string key_str = file.View.ReadString (0x19, 8, Encoding.ASCII); + if (!uint.TryParse (key_str, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out key)) + return null; + + Hashtable dict = null; + using (var index = new ZLibStream (file.CreateStream (index_offset), CompressionMode.Decompress)) + { + var pickle = new Pickle (index); + dict = pickle.Load() as Hashtable; + } + if (null == dict) + return null; + var dir = new List (dict.Count); + foreach (DictionaryEntry item in dict) + { + var name_raw = item.Key as byte[]; + var value = item.Value as ArrayList; + if (null == name_raw || null == value || value.Count < 1) + { + Trace.WriteLine ("invalid index entry", "RpaOpener.TryOpen"); + return null; + } + string name = Encoding.UTF8.GetString (name_raw); + if (string.IsNullOrEmpty (name)) + return null; + var tuple = value[0] as ArrayList; + if (null == tuple || tuple.Count < 2) + { + Trace.WriteLine ("invalid index tuple", "RpaOpener.TryOpen"); + return null; + } + var entry = new RpaEntry + { + Name = name, + Type = FormatCatalog.Instance.GetTypeFromName (name), + Offset = (uint)((int)tuple[0] ^ key), + Size = (uint)((int)tuple[1] ^ key), + }; + if (tuple.Count > 2) + entry.Header = tuple[2] as byte[]; + + dir.Add (entry); + } + if (dir.Count > 0) + Trace.TraceInformation ("[{0}] [{1:X8}] [{2}]", dir[0].Name, dir[0].Offset, dir[0].Size); + return new ArcFile (file, this, dir); + } + + public override Stream OpenEntry (ArcFile arc, Entry entry) + { + var input = arc.File.CreateStream (entry.Offset, entry.Size); + var rpa_entry = entry as RpaEntry; + if (null == rpa_entry || null == rpa_entry.Header) + return input; + return new RpaStream (rpa_entry.Header, input); + } + } + + public class Pickle + { + Stream m_stream; + + ArrayList m_stack = new ArrayList(); + Stack m_marks = new Stack(); + + const int HIGHEST_PROTOCOL = 2; + const int PROTO = 0x80; /* identify pickle protocol */ + const int TUPLE2 = 0x86; /* build 2-tuple from two topmost stack items */ + const int TUPLE3 = 0x87; /* build 3-tuple from three topmost stack items */ + const int MARK = '('; + const int STOP = '.'; + const int BININT = 'J'; + const int BININT1 = 'K'; + const int BININT2 = 'M'; + const int SHORT_BINSTRING = 'U'; + const int EMPTY_LIST = ']'; + const int APPEND = 'a'; + const int BINPUT = 'q'; + const int LONG_BINPUT = 'r'; + const int SETITEMS = 'u'; + const int EMPTY_DICT = '}'; + + public Pickle (Stream stream) + { + m_stream = stream; + } + + public object Load () + { + for (;;) + { + int sym = m_stream.ReadByte(); + switch (sym) + { + case PROTO: + if (!LoadProto()) + break; + continue; + + case EMPTY_DICT: + if (!LoadEmptyDict()) + break; + continue; + + case BINPUT: + if (!LoadBinPut()) + break; + continue; + + case LONG_BINPUT: + if (!LoadLongBinPut()) + break; + continue; + + case MARK: + if (!LoadMark()) + break; + continue; + + case SHORT_BINSTRING: + if (!LoadShortBinstring()) + break; + continue; + + case EMPTY_LIST: + if (!LoadEmptyList()) + break; + continue; + + case BININT: + if (!LoadBinInt (4)) + break; + continue; + + case BININT1: + if (!LoadBinInt (1)) + break; + continue; + + case BININT2: + if (!LoadBinInt (2)) + break; + continue; + + case TUPLE2: + if (!LoadCountedTuple (2)) + break; + continue; + + case TUPLE3: + if (!LoadCountedTuple (3)) + break; + continue; + + case APPEND: + if (!LoadAppend()) + break; + continue; + + case SETITEMS: + if (!LoadSetItems()) + break; + continue; + + case STOP: + break; + + case -1: // EOF + case 0: + Trace.WriteLine ("Unexpected end of file", "Pickle.Load"); + return null; + + default: + Trace.TraceError ("Unknown Pickle serialization key {0:X2}", sym); + return null; + } + break; + } + if (0 == m_stack.Count) + { + Trace.WriteLine ("Invalid pickle data", "Pickle.Load"); + return null; + } + return m_stack.Pop(); + } + + bool LoadProto () + { + int i = m_stream.ReadByte(); + if (-1 == i) + return false; + if (i > HIGHEST_PROTOCOL) + return false; + return true; + } + + bool LoadEmptyDict () + { + m_stack.Push (new Hashtable()); + return true; + } + + bool LoadBinPut () + { + int key = m_stream.ReadByte(); + if (-1 == key || 0 == m_stack.Count) + return false; +// m_memo[key] = m_stack.Peek(); + return true; + } + + bool LoadLongBinPut () + { + int key; + if (!ReadInt (4, out key) || 0 == m_stack.Count || key < 0) + return false; +// m_memo[key] = m_stack.Peek(); + return true; + } + + bool LoadMark () + { + m_marks.Push (m_stack.Count); + return true; + } + + int GetMarker () + { + if (0 == m_marks.Count) + { + Trace.TraceError ("MARK list is empty"); + return -1; + } + return m_marks.Pop(); + } + + bool LoadShortBinstring () + { + int length = m_stream.ReadByte(); + if (-1 == length) + return false; + var bytes = new byte[length]; + if (length != m_stream.Read (bytes, 0, length)) + return false; + m_stack.Push (bytes); + return true; + } + + bool LoadEmptyList () + { + m_stack.Push (new ArrayList()); + return true; + } + + bool ReadInt (int size, out int value) + { + value = 0; + for (int i = 0; i < size; ++i) + { + int b = m_stream.ReadByte(); + if (-1 == b) + return false; + value |= b << (i * 8); + } + return true; + } + + bool LoadBinInt (int size) + { + int x = 0; + if (!ReadInt (size, out x)) + return false; + m_stack.Push (x); + return true; + } + + bool LoadCountedTuple (int count) + { + if (m_stack.Count < count) + return false; + var tuple = new ArrayList (count); + while (--count >= 0) + { + var item = m_stack.Pop(); + tuple.Add (item); + } + tuple.Reverse(); + m_stack.Push (tuple); + return true; + } + + bool LoadAppend () + { + int x = m_stack.Count - 1; + if (m_stack.Count < x || 0 == x) + { + Trace.WriteLine ("Stack underflow", "LoadAppend"); + return false; + } + var list = m_stack[x-1] as ArrayList; + if (null == list) + { + Trace.WriteLine ("Object is not a list", "LoadAppend"); + return false; + } + var slice = PdataPopList (x); + if (null == slice) + return false; + list.AddRange (slice); + return true; + } + + ArrayList PdataPopList (int start) + { + int count = m_stack.Count - start; + var list = new ArrayList (count); + for (int i = start; i < m_stack.Count; ++i) + list.Add (m_stack[i]); + m_stack.RemoveRange (start, count); + return list; + } + + bool LoadSetItems () + { + int mark = GetMarker(); + if (!(m_stack.Count >= mark && mark > 0)) + { + Trace.WriteLine ("Stack underflow", "LoadSetItems"); + return false; + } + var dict = m_stack[mark-1] as Hashtable; + if (null == dict) + { + Trace.WriteLine ("Marked object is not a dictionary", "LoadSetItems"); + return false; + } + for (int i = mark+1; i < m_stack.Count; i += 2) + { + var key = m_stack[i-1]; + var value = m_stack[i]; + dict[key] = value; + } + return PdataClear (mark); + } + + bool PdataClear (int clearto) + { + if (clearto < 0) + return false; + if (clearto >= m_stack.Count) + return true; + m_stack.RemoveRange (clearto, m_stack.Count-clearto); + return true; + } + } + + static public class ArrayListEx + { + static public object Peek (this ArrayList array) + { + return array[array.Count-1]; + } + + static public void Push (this ArrayList array, object item) + { + array.Add (item); + } + + static public object Pop (this ArrayList array) + { + var item = array[array.Count-1]; + array.RemoveAt (array.Count-1); + return item; + } + } + + public class RpaStream : Stream + { + byte[] m_header; + Stream m_stream; + long m_position = 0; + + public RpaStream (byte[] header, Stream main) + { + m_header = header; + m_stream = main; + } + + public override bool CanRead { get { return m_stream.CanRead; } } + public override bool CanSeek { get { return m_stream.CanSeek; } } + public override bool CanWrite { get { return false; } } + public override long Length { get { return m_stream.Length + m_header.Length; } } + public override long Position + { + get { return m_position; } + set + { + m_position = Math.Max (value, 0); + if (m_position > m_header.Length) + { + long stream_pos = m_stream.Seek (m_position - m_header.Length, SeekOrigin.Begin); + m_position = m_header.Length + stream_pos; + } + } + } + + public override void Flush() + { + m_stream.Flush(); + } + + public override long Seek (long offset, SeekOrigin origin) + { + if (SeekOrigin.Begin == origin) + Position = offset; + else if (SeekOrigin.Current == origin) + Position = m_position + offset; + else + Position = Length + offset; + + return m_position; + } + + public override int Read (byte[] buffer, int offset, int count) + { + int read = 0; + if (m_position < m_header.Length) + { + int header_count = Math.Min (count, m_header.Length - (int)m_position); + Array.Copy (m_header, (int)m_position, buffer, offset, header_count); + m_position += header_count; + read += header_count; + offset += header_count; + count -= header_count; + if (count > 0) + m_stream.Position = 0; + } + if (count > 0) + { + int stream_read = m_stream.Read (buffer, offset, count); + m_position += stream_read; + read += stream_read; + } + return read; + } + + public override int ReadByte () + { + if (m_position < m_header.Length) + return m_header[m_position++]; + if (m_position == m_header.Length) + m_stream.Position = 0; + int b = m_stream.ReadByte(); + if (-1 != b) + m_position++; + return b; + } + + public override void SetLength (long length) + { + throw new NotSupportedException ("RpaStream.SetLength method is not supported"); + } + + public override void Write (byte[] buffer, int offset, int count) + { + throw new NotSupportedException ("RpaStream.Write method is not supported"); + } + + public override void WriteByte (byte value) + { + throw new NotSupportedException ("RpaStream.WriteByte method is not supported"); + } + + bool disposed = false; + protected override void Dispose (bool disposing) + { + if (!disposed) + { + m_stream.Dispose(); + disposed = true; + base.Dispose (disposing); + } + } + } +} diff --git a/ArcFormats/Strings/arcStrings.Designer.cs b/ArcFormats/Strings/arcStrings.Designer.cs index 8124072f..cdd7cbcf 100644 --- a/ArcFormats/Strings/arcStrings.Designer.cs +++ b/ArcFormats/Strings/arcStrings.Designer.cs @@ -387,6 +387,15 @@ namespace GameRes.Formats.Strings { } } + /// + /// Looks up a localized string similar to Ren'Py game engine archive. + /// + public static string RPADescription { + get { + return ResourceManager.GetString("RPADescription", resourceCulture); + } + } + /// /// Looks up a localized string similar to Amaterasu Translations Muv-Luv script file. /// diff --git a/ArcFormats/Strings/arcStrings.resx b/ArcFormats/Strings/arcStrings.resx index 86c8d770..ea8b3257 100644 --- a/ArcFormats/Strings/arcStrings.resx +++ b/ArcFormats/Strings/arcStrings.resx @@ -228,6 +228,9 @@ predefined encryption scheme. Scramble contents + + Ren'Py game engine archive + Amaterasu Translations Muv-Luv script file