diff --git a/.gitignore b/.gitignore
index 58bcbf8..267c6ea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,43 +1,188 @@
-# Windows image file caches
-Thumbs.db
-ehthumbs.db
-
-# Folder config file
-Desktop.ini
-
-# Recycle Bin used on file shares
-$RECYCLE.BIN/
-
-# Windows Installer files
-*.cab
-*.msi
-*.msm
-*.msp
-
-# =========================
-# Operating System Files
-# =========================
-
-# OSX
-# =========================
-
-.DS_Store
-.AppleDouble
-.LSOverride
-
-# Icon must end with two \r
-Icon
-
-# Thumbnails
-._*
-
-# Files that might appear on external disk
-.Spotlight-V100
-.Trashes
-
-# Directories potentially created on remote AFP share
-.AppleDB
-.AppleDesktop
-Network Trash Folder
-Temporary Items
-.apdisk
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific folders
+*.sln.ide/
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+build/
+bld/
+[Bb]in/
+[Oo]bj/
+
+# Roslyn cache directories
+*.ide/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+#NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opensdf
+*.sdf
+*.cachefile
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding addin-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# TODO: Comment the next line if you want to checkin your web deploy settings
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/packages/*
+# except build/, which is used as an MSBuild target.
+!**/packages/build/
+# If using the old MSBuild-Integrated Package Restore, uncomment this:
+#!**/packages/repositories.config
+
+# Windows Azure Build Output
+csx/
+*.build.csdef
+
+# Windows Store app package directory
+AppPackages/
+
+# Others
+sql/
+*.Cache
+ClientBin/
+[Ss]tyle[Cc]op.*
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.pfx
+*.publishsettings
+node_modules/
+bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
diff --git a/App.config b/App.config
new file mode 100644
index 0000000..8e15646
--- /dev/null
+++ b/App.config
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ICSharpCode.SharpZipLib.dll b/ICSharpCode.SharpZipLib.dll
new file mode 100644
index 0000000..fe643eb
Binary files /dev/null and b/ICSharpCode.SharpZipLib.dll differ
diff --git a/Options.cs b/Options.cs
new file mode 100644
index 0000000..eca7582
--- /dev/null
+++ b/Options.cs
@@ -0,0 +1,1175 @@
+//
+// Options.cs
+//
+// Authors:
+// Jonathan Pryor
+//
+// Copyright (C) 2008 Novell (http://www.novell.com)
+//
+// 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.
+//
+
+// Compile With:
+// gmcs -debug+ -r:System.Core Options.cs -o:NDesk.Options.dll
+// gmcs -debug+ -d:LINQ -r:System.Core Options.cs -o:NDesk.Options.dll
+//
+// The LINQ version just changes the implementation of
+// OptionSet.Parse(IEnumerable), and confers no semantic changes.
+
+//
+// A Getopt::Long-inspired option parsing library for C#.
+//
+// NDesk.Options.OptionSet is built upon a key/value table, where the
+// key is a option format string and the value is a delegate that is
+// invoked when the format string is matched.
+//
+// Option format strings:
+// Regex-like BNF Grammar:
+// name: .+
+// type: [=:]
+// sep: ( [^{}]+ | '{' .+ '}' )?
+// aliases: ( name type sep ) ( '|' name type sep )*
+//
+// Each '|'-delimited name is an alias for the associated action. If the
+// format string ends in a '=', it has a required value. If the format
+// string ends in a ':', it has an optional value. If neither '=' or ':'
+// is present, no value is supported. `=' or `:' need only be defined on one
+// alias, but if they are provided on more than one they must be consistent.
+//
+// Each alias portion may also end with a "key/value separator", which is used
+// to split option values if the option accepts > 1 value. If not specified,
+// it defaults to '=' and ':'. If specified, it can be any character except
+// '{' and '}' OR the *string* between '{' and '}'. If no separator should be
+// used (i.e. the separate values should be distinct arguments), then "{}"
+// should be used as the separator.
+//
+// Options are extracted either from the current option by looking for
+// the option name followed by an '=' or ':', or is taken from the
+// following option IFF:
+// - The current option does not contain a '=' or a ':'
+// - The current option requires a value (i.e. not a Option type of ':')
+//
+// The `name' used in the option format string does NOT include any leading
+// option indicator, such as '-', '--', or '/'. All three of these are
+// permitted/required on any named option.
+//
+// Option bundling is permitted so long as:
+// - '-' is used to start the option group
+// - all of the bundled options are a single character
+// - at most one of the bundled options accepts a value, and the value
+// provided starts from the next character to the end of the string.
+//
+// This allows specifying '-a -b -c' as '-abc', and specifying '-D name=value'
+// as '-Dname=value'.
+//
+// Option processing is disabled by specifying "--". All options after "--"
+// are returned by OptionSet.Parse() unchanged and unprocessed.
+//
+// Unprocessed options are returned from OptionSet.Parse().
+//
+// Examples:
+// int verbose = 0;
+// OptionSet p = new OptionSet ()
+// .Add ("v", v => ++verbose)
+// .Add ("name=|value=", v => Console.WriteLine (v));
+// p.Parse (new string[]{"-v", "--v", "/v", "-name=A", "/name", "B", "extra"});
+//
+// The above would parse the argument string array, and would invoke the
+// lambda expression three times, setting `verbose' to 3 when complete.
+// It would also print out "A" and "B" to standard output.
+// The returned array would contain the string "extra".
+//
+// C# 3.0 collection initializers are supported and encouraged:
+// var p = new OptionSet () {
+// { "h|?|help", v => ShowHelp () },
+// };
+//
+// System.ComponentModel.TypeConverter is also supported, allowing the use of
+// custom data types in the callback type; TypeConverter.ConvertFromString()
+// is used to convert the value option to an instance of the specified
+// type:
+//
+// var p = new OptionSet () {
+// { "foo=", (Foo f) => Console.WriteLine (f.ToString ()) },
+// };
+//
+// Random other tidbits:
+// - Boolean options (those w/o '=' or ':' in the option format string)
+// are explicitly enabled if they are followed with '+', and explicitly
+// disabled if they are followed with '-':
+// string a = null;
+// var p = new OptionSet () {
+// { "a", s => a = s },
+// };
+// p.Parse (new string[]{"-a"}); // sets v != null
+// p.Parse (new string[]{"-a+"}); // sets v != null
+// p.Parse (new string[]{"-a-"}); // sets v == null
+//
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Globalization;
+using System.IO;
+using System.Runtime.Serialization;
+using System.Security.Permissions;
+using System.Text;
+using System.Text.RegularExpressions;
+
+#if LINQ
+using System.Linq;
+#endif
+
+#if TEST
+using NDesk.Options;
+#endif
+
+namespace NDesk.Options
+{
+
+ public class OptionValueCollection : IList, IList
+ {
+
+ List values = new List();
+ OptionContext c;
+
+ internal OptionValueCollection(OptionContext c)
+ {
+ this.c = c;
+ }
+
+ #region ICollection
+ void ICollection.CopyTo(Array array, int index) { (values as ICollection).CopyTo(array, index); }
+ bool ICollection.IsSynchronized { get { return (values as ICollection).IsSynchronized; } }
+ object ICollection.SyncRoot { get { return (values as ICollection).SyncRoot; } }
+ #endregion
+
+ #region ICollection
+ public void Add(string item) { values.Add(item); }
+ public void Clear() { values.Clear(); }
+ public bool Contains(string item) { return values.Contains(item); }
+ public void CopyTo(string[] array, int arrayIndex) { values.CopyTo(array, arrayIndex); }
+ public bool Remove(string item) { return values.Remove(item); }
+ public int Count { get { return values.Count; } }
+ public bool IsReadOnly { get { return false; } }
+ #endregion
+
+ #region IEnumerable
+ IEnumerator IEnumerable.GetEnumerator() { return values.GetEnumerator(); }
+ #endregion
+
+ #region IEnumerable
+ public IEnumerator GetEnumerator() { return values.GetEnumerator(); }
+ #endregion
+
+ #region IList
+ int IList.Add(object value) { return (values as IList).Add(value); }
+ bool IList.Contains(object value) { return (values as IList).Contains(value); }
+ int IList.IndexOf(object value) { return (values as IList).IndexOf(value); }
+ void IList.Insert(int index, object value) { (values as IList).Insert(index, value); }
+ void IList.Remove(object value) { (values as IList).Remove(value); }
+ void IList.RemoveAt(int index) { (values as IList).RemoveAt(index); }
+ bool IList.IsFixedSize { get { return false; } }
+ object IList.this[int index] { get { return this[index]; } set { (values as IList)[index] = value; } }
+ #endregion
+
+ #region IList
+ public int IndexOf(string item) { return values.IndexOf(item); }
+ public void Insert(int index, string item) { values.Insert(index, item); }
+ public void RemoveAt(int index) { values.RemoveAt(index); }
+
+ private void AssertValid(int index)
+ {
+ if (c.Option == null)
+ throw new InvalidOperationException("OptionContext.Option is null.");
+ if (index >= c.Option.MaxValueCount)
+ throw new ArgumentOutOfRangeException("index");
+ if (c.Option.OptionValueType == OptionValueType.Required &&
+ index >= values.Count)
+ throw new OptionException(string.Format(
+ c.OptionSet.MessageLocalizer("Missing required value for option '{0}'."), c.OptionName),
+ c.OptionName);
+ }
+
+ public string this[int index]
+ {
+ get
+ {
+ AssertValid(index);
+ return index >= values.Count ? null : values[index];
+ }
+ set
+ {
+ values[index] = value;
+ }
+ }
+ #endregion
+
+ public List ToList()
+ {
+ return new List(values);
+ }
+
+ public string[] ToArray()
+ {
+ return values.ToArray();
+ }
+
+ public override string ToString()
+ {
+ return string.Join(", ", values.ToArray());
+ }
+ }
+
+ public class OptionContext
+ {
+ private Option option;
+ private string name;
+ private int index;
+ private OptionSet set;
+ private OptionValueCollection c;
+
+ public OptionContext(OptionSet set)
+ {
+ this.set = set;
+ this.c = new OptionValueCollection(this);
+ }
+
+ public Option Option
+ {
+ get { return option; }
+ set { option = value; }
+ }
+
+ public string OptionName
+ {
+ get { return name; }
+ set { name = value; }
+ }
+
+ public int OptionIndex
+ {
+ get { return index; }
+ set { index = value; }
+ }
+
+ public OptionSet OptionSet
+ {
+ get { return set; }
+ }
+
+ public OptionValueCollection OptionValues
+ {
+ get { return c; }
+ }
+ }
+
+ public enum OptionValueType
+ {
+ None,
+ Optional,
+ Required,
+ }
+
+ public abstract class Option
+ {
+ string prototype, description;
+ string[] names;
+ OptionValueType type;
+ int count;
+ string[] separators;
+
+ protected Option(string prototype, string description)
+ : this(prototype, description, 1)
+ {
+ }
+
+ protected Option(string prototype, string description, int maxValueCount)
+ {
+ if (prototype == null)
+ throw new ArgumentNullException("prototype");
+ if (prototype.Length == 0)
+ throw new ArgumentException("Cannot be the empty string.", "prototype");
+ if (maxValueCount < 0)
+ throw new ArgumentOutOfRangeException("maxValueCount");
+
+ this.prototype = prototype;
+ this.names = prototype.Split('|');
+ this.description = description;
+ this.count = maxValueCount;
+ this.type = ParsePrototype();
+
+ if (this.count == 0 && type != OptionValueType.None)
+ throw new ArgumentException(
+ "Cannot provide maxValueCount of 0 for OptionValueType.Required or " +
+ "OptionValueType.Optional.",
+ "maxValueCount");
+ if (this.type == OptionValueType.None && maxValueCount > 1)
+ throw new ArgumentException(
+ string.Format("Cannot provide maxValueCount of {0} for OptionValueType.None.", maxValueCount),
+ "maxValueCount");
+ if (Array.IndexOf(names, "<>") >= 0 &&
+ ((names.Length == 1 && this.type != OptionValueType.None) ||
+ (names.Length > 1 && this.MaxValueCount > 1)))
+ throw new ArgumentException(
+ "The default option handler '<>' cannot require values.",
+ "prototype");
+ }
+
+ public string Prototype { get { return prototype; } }
+ public string Description { get { return description; } }
+ public OptionValueType OptionValueType { get { return type; } }
+ public int MaxValueCount { get { return count; } }
+
+ public string[] GetNames()
+ {
+ return (string[])names.Clone();
+ }
+
+ public string[] GetValueSeparators()
+ {
+ if (separators == null)
+ return new string[0];
+ return (string[])separators.Clone();
+ }
+
+ protected static T Parse(string value, OptionContext c)
+ {
+ TypeConverter conv = TypeDescriptor.GetConverter(typeof(T));
+ T t = default(T);
+ try
+ {
+ if (value != null)
+ t = (T)conv.ConvertFromString(value);
+ }
+ catch (Exception e)
+ {
+ throw new OptionException(
+ string.Format(
+ c.OptionSet.MessageLocalizer("Could not convert string `{0}' to type {1} for option `{2}'."),
+ value, typeof(T).Name, c.OptionName),
+ c.OptionName, e);
+ }
+ return t;
+ }
+
+ internal string[] Names { get { return names; } }
+ internal string[] ValueSeparators { get { return separators; } }
+
+ static readonly char[] NameTerminator = new char[] { '=', ':' };
+
+ private OptionValueType ParsePrototype()
+ {
+ char type = '\0';
+ List seps = new List();
+ for (int i = 0; i < names.Length; ++i)
+ {
+ string name = names[i];
+ if (name.Length == 0)
+ throw new ArgumentException("Empty option names are not supported.", "prototype");
+
+ int end = name.IndexOfAny(NameTerminator);
+ if (end == -1)
+ continue;
+ names[i] = name.Substring(0, end);
+ if (type == '\0' || type == name[end])
+ type = name[end];
+ else
+ throw new ArgumentException(
+ string.Format("Conflicting option types: '{0}' vs. '{1}'.", type, name[end]),
+ "prototype");
+ AddSeparators(name, end, seps);
+ }
+
+ if (type == '\0')
+ return OptionValueType.None;
+
+ if (count <= 1 && seps.Count != 0)
+ throw new ArgumentException(
+ string.Format("Cannot provide key/value separators for Options taking {0} value(s).", count),
+ "prototype");
+ if (count > 1)
+ {
+ if (seps.Count == 0)
+ this.separators = new string[] { ":", "=" };
+ else if (seps.Count == 1 && seps[0].Length == 0)
+ this.separators = null;
+ else
+ this.separators = seps.ToArray();
+ }
+
+ return type == '=' ? OptionValueType.Required : OptionValueType.Optional;
+ }
+
+ private static void AddSeparators(string name, int end, ICollection seps)
+ {
+ int start = -1;
+ for (int i = end + 1; i < name.Length; ++i)
+ {
+ switch (name[i])
+ {
+ case '{':
+ if (start != -1)
+ throw new ArgumentException(
+ string.Format("Ill-formed name/value separator found in \"{0}\".", name),
+ "prototype");
+ start = i + 1;
+ break;
+ case '}':
+ if (start == -1)
+ throw new ArgumentException(
+ string.Format("Ill-formed name/value separator found in \"{0}\".", name),
+ "prototype");
+ seps.Add(name.Substring(start, i - start));
+ start = -1;
+ break;
+ default:
+ if (start == -1)
+ seps.Add(name[i].ToString());
+ break;
+ }
+ }
+ if (start != -1)
+ throw new ArgumentException(
+ string.Format("Ill-formed name/value separator found in \"{0}\".", name),
+ "prototype");
+ }
+
+ public void Invoke(OptionContext c)
+ {
+ OnParseComplete(c);
+ c.OptionName = null;
+ c.Option = null;
+ c.OptionValues.Clear();
+ }
+
+ protected abstract void OnParseComplete(OptionContext c);
+
+ public override string ToString()
+ {
+ return Prototype;
+ }
+ }
+
+ [Serializable]
+ public class OptionException : Exception
+ {
+ private string option;
+
+ public OptionException()
+ {
+ }
+
+ public OptionException(string message, string optionName)
+ : base(message)
+ {
+ this.option = optionName;
+ }
+
+ public OptionException(string message, string optionName, Exception innerException)
+ : base(message, innerException)
+ {
+ this.option = optionName;
+ }
+
+ protected OptionException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ {
+ this.option = info.GetString("OptionName");
+ }
+
+ public string OptionName
+ {
+ get { return this.option; }
+ }
+
+ [SecurityPermission(SecurityAction.LinkDemand, SerializationFormatter = true)]
+ public override void GetObjectData(SerializationInfo info, StreamingContext context)
+ {
+ base.GetObjectData(info, context);
+ info.AddValue("OptionName", option);
+ }
+ }
+
+ public delegate void OptionAction(TKey key, TValue value);
+
+ public class OptionSet : KeyedCollection
+ {
+ public OptionSet()
+ : this(delegate(string f) { return f; })
+ {
+ }
+
+ public OptionSet(Converter localizer)
+ {
+ this.localizer = localizer;
+ }
+
+ Converter localizer;
+
+ public Converter MessageLocalizer
+ {
+ get { return localizer; }
+ }
+
+ protected override string GetKeyForItem(Option item)
+ {
+ if (item == null)
+ throw new ArgumentNullException("option");
+ if (item.Names != null && item.Names.Length > 0)
+ return item.Names[0];
+ // This should never happen, as it's invalid for Option to be
+ // constructed w/o any names.
+ throw new InvalidOperationException("Option has no names!");
+ }
+
+ [Obsolete("Use KeyedCollection.this[string]")]
+ protected Option GetOptionForName(string option)
+ {
+ if (option == null)
+ throw new ArgumentNullException("option");
+ try
+ {
+ return base[option];
+ }
+ catch (KeyNotFoundException)
+ {
+ return null;
+ }
+ }
+
+ protected override void InsertItem(int index, Option item)
+ {
+ base.InsertItem(index, item);
+ AddImpl(item);
+ }
+
+ protected override void RemoveItem(int index)
+ {
+ base.RemoveItem(index);
+ Option p = Items[index];
+ // KeyedCollection.RemoveItem() handles the 0th item
+ for (int i = 1; i < p.Names.Length; ++i)
+ {
+ Dictionary.Remove(p.Names[i]);
+ }
+ }
+
+ protected override void SetItem(int index, Option item)
+ {
+ base.SetItem(index, item);
+ RemoveItem(index);
+ AddImpl(item);
+ }
+
+ private void AddImpl(Option option)
+ {
+ if (option == null)
+ throw new ArgumentNullException("option");
+ List added = new List(option.Names.Length);
+ try
+ {
+ // KeyedCollection.InsertItem/SetItem handle the 0th name.
+ for (int i = 1; i < option.Names.Length; ++i)
+ {
+ Dictionary.Add(option.Names[i], option);
+ added.Add(option.Names[i]);
+ }
+ }
+ catch (Exception)
+ {
+ foreach (string name in added)
+ Dictionary.Remove(name);
+ throw;
+ }
+ }
+
+ public new OptionSet Add(Option option)
+ {
+ base.Add(option);
+ return this;
+ }
+
+ sealed class ActionOption : Option
+ {
+ Action action;
+
+ public ActionOption(string prototype, string description, int count, Action action)
+ : base(prototype, description, count)
+ {
+ if (action == null)
+ throw new ArgumentNullException("action");
+ this.action = action;
+ }
+
+ protected override void OnParseComplete(OptionContext c)
+ {
+ action(c.OptionValues);
+ }
+ }
+
+ public OptionSet Add(string prototype, Action action)
+ {
+ return Add(prototype, null, action);
+ }
+
+ public OptionSet Add(string prototype, string description, Action action)
+ {
+ if (action == null)
+ throw new ArgumentNullException("action");
+ Option p = new ActionOption(prototype, description, 1,
+ delegate(OptionValueCollection v) { action(v[0]); });
+ base.Add(p);
+ return this;
+ }
+
+ public OptionSet Add(string prototype, OptionAction action)
+ {
+ return Add(prototype, null, action);
+ }
+
+ public OptionSet Add(string prototype, string description, OptionAction action)
+ {
+ if (action == null)
+ throw new ArgumentNullException("action");
+ Option p = new ActionOption(prototype, description, 2,
+ delegate(OptionValueCollection v) { action(v[0], v[1]); });
+ base.Add(p);
+ return this;
+ }
+
+ sealed class ActionOption : Option
+ {
+ Action action;
+
+ public ActionOption(string prototype, string description, Action action)
+ : base(prototype, description, 1)
+ {
+ if (action == null)
+ throw new ArgumentNullException("action");
+ this.action = action;
+ }
+
+ protected override void OnParseComplete(OptionContext c)
+ {
+ action(Parse(c.OptionValues[0], c));
+ }
+ }
+
+ sealed class ActionOption : Option
+ {
+ OptionAction action;
+
+ public ActionOption(string prototype, string description, OptionAction action)
+ : base(prototype, description, 2)
+ {
+ if (action == null)
+ throw new ArgumentNullException("action");
+ this.action = action;
+ }
+
+ protected override void OnParseComplete(OptionContext c)
+ {
+ action(
+ Parse(c.OptionValues[0], c),
+ Parse(c.OptionValues[1], c));
+ }
+ }
+
+ public OptionSet Add(string prototype, Action action)
+ {
+ return Add(prototype, null, action);
+ }
+
+ public OptionSet Add(string prototype, string description, Action action)
+ {
+ return Add(new ActionOption(prototype, description, action));
+ }
+
+ public OptionSet Add(string prototype, OptionAction action)
+ {
+ return Add(prototype, null, action);
+ }
+
+ public OptionSet Add(string prototype, string description, OptionAction action)
+ {
+ return Add(new ActionOption(prototype, description, action));
+ }
+
+ protected virtual OptionContext CreateOptionContext()
+ {
+ return new OptionContext(this);
+ }
+
+#if LINQ
+ public List Parse (IEnumerable arguments)
+ {
+ bool process = true;
+ OptionContext c = CreateOptionContext ();
+ c.OptionIndex = -1;
+ var def = GetOptionForName ("<>");
+ var unprocessed =
+ from argument in arguments
+ where ++c.OptionIndex >= 0 && (process || def != null)
+ ? process
+ ? argument == "--"
+ ? (process = false)
+ : !Parse (argument, c)
+ ? def != null
+ ? Unprocessed (null, def, c, argument)
+ : true
+ : false
+ : def != null
+ ? Unprocessed (null, def, c, argument)
+ : true
+ : true
+ select argument;
+ List r = unprocessed.ToList ();
+ if (c.Option != null)
+ c.Option.Invoke (c);
+ return r;
+ }
+#else
+ public List Parse(IEnumerable arguments)
+ {
+ OptionContext c = CreateOptionContext();
+ c.OptionIndex = -1;
+ bool process = true;
+ List unprocessed = new List();
+ Option def = Contains("<>") ? this["<>"] : null;
+ foreach (string argument in arguments)
+ {
+ ++c.OptionIndex;
+ if (argument == "--")
+ {
+ process = false;
+ continue;
+ }
+ if (!process)
+ {
+ Unprocessed(unprocessed, def, c, argument);
+ continue;
+ }
+ if (!Parse(argument, c))
+ Unprocessed(unprocessed, def, c, argument);
+ }
+ if (c.Option != null)
+ c.Option.Invoke(c);
+ return unprocessed;
+ }
+#endif
+
+ private static bool Unprocessed(ICollection extra, Option def, OptionContext c, string argument)
+ {
+ if (def == null)
+ {
+ extra.Add(argument);
+ return false;
+ }
+ c.OptionValues.Add(argument);
+ c.Option = def;
+ c.Option.Invoke(c);
+ return false;
+ }
+
+ private readonly Regex ValueOption = new Regex(
+ @"^(?--|-|/)(?[^:=]+)((?[:=])(?.*))?$");
+
+ protected bool GetOptionParts(string argument, out string flag, out string name, out string sep, out string value)
+ {
+ if (argument == null)
+ throw new ArgumentNullException("argument");
+
+ flag = name = sep = value = null;
+ Match m = ValueOption.Match(argument);
+ if (!m.Success)
+ {
+ return false;
+ }
+ flag = m.Groups["flag"].Value;
+ name = m.Groups["name"].Value;
+ if (m.Groups["sep"].Success && m.Groups["value"].Success)
+ {
+ sep = m.Groups["sep"].Value;
+ value = m.Groups["value"].Value;
+ }
+ return true;
+ }
+
+ protected virtual bool Parse(string argument, OptionContext c)
+ {
+ if (c.Option != null)
+ {
+ ParseValue(argument, c);
+ return true;
+ }
+
+ string f, n, s, v;
+ if (!GetOptionParts(argument, out f, out n, out s, out v))
+ return false;
+
+ Option p;
+ if (Contains(n))
+ {
+ p = this[n];
+ c.OptionName = f + n;
+ c.Option = p;
+ switch (p.OptionValueType)
+ {
+ case OptionValueType.None:
+ c.OptionValues.Add(n);
+ c.Option.Invoke(c);
+ break;
+ case OptionValueType.Optional:
+ case OptionValueType.Required:
+ ParseValue(v, c);
+ break;
+ }
+ return true;
+ }
+ // no match; is it a bool option?
+ if (ParseBool(argument, n, c))
+ return true;
+ // is it a bundled option?
+ if (ParseBundledValue(f, string.Concat(n + s + v), c))
+ return true;
+
+ return false;
+ }
+
+ private void ParseValue(string option, OptionContext c)
+ {
+ if (option != null)
+ foreach (string o in c.Option.ValueSeparators != null
+ ? option.Split(c.Option.ValueSeparators, StringSplitOptions.None)
+ : new string[] { option })
+ {
+ c.OptionValues.Add(o);
+ }
+ if (c.OptionValues.Count == c.Option.MaxValueCount ||
+ c.Option.OptionValueType == OptionValueType.Optional)
+ c.Option.Invoke(c);
+ else if (c.OptionValues.Count > c.Option.MaxValueCount)
+ {
+ throw new OptionException(localizer(string.Format(
+ "Error: Found {0} option values when expecting {1}.",
+ c.OptionValues.Count, c.Option.MaxValueCount)),
+ c.OptionName);
+ }
+ }
+
+ private bool ParseBool(string option, string n, OptionContext c)
+ {
+ Option p;
+ string rn;
+ if (n.Length >= 1 && (n[n.Length - 1] == '+' || n[n.Length - 1] == '-') &&
+ Contains((rn = n.Substring(0, n.Length - 1))))
+ {
+ p = this[rn];
+ string v = n[n.Length - 1] == '+' ? option : null;
+ c.OptionName = option;
+ c.Option = p;
+ c.OptionValues.Add(v);
+ p.Invoke(c);
+ return true;
+ }
+ return false;
+ }
+
+ private bool ParseBundledValue(string f, string n, OptionContext c)
+ {
+ if (f != "-")
+ return false;
+ for (int i = 0; i < n.Length; ++i)
+ {
+ Option p;
+ string opt = f + n[i].ToString();
+ string rn = n[i].ToString();
+ if (!Contains(rn))
+ {
+ if (i == 0)
+ return false;
+ throw new OptionException(string.Format(localizer(
+ "Cannot bundle unregistered option '{0}'."), opt), opt);
+ }
+ p = this[rn];
+ switch (p.OptionValueType)
+ {
+ case OptionValueType.None:
+ Invoke(c, opt, n, p);
+ break;
+ case OptionValueType.Optional:
+ case OptionValueType.Required:
+ {
+ string v = n.Substring(i + 1);
+ c.Option = p;
+ c.OptionName = opt;
+ ParseValue(v.Length != 0 ? v : null, c);
+ return true;
+ }
+ default:
+ throw new InvalidOperationException("Unknown OptionValueType: " + p.OptionValueType);
+ }
+ }
+ return true;
+ }
+
+ private static void Invoke(OptionContext c, string name, string value, Option option)
+ {
+ c.OptionName = name;
+ c.Option = option;
+ c.OptionValues.Add(value);
+ option.Invoke(c);
+ }
+
+ private const int OptionWidth = 29;
+
+ public void WriteOptionDescriptions(TextWriter o)
+ {
+ foreach (Option p in this)
+ {
+ int written = 0;
+ if (!WriteOptionPrototype(o, p, ref written))
+ continue;
+
+ if (written < OptionWidth)
+ o.Write(new string(' ', OptionWidth - written));
+ else
+ {
+ o.WriteLine();
+ o.Write(new string(' ', OptionWidth));
+ }
+
+ List lines = GetLines(localizer(GetDescription(p.Description)));
+ o.WriteLine(lines[0]);
+ string prefix = new string(' ', OptionWidth + 2);
+ for (int i = 1; i < lines.Count; ++i)
+ {
+ o.Write(prefix);
+ o.WriteLine(lines[i]);
+ }
+ }
+ }
+
+ bool WriteOptionPrototype(TextWriter o, Option p, ref int written)
+ {
+ string[] names = p.Names;
+
+ int i = GetNextOptionIndex(names, 0);
+ if (i == names.Length)
+ return false;
+
+ if (names[i].Length == 1)
+ {
+ Write(o, ref written, " -");
+ Write(o, ref written, names[0]);
+ }
+ else
+ {
+ Write(o, ref written, " --");
+ Write(o, ref written, names[0]);
+ }
+
+ for (i = GetNextOptionIndex(names, i + 1);
+ i < names.Length; i = GetNextOptionIndex(names, i + 1))
+ {
+ Write(o, ref written, ", ");
+ Write(o, ref written, names[i].Length == 1 ? "-" : "--");
+ Write(o, ref written, names[i]);
+ }
+
+ if (p.OptionValueType == OptionValueType.Optional ||
+ p.OptionValueType == OptionValueType.Required)
+ {
+ if (p.OptionValueType == OptionValueType.Optional)
+ {
+ Write(o, ref written, localizer("["));
+ }
+ Write(o, ref written, localizer("=" + GetArgumentName(0, p.MaxValueCount, p.Description)));
+ string sep = p.ValueSeparators != null && p.ValueSeparators.Length > 0
+ ? p.ValueSeparators[0]
+ : " ";
+ for (int c = 1; c < p.MaxValueCount; ++c)
+ {
+ Write(o, ref written, localizer(sep + GetArgumentName(c, p.MaxValueCount, p.Description)));
+ }
+ if (p.OptionValueType == OptionValueType.Optional)
+ {
+ Write(o, ref written, localizer("]"));
+ }
+ }
+ return true;
+ }
+
+ static int GetNextOptionIndex(string[] names, int i)
+ {
+ while (i < names.Length && names[i] == "<>")
+ {
+ ++i;
+ }
+ return i;
+ }
+
+ static void Write(TextWriter o, ref int n, string s)
+ {
+ n += s.Length;
+ o.Write(s);
+ }
+
+ private static string GetArgumentName(int index, int maxIndex, string description)
+ {
+ if (description == null)
+ return maxIndex == 1 ? "VALUE" : "VALUE" + (index + 1);
+ string[] nameStart;
+ if (maxIndex == 1)
+ nameStart = new string[] { "{0:", "{" };
+ else
+ nameStart = new string[] { "{" + index + ":" };
+ for (int i = 0; i < nameStart.Length; ++i)
+ {
+ int start, j = 0;
+ do
+ {
+ start = description.IndexOf(nameStart[i], j);
+ } while (start >= 0 && j != 0 ? description[j++ - 1] == '{' : false);
+ if (start == -1)
+ continue;
+ int end = description.IndexOf("}", start);
+ if (end == -1)
+ continue;
+ return description.Substring(start + nameStart[i].Length, end - start - nameStart[i].Length);
+ }
+ return maxIndex == 1 ? "VALUE" : "VALUE" + (index + 1);
+ }
+
+ private static string GetDescription(string description)
+ {
+ if (description == null)
+ return string.Empty;
+ StringBuilder sb = new StringBuilder(description.Length);
+ int start = -1;
+ for (int i = 0; i < description.Length; ++i)
+ {
+ switch (description[i])
+ {
+ case '{':
+ if (i == start)
+ {
+ sb.Append('{');
+ start = -1;
+ }
+ else if (start < 0)
+ start = i + 1;
+ break;
+ case '}':
+ if (start < 0)
+ {
+ if ((i + 1) == description.Length || description[i + 1] != '}')
+ throw new InvalidOperationException("Invalid option description: " + description);
+ ++i;
+ sb.Append("}");
+ }
+ else
+ {
+ sb.Append(description.Substring(start, i - start));
+ start = -1;
+ }
+ break;
+ case ':':
+ if (start < 0)
+ goto default;
+ start = i + 1;
+ break;
+ default:
+ if (start < 0)
+ sb.Append(description[i]);
+ break;
+ }
+ }
+ return sb.ToString();
+ }
+
+ private static List GetLines(string description)
+ {
+ List lines = new List();
+ if (string.IsNullOrEmpty(description))
+ {
+ lines.Add(string.Empty);
+ return lines;
+ }
+ int length = 80 - OptionWidth - 2;
+ int start = 0, end;
+ do
+ {
+ end = GetLineEnd(start, length, description);
+ bool cont = false;
+ if (end < description.Length)
+ {
+ char c = description[end];
+ if (c == '-' || (char.IsWhiteSpace(c) && c != '\n'))
+ ++end;
+ else if (c != '\n')
+ {
+ cont = true;
+ --end;
+ }
+ }
+ lines.Add(description.Substring(start, end - start));
+ if (cont)
+ {
+ lines[lines.Count - 1] += "-";
+ }
+ start = end;
+ if (start < description.Length && description[start] == '\n')
+ ++start;
+ } while (end < description.Length);
+ return lines;
+ }
+
+ private static int GetLineEnd(int start, int length, string description)
+ {
+ int end = Math.Min(start + length, description.Length);
+ int sep = -1;
+ for (int i = start; i < end; ++i)
+ {
+ switch (description[i])
+ {
+ case ' ':
+ case '\t':
+ case '\v':
+ case '-':
+ case ',':
+ case '.':
+ case ';':
+ sep = i;
+ break;
+ case '\n':
+ return i;
+ }
+ }
+ if (sep == -1 || end == description.Length)
+ return end;
+ return sep;
+ }
+ }
+}
+
diff --git a/Program.cs b/Program.cs
new file mode 100644
index 0000000..ff3ad40
--- /dev/null
+++ b/Program.cs
@@ -0,0 +1,450 @@
+using System;
+using System.IO;
+using System.Text;
+using System.Linq;
+using System.Collections.Generic;
+using System.IO.Compression;
+using System.Threading;
+using System.Diagnostics;
+using ICSharpCode.SharpZipLib.BZip2;
+using NDesk.Options;
+
+namespace RSCacheTool
+{
+ static class Program
+ {
+ static string cacheDir = Environment.ExpandEnvironmentVariables(@"%USERPROFILE%\jagexcache\runescape\LIVE\");
+ static string outDir = "cache\\";
+
+ static int Main(string[] args)
+ {
+ bool error = false;
+
+ bool help = false, extract = false, combine = false, combineOverwrite = false, combineMergeIncomplete = false;
+ int extractArchive = -1, combineArchive = 40, combineStartFile = 0;
+
+ OptionSet argsParser = new OptionSet() {
+ { "h", "show this message", val => { help = true; } },
+
+ { "e", "extract files from cache", val => { extract = true; } },
+ { "a=", "single archive to extract, if not given all archives will be extracted", val => { extractArchive = Convert.ToInt32(val); } },
+
+ { "c", "combine sound", val => { combine = true; } },
+ { "s=", "archive to combine sounds of, defaults to 40", val => { combineArchive = Convert.ToInt32(val); } },
+ { "o", "overwrite existing soundfiles", val => { combineOverwrite = true; } },
+ { "i", "merge incomplete files (into special directory)", val => { combineMergeIncomplete = true; } },
+ };
+
+ List otherArgs = argsParser.Parse(args);
+
+ for (int i = 0; i < otherArgs.Count; i++)
+ {
+ string parsedPath = otherArgs[i];
+ if (!parsedPath.EndsWith("\\"))
+ parsedPath += "\\";
+
+ parsedPath = Environment.ExpandEnvironmentVariables(parsedPath);
+
+ if (Directory.Exists(parsedPath))
+ {
+ if (i == 0)
+ outDir = parsedPath;
+ else if (i == 1)
+ cacheDir = parsedPath;
+ }
+ else
+ {
+ Console.WriteLine("The directory: " + parsedPath + " is not valid.");
+ error = true;
+ }
+ }
+
+ if (args.Length == 0 || help)
+ {
+ Console.WriteLine(
+ "Usage: rscachetools [options] outDir cacheDir\n" +
+ "Provides various tools for extracting and manipulating RuneScape's cache files.\n" +
+ "\n" +
+ "Arguments:\n" +
+ "outDir - The directory in which all files generated by this tool will be placed. Default: cache\\\n" +
+ "cacheDir - The directory that contains all cache files. Default: %USERPROFILE%\\jagexcache\\runescape\\LIVE\\.\n" +
+ "\n" +
+ "Options:"
+ );
+
+ argsParser.WriteOptionDescriptions(Console.Out);
+ }
+ else if (!error)
+ {
+ if (extract)
+ ExtractFiles(extractArchive);
+
+ if (combine)
+ CombineSounds(combineArchive, combineStartFile, combineOverwrite, combineMergeIncomplete);
+ }
+
+ return 0;
+ }
+
+ ///
+ /// Rips all files from the cachefile and puts them (structured and given a fitting extension where possible) in the fileDir.
+ ///
+ static void ExtractFiles(int archive)
+ {
+ int startArchive = 0, endArchive = 255;
+
+ if (archive != -1)
+ {
+ startArchive = archive;
+ endArchive = archive;
+ }
+
+ using (FileStream cacheFile = File.Open(cacheDir + "main_file_cache.dat2", FileMode.Open, FileAccess.Read))
+ {
+ for (int archiveIndex = startArchive; archiveIndex <= endArchive; archiveIndex++)
+ {
+ string indexFileString = cacheDir + "main_file_cache.idx" + archiveIndex.ToString();
+
+ if (File.Exists(indexFileString))
+ {
+ FileStream indexFile = File.Open(indexFileString, FileMode.Open, FileAccess.Read);
+
+ int fileCount = (int)indexFile.Length / 6;
+
+ for (int fileIndex = 0; fileIndex < fileCount; fileIndex++)
+ {
+ bool fileError = false;
+
+ indexFile.Seek(fileIndex * 6, SeekOrigin.Begin);
+
+ uint fileSize = indexFile.ReadBytes(3);
+ long startChunkOffset = indexFile.ReadBytes(3) * 520L;
+
+ //Console.WriteLine("New file: archive: {0} file: {1} offset: {3} size: {2}", archiveIndex, fileIndex, fileSize, startChunkOffset);
+
+ //filter teeny tiny files (<=1kB) because they gobble up time
+ if (fileSize > 1024 && startChunkOffset > 0 && startChunkOffset + fileSize <= cacheFile.Length)
+ {
+ byte[] buffer = new byte[fileSize];
+ int writeOffset = 0;
+ long currentChunkOffset = startChunkOffset;
+
+ for (int chunkIndex = 0; writeOffset < fileSize && currentChunkOffset > 0; chunkIndex++)
+ {
+ cacheFile.Seek(currentChunkOffset, SeekOrigin.Begin);
+
+ int chunkSize;
+ int checksumFileIndex = 0;
+
+ if (fileIndex < 65536)
+ {
+ chunkSize = (int)Math.Min(512, fileSize - writeOffset);
+ }
+ else
+ {
+ //if file index exceeds 2 bytes, add 65536 and read 2(?) extra bytes
+ chunkSize = (int)Math.Min(510, fileSize - writeOffset);
+
+ cacheFile.ReadByte();
+ checksumFileIndex = (cacheFile.ReadByte() << 16);
+ }
+
+ checksumFileIndex += (int)cacheFile.ReadBytes(2);
+ int checksumChunkIndex = (int)cacheFile.ReadBytes(2);
+ long nextChunkOffset = cacheFile.ReadBytes(3) * 520L;
+ int checksumArchiveIndex = cacheFile.ReadByte();
+
+ //Console.WriteLine("Chunk {2}: archive: {3} file: {1} size: {0} nextoffset: {4}", chunkSize, checksumFileIndex, checksumChunkIndex, checksumArchiveIndex, nextChunkOffset);
+
+ if (checksumFileIndex == fileIndex && checksumChunkIndex == chunkIndex && checksumArchiveIndex == archiveIndex &&
+ nextChunkOffset >= 0 && nextChunkOffset < cacheFile.Length)
+ {
+ cacheFile.Read(buffer, writeOffset, chunkSize);
+ writeOffset += chunkSize;
+ currentChunkOffset = nextChunkOffset;
+ }
+ else
+ {
+ Console.WriteLine("Ignoring file because a chunk's checksum doesn't match, ideally should not happen.");
+
+ fileError = true;
+ break;
+ }
+ }
+
+ if (!fileError)
+ {
+ //process file
+ string outFileDir = outDir + archiveIndex + "\\";
+ string outFileName = fileIndex.ToString();
+ byte[] tempBuffer;
+
+ //decompress gzip
+ if ((buffer[9] << 8) + buffer[10] == 0x1f8b) //gzip
+ {
+ //remove the first 9 bytes cause they seem to be descriptors of sorts (no idea what they do but they are not part of the file)
+ tempBuffer = new byte[fileSize - 9];
+ Array.Copy(buffer, 9, tempBuffer, 0, fileSize - 9);
+ buffer = tempBuffer;
+
+ GZipStream decompressionStream = new GZipStream(new MemoryStream(buffer), CompressionMode.Decompress);
+
+ int readBytes;
+ tempBuffer = new byte[0];
+
+ do
+ {
+ byte[] readBuffer = new byte[100000];
+ readBytes = decompressionStream.Read(readBuffer, 0, 100000);
+
+ int storedBytes = tempBuffer.Length;
+ Array.Resize(ref tempBuffer, tempBuffer.Length + readBytes);
+ Array.Copy(readBuffer, 0, tempBuffer, storedBytes, readBytes);
+ }
+ while (readBytes == 100000);
+
+ buffer = tempBuffer;
+
+ Console.WriteLine("File decompressed as gzip.");
+ }
+
+ //decompress bzip2
+ if (buffer[9] == 0x31 && buffer[10] == 0x41 && buffer[11] == 0x59 && buffer[12] == 0x26 && buffer[13] == 0x53 && buffer[14] == 0x59) //bzip2
+ {
+ //remove the first 9 bytes cause they seem to be descriptors of sorts (no idea what they do but they are not part of the file)
+ tempBuffer = new byte[fileSize - 9];
+ Array.Copy(buffer, 9, tempBuffer, 0, fileSize - 9);
+ buffer = tempBuffer;
+
+ //prepend file header
+ byte[] magic = new byte[] {
+ 0x42, 0x5a, //BZ (signature)
+ 0x68, //h (version)
+ 0x31 //*100kB block-size
+ };
+
+ tempBuffer = new byte[magic.Length + buffer.Length];
+ magic.CopyTo(tempBuffer, 0);
+ buffer.CopyTo(tempBuffer, magic.Length);
+ buffer = tempBuffer;
+
+ BZip2InputStream decompressionStream = new BZip2InputStream(new MemoryStream(buffer));
+
+ int readBytes;
+ tempBuffer = new byte[0];
+
+ do
+ {
+ byte[] readBuffer = new byte[100000];
+ readBytes = decompressionStream.Read(readBuffer, 0, 100000);
+
+ int storedBytes = tempBuffer.Length;
+ Array.Resize(ref tempBuffer, tempBuffer.Length + readBytes);
+ Array.Copy(readBuffer, 0, tempBuffer, storedBytes, readBytes);
+ }
+ while (readBytes == 100000);
+
+ buffer = tempBuffer;
+
+ Console.WriteLine("File decompressed as bzip2.");
+ }
+
+ //detect ogg: OggS
+ if ((buffer[0] << 24) + (buffer[1] << 16) + (buffer[2] << 8) + buffer[3] == 0x4f676753)
+ outFileName += ".ogg";
+
+ //detect ogg: 5 bytes - OggS
+ if ((buffer[5] << 24) + (buffer[6] << 16) + (buffer[7] << 8) + buffer[8] == 0x4f676753)
+ {
+ tempBuffer = new byte[fileSize - 5];
+ Array.Copy(buffer, 5, tempBuffer, 0, fileSize - 5);
+ buffer = tempBuffer;
+
+ outFileName += ".ogg";
+ }
+
+ //detect jag: JAGA
+ if ((buffer[0] << 24) + (buffer[1] << 16) + (buffer[2] << 8) + buffer[3] == 0x4a414741)
+ outFileName += ".jaga";
+
+ //detect jag: 5 bytes - JAGA
+ if ((buffer[5] << 24) + (buffer[6] << 16) + (buffer[7] << 8) + buffer[8] == 0x4a414741)
+ {
+ tempBuffer = new byte[fileSize - 5];
+ Array.Copy(buffer, 5, tempBuffer, 0, fileSize - 5);
+ buffer = tempBuffer;
+
+ outFileName += ".jaga";
+ }
+
+ //detect png: .PNG
+ if ((uint)(buffer[0] << 24) + (buffer[1] << 16) + (buffer[2] << 8) + buffer[3] == 0x89504e47)
+ outFileName += ".png";
+
+ //detect png: 5 bytes - .PNG
+ if ((uint)(buffer[5] << 24) + (buffer[6] << 16) + (buffer[7] << 8) + buffer[8] == 0x89504e47)
+ {
+ tempBuffer = new byte[fileSize - 5];
+ Array.Copy(buffer, 5, tempBuffer, 0, fileSize - 5);
+ buffer = tempBuffer;
+
+ outFileName += ".png";
+ }
+
+ //create and write file
+ if (!Directory.Exists(outFileDir))
+ Directory.CreateDirectory(outFileDir);
+
+ using (FileStream outFile = File.Open(outFileDir + outFileName, FileMode.Create, FileAccess.Write))
+ {
+ outFile.Write(buffer, 0, buffer.Length);
+ Console.WriteLine(outFileDir + outFileName);
+ }
+ }
+ }
+ else
+ {
+ Console.WriteLine("Ignoring file because of size or offset.");
+ }
+ }
+ }
+ }
+ }
+
+ Console.WriteLine("Done extracting files.");
+ }
+
+ ///
+ /// Combines the sound files (.jag & .ogg) in the specified archive (40 for the build it was made on), and puts them into the soundtracks directory.
+ ///
+ static void CombineSounds(int archive = 40, int startFile = 0, bool overwriteExisting = false, bool mergeIncomplete = false)
+ {
+ string archiveDir = outDir + archive + "\\";
+ string soundDir = outDir + "sound\\";
+
+ //gather all index files
+ string[] indexFiles = Directory.GetFiles(archiveDir, "*.jag", SearchOption.TopDirectoryOnly);
+
+ //create directories
+ if (!Directory.Exists(soundDir + "incomplete\\"))
+ Directory.CreateDirectory(soundDir + "incomplete\\");
+
+ int i = 0;
+ foreach (string indexFileString in indexFiles)
+ {
+ if (i < startFile)
+ {
+ i++;
+ continue;
+ }
+
+ bool incomplete = false;
+ List chunkFiles = new List();
+
+ using (FileStream indexFileStream = File.Open(indexFileString, FileMode.Open, FileAccess.Read, FileShare.Read))
+ {
+ indexFileStream.Seek(32, SeekOrigin.Begin);
+
+ while (indexFileStream.ReadBytes(4) != 0x4f676753)
+ {
+ uint fileId = indexFileStream.ReadBytes(4);
+
+ //check if the file exists and add it to the buffer if it does
+ if (File.Exists(archiveDir + fileId + ".ogg"))
+ chunkFiles.Add(archiveDir + fileId + ".ogg");
+ else
+ incomplete = true;
+ }
+
+ //copy the first chunk to a temp file so SoX can handle the combining
+ indexFileStream.Seek(-4, SeekOrigin.Current);
+
+ //wait till file is available
+ while (true)
+ {
+ try
+ {
+ using (FileStream tempIndexFile = File.Open("~index.ogg", FileMode.OpenOrCreate, FileAccess.Write, FileShare.None))
+ {
+ indexFileStream.CopyTo(tempIndexFile);
+ break;
+ }
+ }
+ catch (IOException)
+ {
+ Thread.Sleep(200);
+ }
+ }
+ }
+
+ if (!incomplete || incomplete && mergeIncomplete)
+ {
+ string outFile = soundDir + (incomplete ? "incomplete\\" : "") + i + ".ogg";
+
+ if (!overwriteExisting && File.Exists(outFile))
+ Console.WriteLine("Skipping track because it already exists.");
+ else
+ {
+ //combine the files with sox
+ Console.WriteLine("Running SoX to concatenate ogg audio chunks.");
+
+ Process soxProcess = new Process();
+ soxProcess.StartInfo.FileName = "sox.exe";
+
+ soxProcess.StartInfo.Arguments = "--combine concatenate ~index.ogg";
+ chunkFiles.ForEach((str) =>
+ {
+ soxProcess.StartInfo.Arguments += " " + str;
+ });
+ soxProcess.StartInfo.Arguments += " " + soundDir + "incomplete\\" + i + ".ogg ";
+ soxProcess.StartInfo.UseShellExecute = false;
+
+ soxProcess.Start();
+ soxProcess.WaitForExit();
+
+ if (soxProcess.ExitCode == 0)
+ {
+ if (!incomplete)
+ {
+ //clear space
+ if (File.Exists(soundDir + i + ".ogg"))
+ File.Delete(soundDir + i + ".ogg");
+
+ File.Move(soundDir + "incomplete\\" + i + ".ogg", soundDir + i + ".ogg");
+ }
+
+ Console.WriteLine(soundDir + (incomplete ? "incomplete\\" : "") + i + ".ogg");
+ }
+ else
+ Console.WriteLine("SoX encountered error code " + soxProcess.ExitCode + " and probably didn't finish processing the files.");
+ }
+ }
+ else
+ Console.WriteLine("Skipping track because it's incomplete.");
+
+ i++;
+ }
+
+ //cleanup on isle 4
+ File.Delete("~index.ogg");
+
+ Console.WriteLine("Done combining sound.");
+ }
+
+ ///
+ /// Reads a given amount of bytes from the stream.
+ ///
+ public static uint ReadBytes(this Stream stream, byte bytes)
+ {
+ if (bytes == 0 || bytes > 4)
+ throw new ArgumentOutOfRangeException();
+
+ uint result = 0;
+
+ for (int i = 0; i < bytes; i++)
+ result += (uint)stream.ReadByte() << (bytes - i - 1) * 8;
+
+ return result;
+ }
+ }
+}
diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..0d0b88c
--- /dev/null
+++ b/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("RSCacheTool")]
+[assembly: AssemblyDescription("Various tools for extracting and manipulating RuneScape's cache files.")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("Villermen")]
+[assembly: AssemblyProduct("RSCacheTool")]
+[assembly: AssemblyCopyright("Copyright © 2014")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("bbc70dd4-1015-4395-8340-4838b6a23815")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/RSCacheTool.csproj b/RSCacheTool.csproj
new file mode 100644
index 0000000..01785a2
--- /dev/null
+++ b/RSCacheTool.csproj
@@ -0,0 +1,61 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {722D3FF9-C3E9-40D9-B68A-3FA1C304A34C}
+ Exe
+ Properties
+ RSCacheTool
+ RSCacheTool
+ v4.5
+ 512
+
+
+ AnyCPU
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+ .\ICSharpCode.SharpZipLib.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
+
\ No newline at end of file
diff --git a/sox.exe b/sox.exe
new file mode 100644
index 0000000..d35e652
Binary files /dev/null and b/sox.exe differ