280 lines
11 KiB
C#

//! \file ArcWILL.cs
//! \date Fri Oct 31 13:37:11 2014
//! \brief Will ARC archive format implementation.
//
// Copyright (C) 2014-2017 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 System.Linq;
using GameRes.Formats.Strings;
using GameRes.Utility;
namespace GameRes.Formats.Will
{
internal class ExtRecord
{
public string Extension;
public int FileCount;
public uint DirOffset;
}
public class ArcOptions : ResourceOptions
{
public int NameLength { get; set; }
}
[Export(typeof(ArchiveFormat))]
public class ArcOpener : ArchiveFormat
{
public override string Tag { get { return "ARC/Will"; } }
public override string Description { get { return "Will Co. game engine resource archive"; } }
public override uint Signature { get { return 0; } }
public override bool IsHierarchic { get { return false; } }
public override bool CanWrite { get { return true; } }
ArcOpener ()
{
Extensions = new string[] { "arc" };
Signatures = new uint[] { 1, 0, 5 };
}
public override ArcFile TryOpen (ArcView file)
{
int ext_count = file.View.ReadInt32 (0);
if (ext_count <= 0 || ext_count > 0xff)
return null;
uint dir_offset = 4;
var ext_list = new List<ExtRecord> (ext_count);
for (int i = 0; i < ext_count; ++i)
{
string ext = file.View.ReadString (dir_offset, 4).ToLowerInvariant();
int count = file.View.ReadInt32 (dir_offset+4);
uint offset = file.View.ReadUInt32 (dir_offset+8);
if (count <= 0 || count > 0xffff || offset <= dir_offset || offset > file.MaxOffset)
return null;
ext_list.Add (new ExtRecord { Extension = ext, FileCount = count, DirOffset = offset });
dir_offset += 12;
}
List<Entry> dir = null;
try
{
dir = ReadFileList (file, ext_list, 9);
}
catch { /* ignore parse errors */ }
if (null == dir)
dir = ReadFileList (file, ext_list, 13);
if (null == dir)
return null;
return new ArcFile (file, this, dir);
}
List<Entry> ReadFileList (ArcView file, IEnumerable<ExtRecord> ext_list, uint name_size)
{
var dir = new List<Entry> (ext_list.Sum (ext => ext.FileCount));
foreach (var ext in ext_list)
{
uint dir_offset = ext.DirOffset;
for (int i = 0; i < ext.FileCount; ++i)
{
string name = file.View.ReadString (dir_offset, name_size);
if (string.IsNullOrEmpty (name))
return null;
name = name.ToLowerInvariant();
if (ext.Extension.Length > 0)
name = Path.ChangeExtension (name, ext.Extension);
var entry = FormatCatalog.Instance.Create<Entry> (name);
entry.Size = file.View.ReadUInt32 (dir_offset+name_size);
entry.Offset = file.View.ReadUInt32 (dir_offset+name_size+4);
if (!entry.CheckPlacement (file.MaxOffset))
return null;
dir.Add (entry);
dir_offset += name_size+8;
}
}
return dir;
}
public override Stream OpenEntry (ArcFile arc, Entry entry)
{
if (!IsScriptFile (entry.Name))
return arc.File.CreateStream (entry.Offset, entry.Size);
var data = arc.File.View.ReadBytes (entry.Offset, entry.Size);
DecodeScript (data);
return new BinMemoryStream (data, entry.Name);
}
private static void DecodeScript (byte[] data)
{
for (int i = 0; i < data.Length; ++i)
{
data[i] = Binary.RotByteR (data[i], 2);
}
}
private static void EncodeScript (byte[] data)
{
for (int i = 0; i < data.Length; ++i)
{
data[i] = Binary.RotByteL (data[i], 2);
}
}
public override ResourceOptions GetDefaultOptions ()
{
return new ArcOptions { NameLength = Properties.Settings.Default.ARCNameLength };
}
public override object GetCreationWidget ()
{
return new GUI.CreateARCWidget();
}
internal class ArcEntry : Entry
{
public byte[] RawName;
}
internal class ArcDirectory
{
public byte[] Extension;
public uint DirOffset;
public List<ArcEntry> Files;
}
public override void Create (Stream output, IEnumerable<Entry> list, ResourceOptions options,
EntryCallback callback)
{
var arc_options = GetOptions<ArcOptions> (options);
var encoding = Encodings.cp932.WithFatalFallback();
int file_count = 0;
var file_table = new SortedDictionary<string, ArcDirectory>();
foreach (var entry in list)
{
string ext = Path.GetExtension (entry.Name).TrimStart ('.').ToUpperInvariant();
if (ext.Length > 3)
throw new InvalidFileName (entry.Name, arcStrings.MsgExtensionTooLong);
string name = Path.GetFileNameWithoutExtension (entry.Name).ToUpperInvariant();
byte[] raw_name = encoding.GetBytes (name);
if (raw_name.Length > arc_options.NameLength)
throw new InvalidFileName (entry.Name, arcStrings.MsgFileNameTooLong);
ArcDirectory dir;
if (!file_table.TryGetValue (ext, out dir))
{
byte[] raw_ext = encoding.GetBytes (ext);
if (raw_ext.Length > 3)
throw new InvalidFileName (entry.Name, arcStrings.MsgExtensionTooLong);
dir = new ArcDirectory { Extension = raw_ext, Files = new List<ArcEntry>() };
file_table[ext] = dir;
}
dir.Files.Add (new ArcEntry { Name = entry.Name, RawName = raw_name });
++file_count;
}
if (null != callback)
callback (file_count+1, null, null);
int callback_count = 0;
long dir_offset = 4 + file_table.Count * 12;
long data_offset = dir_offset + (arc_options.NameLength + 9) * file_count;
output.Position = data_offset;
foreach (var ext in file_table.Keys)
{
var dir = file_table[ext];
dir.DirOffset = (uint)dir_offset;
dir_offset += (arc_options.NameLength + 9) * dir.Files.Count;
foreach (var entry in dir.Files)
{
if (null != callback)
callback (callback_count++, entry, arcStrings.MsgAddingFile);
entry.Offset = data_offset;
entry.Size = WriteEntry (entry.Name, output);
data_offset += entry.Size;
if (data_offset > uint.MaxValue)
throw new FileSizeException();
}
}
if (null != callback)
callback (callback_count++, null, arcStrings.MsgWritingIndex);
output.Position = 0;
using (var header = new BinaryWriter (output, encoding, true))
{
byte[] buffer = new byte[arc_options.NameLength+1];
header.Write (file_table.Count);
foreach (var ext in file_table)
{
Buffer.BlockCopy (ext.Value.Extension, 0, buffer, 0, ext.Value.Extension.Length);
for (int i = ext.Value.Extension.Length; i < 4; ++i)
buffer[i] = 0;
header.Write (buffer, 0, 4);
header.Write (ext.Value.Files.Count);
header.Write (ext.Value.DirOffset);
}
foreach (var ext in file_table)
{
foreach (var entry in ext.Value.Files)
{
Buffer.BlockCopy (entry.RawName, 0, buffer, 0, entry.RawName.Length);
for (int i = entry.RawName.Length; i < buffer.Length; ++i)
buffer[i] = 0;
header.Write (buffer);
header.Write (entry.Size);
header.Write ((uint)entry.Offset);
}
}
}
}
private uint WriteEntry (string filename, Stream output)
{
if (!IsScriptFile (filename))
{
using (var input = File.OpenRead (filename))
{
var size = input.Length;
if (size > uint.MaxValue)
throw new FileSizeException();
input.CopyTo (output);
return (uint)size;
}
}
else
{
var input = File.ReadAllBytes (filename);
EncodeScript (input);
output.Write (input, 0, input.Length);
return (uint)input.Length;
}
}
private static bool IsScriptFile (string filename)
{
return filename.HasAnyOfExtensions ("scr", "wsc");
}
}
}