diff --git a/CMakeLists.txt b/CMakeLists.txt
deleted file mode 100644
index c40f1bb..0000000
--- a/CMakeLists.txt
+++ /dev/null
@@ -1,158 +0,0 @@
-#########################################
-#### CMake generator file for Niotso ####
-
-cmake_minimum_required(VERSION 2.6)
-
-enable_language(ASM)
-set(CMAKE_C_COMPILER "gcc")
-set(CMAKE_CXX_COMPILER "gcc")
-set(CMAKE_ASM_COMPILER "gcc")
-
-project(Niotso)
-
-# Installation directory
-if(WIN32)
- set(CMAKE_INSTALL_PREFIX "c:/Program Files (x86)/Maxis/The Sims Online/Niotso" CACHE FILEPATH "Installation directory")
-else()
- set(CMAKE_INSTALL_PREFIX "/usr/bin" CACHE FILEPATH "Installation directory")
-endif()
-
-# Build type
-if(CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUCXX)
- set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Build configurations: Release Debug Release-MakeProfile Release-UseProfile")
-else()
- set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Build configurations: Release Debug")
-endif()
-
-if(WIN32)
- set(64BIT 0)
-else()
- set(64BIT 1)
-endif()
-
-
-if(CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUCXX)
- # Base options
- set(CFLAGS "-Wall -Wextra -Wabi -pedantic -mmmx -msse -msse2 -msse3 -fvisibility=hidden")
- set(LDFLAGS "-static-libgcc")
- set(RCFLAGS "")
-
- set(CFLAGS_LANG_C "-ansi")
- set(CFLAGS_LANG_CPP "-fvisibility-inlines-hidden -fno-exceptions -fno-rtti -fno-threadsafe-statics -D__STDC_LIMIT_MACROS")
-
- if(64BIT)
- set(CFLAGS "-m64 ${CFLAGS}")
- set(LDFLAGS "-m64 ${LDFLAGS}")
- set(RCFLAGS "${RCFLAGS} -F pe-x86-64")
- else()
- set(CFLAGS "-m32 ${CFLAGS}")
- set(LDFLAGS "-m32 ${LDFLAGS}")
- set(RCFLAGS "${RCFLAGS} -F pe-i386")
- endif()
-
- ####
- ## [Profiles]
-
- if(NOT (CMAKE_BUILD_TYPE MATCHES "Debug"))
- if(CMAKE_BUILD_TYPE MATCHES "Release-MakeProfile")
- set(CFLAGS "${CFLAGS} -fprofile-generate")
- set(LDFLAGS "${LDFLAGS} -lgcov")
- elseif(CMAKE_BUILD_TYPE MATCHES "Release-UseProfile")
- set(CFLAGS "${CFLAGS} -fprofile-use")
- endif()
-
- # Size
- set(CFLAGS_SIZE "${CFLAGS} -Os -g0 -fomit-frame-pointer -mfpmath=both -msahf -malign-double -mpc32 -ffast-math -fmerge-all-constants -funsafe-loop-optimizations -fsched-pressure -mstringop-strategy=rep_byte -fno-stack-protector")
- set(LDFLAGS_SIZE "${LDFLAGS} -s -fwhole-program -flto -fno-stack-protector")
-
- # Speed
- set(CFLAGS_SPEED "${CFLAGS} -O3 -g0 -fomit-frame-pointer -mfpmath=both -msahf -malign-double -mpc32 -ffast-math -fmerge-all-constants -funsafe-loop-optimizations -fsched-pressure -fno-stack-protector -fmodulo-sched -fmodulo-sched-allow-regmoves -fgcse-sm -fgcse-las -fsched-spec-load -fsched-spec-load-dangerous -fsched-stalled-insns=0 -fsched-stalled-insns-dep -fsched2-use-superblocks -fipa-pta -fipa-matrix-reorg -ftree-loop-linear -floop-interchange -floop-strip-mine -floop-block -fgraphite-identity -floop-parallelize-all -ftree-loop-distribution -ftree-loop-im -ftree-loop-ivcanon -fivopts -fvect-cost-model -fvariable-expansion-in-unroller -fbranch-target-load-optimize -maccumulate-outgoing-args -flto")
- set(LDFLAGS_SPEED "${LDFLAGS} -s -fwhole-program -flto -fno-stack-protector")
- else()
- # Debug
- set(CFLAGS_DEBUG "${CFLAGS} -O0 -g3 -fstack-protector-all -D_FORTIFY_SOURCE=2 -DDEBUG")
- set(LDFLAGS_DEBUG "${LDFLAGS} -fstack-protector-all")
- set(CFLAGS_SIZE "${CFLAGS_DEBUG}")
- set(LDFLAGS_SIZE "${LDFLAGS_DEBUG}")
- set(CFLAGS_SPEED "${CFLAGS_DEBUG}")
- set(LDFLAGS_SPEED "${LDFLAGS_DEBUG}")
- endif()
-
- set(CMAKE_C_FLAGS "${CFLAGS_LANG_C} ${CFLAGS_SIZE}")
- set(CMAKE_CXX_FLAGS "${CFLAGS_LANG_CPP} ${CFLAGS_SIZE}")
- if(64BIT)
- set(CMAKE_SHARED_LIBRARY_C_FLAGS "-fpic")
- set(CMAKE_SHARED_LIBRARY_CXX_FLAGS "-fpic")
- set(CMAKE_SHARED_LIBRARY_ASM_FLAGS "-fpic")
- else()
- set(CMAKE_SHARED_LIBRARY_C_FLAGS "")
- set(CMAKE_SHARED_LIBRARY_CXX_FLAGS "")
- set(CMAKE_SHARED_LIBRARY_ASM_FLAGS "")
- endif()
- set(CMAKE_SHARED_LINKER_FLAGS "-shared ${LDFLAGS} ${LDFLAGS_SIZE}")
- set(CMAKE_EXE_LINKER_FLAGS "${LDFLAGS} ${LDFLAGS_SIZE}")
- set(CMAKE_RC_FLAGS "${RCFLAGS}")
- set(CMAKE_ASM_FLAGS "${CFLAGS}")
-
- if(WIN32)
- set(DIST_NAME "windows" CACHE STRING "Output folder name for the _dist folder (no start or end slash)")
- elseif(APPLE)
- set(DIST_NAME "mac" CACHE STRING "Output folder name for the _dist folder (no start or end slash)")
- elseif(UNIX)
- set(DIST_NAME "linux" CACHE STRING "Output folder name for the _dist folder (no start or end slash)")
- else()
- set(DIST_NAME "unknown" CACHE STRING "Output folder name for the _dist folder (no start or end slash)")
- endif()
-
-endif()
-
-#set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/_dist/${DIST_NAME}") (-flto means our archive files should not be redistributed)
-set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/_dist/${DIST_NAME}")
-set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/_dist/${DIST_NAME}")
-
-if(WIN32)
- set(FREETYPE_INCLUDE ${CMAKE_SOURCE_DIR}/_deps/freetype/include ${CMAKE_SOURCE_DIR}/_deps/freetype/include/freetype/config)
- set(LIBJPEGTURBO_INCLUDE ${CMAKE_SOURCE_DIR}/_deps/libjpeg-turbo)
- set(LIBMPG123_INCLUDE ${CMAKE_SOURCE_DIR}/_deps/libmpg123)
- set(LIBPNG_INCLUDE ${CMAKE_SOURCE_DIR}/_deps/libpng)
- set(LIBPQ_INCLUDE ${CMAKE_SOURCE_DIR}/_deps/libpq ${CMAKE_SOURCE_DIR}/_deps/libpq/include ${CMAKE_SOURCE_DIR}/_deps/libpq/include/port/win32 ${CMAKE_SOURCE_DIR}/_deps/libpq/include/port)
- set(ZLIB_INCLUDE ${CMAKE_SOURCE_DIR}/_deps/zlib)
-
- add_subdirectory(_deps/freetype)
- add_subdirectory(_deps/libjpeg-turbo)
- add_subdirectory(_deps/libmpg123)
- add_subdirectory(_deps/libpng)
- add_subdirectory(_deps/libpq)
- add_subdirectory(_deps/zlib)
-
- set(FREETYPE_LINK freetype_shared)
- set(LIBJPEG_LINK jpegturbo_static)
- set(LIBMPG123_LINK libmpg123_static)
- set(LIBPNG_LINK libpng_static)
- set(LIBPQ_LINK libpq_shared)
- set(ZLIB_LINK zlib_static)
-else()
- set(FREETYPE_LINK freetype)
- set(LIBJPEG_LINK jpeg)
- set(LIBMPG123_LINK mpg123)
- set(LIBPNG_LINK png)
- set(LIBPQ_LINK pq)
- set(ZLIB_LINK z)
-endif()
-
-set(FILEHANDLER_INCLUDE ${CMAKE_SOURCE_DIR}/library/FileHandler)
-set(LIBGLDEMO_INCLUDE ${CMAKE_SOURCE_DIR}/library/libgldemo)
-set(LIBVITABOY_INCLUDE ${CMAKE_SOURCE_DIR}/library/libvitaboy)
-
-if(WIN32)
- set(GLDEMO_EXE WIN32)
- set(GLDEMO_LINK mingw32 libgldemo_static opengl32 glu32)
-else()
- set(GLDEMO_EXE "")
- set(GLDEMO_LINK libgldemo_static Xxf86vm rt Xext X11 GL GLU)
-endif()
-
-add_subdirectory(Client)
-add_subdirectory(library)
-add_subdirectory(Server)
-add_subdirectory(Tools)
diff --git a/README.md b/README.md
index c35ecf9..f9059f2 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,9 @@ Will this succeed? *I have no idea*. I'm not much of a game developer, but that
## To do
-- [ ] Rewrite header files
+- [ ] Rewrite header files
+
+- [ ] Communicate with FreeSO-based API server
- [ ] Write Vitaboy renderer in Zig
diff --git a/server/FSO.Server.Api.Core/FSO.Server.Api.Core.csproj b/server/FSO.Server.Api.Core/FSO.Server.Api.Core.csproj
index 8ecab64..2d98db3 100755
--- a/server/FSO.Server.Api.Core/FSO.Server.Api.Core.csproj
+++ b/server/FSO.Server.Api.Core/FSO.Server.Api.Core.csproj
@@ -1,9 +1,9 @@
- netcoreapp2.2
+ net8.0
true
- false
+ false
@@ -63,4 +63,4 @@
-
+
\ No newline at end of file
diff --git a/server/README.md b/server/README.md
index 7a93b53..7fe71ce 100644
--- a/server/README.md
+++ b/server/README.md
@@ -2,7 +2,7 @@
zTSO uses FreeSO's API architecture for the backend. However, zTSO is not 100% compatible with FreeSO due to differences in how the simulation is managed. Instructions for setting up an API server are available in the appropriate directories. zTSO's API is based on FreeSO `beta-update-88b`.
-To maintain compatibility with legacy servers, zTSO follows a slightly altered version scheme that adheres to SemVer guidelines. Instead of `beta/update-88b`, it becomes `beta/update-0.88.101`. Prefixed letters are replaced with a three-digit incremental patch number, akin to .NET's SDK versions. This approach aims to ensure that zTSO servers cannot access FreeSO's servers while remaining familiar to existing users and server operators.
+To maintain compatibility with legacy servers, zTSO follows a slightly altered version scheme that adheres to SemVer guidelines. Instead of `beta/update-88b`, it becomes `beta/update-0.88.101`. Prefixed letters are replaced with a three-digit incremental patch number, akin to .NET's SDK versions. This approach aims to ensure that zTSO clients cannot access FreeSO's servers while remaining familiar to existing users and server operators.
As the API server is already complete, zTSO's client and server will have separate release cycles. The API server will have a Long-Term Support (LTS) release to support existing server operators, while the client will follow a rapid rolling release. To prevent conflicts between the two clients, the API server will be modified to check for zTSO-based clients as part of an additional handshake during login. This check will be ignored by FreeSO-based clients as it constitutes a separate API call.
diff --git a/server/tso.files/Endian.cs b/server/tso.files/Endian.cs
new file mode 100755
index 0000000..23e5bd3
--- /dev/null
+++ b/server/tso.files/Endian.cs
@@ -0,0 +1,64 @@
+using System;
+
+namespace FSO.Files
+{
+ public class Endian
+ {
+ static Endian()
+ {
+ _LittleEndian = BitConverter.IsLittleEndian;
+ }
+
+ public static short SwapInt16(short v)
+ {
+ return (short)(((v & 0xff) << 8) | ((v >> 8) & 0xff));
+ }
+
+ public static ushort SwapUInt16(ushort v)
+ {
+ return (ushort)(((v & 0xff) << 8) | ((v >> 8) & 0xff));
+ }
+
+ public static int SwapInt32(int v)
+ {
+ return (int)(((SwapInt16((short)v) & 0xffff) << 0x10) |
+ (SwapInt16((short)(v >> 0x10)) & 0xffff));
+ }
+
+ public static uint SwapUInt32(uint v)
+ {
+ return (uint)(((SwapUInt16((ushort)v) & 0xffff) << 0x10) |
+ (SwapUInt16((ushort)(v >> 0x10)) & 0xffff));
+ }
+
+ public static long SwapInt64(long v)
+ {
+ return (long)(((SwapInt32((int)v) & 0xffffffffL) << 0x20) |
+ (SwapInt32((int)(v >> 0x20)) & 0xffffffffL));
+ }
+
+ public static ulong SwapUInt64(ulong v)
+ {
+ return (ulong)(((SwapUInt32((uint)v) & 0xffffffffL) << 0x20) |
+ (SwapUInt32((uint)(v >> 0x20)) & 0xffffffffL));
+ }
+
+ public static bool IsBigEndian
+ {
+ get
+ {
+ return !_LittleEndian;
+ }
+ }
+
+ public static bool IsLittleEndian
+ {
+ get
+ {
+ return _LittleEndian;
+ }
+ }
+
+ private static readonly bool _LittleEndian;
+ }
+}
diff --git a/server/tso.files/FAR1/FAR1Archive.cs b/server/tso.files/FAR1/FAR1Archive.cs
new file mode 100755
index 0000000..f2d227a
--- /dev/null
+++ b/server/tso.files/FAR1/FAR1Archive.cs
@@ -0,0 +1,150 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.IO;
+
+namespace FSO.Files.FAR1
+{
+ ///
+ /// A FAR1 (File Archive v1) archive.
+ ///
+ public class FAR1Archive
+ {
+ private string m_Path;
+ private BinaryReader m_Reader;
+
+ private uint m_ManifestOffset;
+ private uint m_NumFiles;
+ private List m_Entries = new List();
+ private bool V1b = true;
+
+ ///
+ /// The offset into the archive of the manifest.
+ ///
+ public uint ManifestOffset
+ {
+ get { return m_ManifestOffset; }
+ }
+
+ ///
+ /// The number of files/entries in the archive.
+ ///
+ public uint NumFiles
+ {
+ get { return m_NumFiles; }
+ }
+
+ ///
+ /// Creates a new FAR1Archive instance from a path.
+ ///
+ /// The path to the archive.
+ public FAR1Archive(string Path, bool v1b)
+ {
+ m_Path = Path;
+ m_Reader = new BinaryReader(File.Open(Path, FileMode.Open, FileAccess.Read, FileShare.Read));
+
+ //Magic number - An 8-byte string (not null-terminated), consisting of the ASCII characters "FAR!byAZ"
+ string Header = Encoding.ASCII.GetString(m_Reader.ReadBytes(8));
+ //Version - A 4-byte unsigned integer specifying the version; 1a and 1b each specify 1.
+ uint Version = m_Reader.ReadUInt32();
+
+ if ((Header != "FAR!byAZ") || (Version != 1))
+ {
+ throw (new Exception("Archive wasn't a valid FAR V.1 archive!"));
+ }
+
+ //File table offset - A 4-byte unsigned integer specifying the offset to the file table
+ //from the beginning of the archive.
+ m_ManifestOffset = m_Reader.ReadUInt32();
+ m_Reader.BaseStream.Seek(m_ManifestOffset, SeekOrigin.Begin);
+
+ m_NumFiles = m_Reader.ReadUInt32();
+
+
+ for (int i = 0; i < m_NumFiles; i++)
+ {
+
+ FarEntry Entry = new FarEntry();
+ Entry.DataLength = m_Reader.ReadInt32();
+ Entry.DataLength2 = m_Reader.ReadInt32();
+ Entry.DataOffset = m_Reader.ReadInt32();
+ Entry.FilenameLength = (v1b) ? m_Reader.ReadInt16() : (short)m_Reader.ReadInt32();
+ Entry.Filename = Encoding.ASCII.GetString(m_Reader.ReadBytes(Entry.FilenameLength));
+
+ m_Entries.Add(Entry);
+ }
+ }
+
+ ///
+ /// Gets an entry based on a KeyValuePair.
+ ///
+ /// A KeyValuePair (string, byte[]) representing the entry. The byte array can be null.
+ /// A FarEntry or null if the entry wasn't found.
+ public byte[] GetEntry(KeyValuePair Entry)
+ {
+ foreach (FarEntry Ent in m_Entries)
+ {
+ if (Ent.Filename == Entry.Key)
+ {
+ m_Reader.BaseStream.Seek(Ent.DataOffset, SeekOrigin.Begin);
+ return m_Reader.ReadBytes(Ent.DataLength);
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// Gets an entry's data from a FarEntry instance.
+ ///
+ /// A FarEntry instance.
+ /// The entry's data.
+ public byte[] GetEntry(FarEntry Entry)
+ {
+ foreach (FarEntry Ent in m_Entries)
+ {
+ if (Ent.Filename == Entry.Filename)
+ {
+ m_Reader.BaseStream.Seek(Ent.DataOffset, SeekOrigin.Begin);
+ return m_Reader.ReadBytes(Ent.DataLength);
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// Returns a list of all FarEntry instances in this archive.
+ ///
+ ///
+ public List GetAllFarEntries()
+ {
+ return m_Entries;
+ }
+
+ ///
+ /// Gets all entries in the archive.
+ ///
+ /// A List of KeyValuePair instances.
+ public List> GetAllEntries()
+ {
+ List> Entries = new List>();
+
+ foreach (FarEntry Entry in m_Entries)
+ {
+ m_Reader.BaseStream.Seek(Entry.DataOffset, SeekOrigin.Begin);
+ byte[] Data = m_Reader.ReadBytes(Entry.DataLength);
+
+ KeyValuePair KvP = new KeyValuePair(Entry.Filename, Data);
+ Entries.Add(KvP);
+ }
+
+ return Entries;
+ }
+
+ public void Close()
+ {
+ m_Reader.Close();
+ }
+ }
+}
diff --git a/server/tso.files/FAR1/FarEntry.cs b/server/tso.files/FAR1/FarEntry.cs
new file mode 100755
index 0000000..1d4e4ba
--- /dev/null
+++ b/server/tso.files/FAR1/FarEntry.cs
@@ -0,0 +1,22 @@
+namespace FSO.Files.FAR1
+{
+ ///
+ /// Represents an entry in a FAR1 archive.
+ ///
+ public class FarEntry
+ {
+ //Decompressed data size - A 4-byte unsigned integer specifying the uncompressed size of the file.
+ public int DataLength;
+ //A 4-byte unsigned integer specifying the compressed size of the file; if this and the previous field are the same,
+ //the file is considered uncompressed. (It is the responsibility of the archiver to only store data compressed when
+ //its size is less than the size of the original data.) Note that The Sims 1 does not actually support any form
+ //of compression.
+ public int DataLength2;
+ //A 4-byte unsigned integer specifying the offset of the file from the beginning of the archive.
+ public int DataOffset;
+ //A 4-byte unsigned integer specifying the length of the filename field that follows.
+ public short FilenameLength;
+ //Filename - The name of the archived file; size depends on the previous field.
+ public string Filename;
+ }
+}
diff --git a/server/tso.files/FAR3/Decompresser.cs b/server/tso.files/FAR3/Decompresser.cs
new file mode 100755
index 0000000..89f1056
--- /dev/null
+++ b/server/tso.files/FAR3/Decompresser.cs
@@ -0,0 +1,419 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+
+namespace FSO.Files.FAR3
+{
+ ///
+ /// Represents a decompresser that can decompress files in a FAR3
+ /// archive. If you have some kind of need to understand this code, go to:
+ /// http://wiki.niotso.org/RefPack
+ /// The code in this class was ported from DBPF4J:
+ /// http://sc4dbpf4j.cvs.sourceforge.net/viewvc/sc4dbpf4j/DBPF4J/
+ ///
+ public class Decompresser
+ {
+ private long m_CompressedSize = 0;
+ private long m_DecompressedSize = 0;
+
+ public long DecompressedSize
+ {
+ get { return m_DecompressedSize; }
+ set { m_DecompressedSize = value; }
+ }
+
+ public long CompressedSize
+ {
+ get { return m_CompressedSize; }
+ set { m_CompressedSize = value; }
+ }
+
+ ///
+ /// Copies data from source to destination array.
+ /// The copy is byte by byte from srcPos to destPos and given length.
+ ///
+ /// The source array.
+ /// The source Position.
+ /// The destination array.
+ /// The destination Position.
+ /// The length.
+ private void ArrayCopy2(byte[] Src, int SrcPos, ref byte[] Dest, int DestPos, long Length)
+ {
+ if (Dest.Length < DestPos + Length)
+ {
+ byte[] DestExt = new byte[(int)(DestPos + Length)];
+ Array.Copy(Dest, 0, DestExt, 0, Dest.Length);
+ Dest = DestExt;
+ }
+
+ for (int i = 0; i < Length/* - 1*/; i++)
+ Dest[DestPos + i] = Src[SrcPos + i];
+ }
+
+ ///
+ /// Copies data from array at destPos-srcPos to array at destPos.
+ ///
+ /// The array.
+ /// The Position to copy from (reverse from end of array!)
+ /// The Position to copy to.
+ /// The length of data to copy.
+ private void OffsetCopy(ref byte[] array, int srcPos, int destPos, long length)
+ {
+ srcPos = destPos - srcPos;
+
+ if (array.Length < destPos + length)
+ {
+ byte[] NewArray = new byte[(int)(destPos + length)];
+ Array.Copy(array, 0, NewArray, 0, array.Length);
+ array = NewArray;
+ }
+
+ for (int i = 0; i < length /*- 1*/; i++)
+ {
+ array[destPos + i] = array[srcPos + i];
+ }
+ }
+
+ ///
+ /// Compresses data and returns it as an array of bytes.
+ /// Assumes that the array of bytes passed contains
+ /// uncompressed data.
+ ///
+ /// The data to be compressed.
+ /// An array of bytes with compressed data.
+ public byte[] Compress(byte[] Data)
+ {
+ // if data is big enough for compress
+ if (Data.Length > 6)
+ {
+ // some Compression Data
+ const int MAX_OFFSET = 0x20000;
+ const int MAX_COPY_COUNT = 0x404;
+ // used to finetune the lookup (small values increase the
+ // compression for Big Files)
+ const int QFS_MAXITER = 0x80;
+
+ // contains the latest offset for a combination of two
+ // characters
+ Dictionary cmpmap2 = new Dictionary();
+
+ // will contain the compressed data (maximal size =
+ // uncompressedSize+MAX_COPY_COUNT)
+ byte[] cData = new byte[Data.Length + MAX_COPY_COUNT];
+
+ // init some vars
+ int writeIndex = 9; // leave 9 bytes for the header
+ int lastReadIndex = 0;
+ ArrayList indexList = null;
+ int copyOffset = 0;
+ int copyCount = 0;
+ int index = -1;
+ bool end = false;
+
+ // begin main compression loop
+ while (index < Data.Length - 3)
+ {
+ // get all Compression Candidates (list of offsets for all
+ // occurances of the current 3 bytes)
+ do
+ {
+ index++;
+ if (index >= Data.Length - 2)
+ {
+ end = true;
+ break;
+ }
+ int mapindex = Data[index] + (Data[index + 1] << 8)
+ + (Data[index + 2] << 16);
+
+ indexList = cmpmap2[mapindex];
+ if (indexList == null)
+ {
+ indexList = new ArrayList();
+ cmpmap2.Add(mapindex, indexList);
+ }
+ indexList.Add(index);
+ } while (index < lastReadIndex);
+ if (end)
+ break;
+
+ // find the longest repeating byte sequence in the index
+ // List (for offset copy)
+ int offsetCopyCount = 0;
+ int loopcount = 1;
+ while ((loopcount < indexList.Count) && (loopcount < QFS_MAXITER))
+ {
+ int foundindex = (int) indexList[(indexList.Count - 1) - loopcount];
+ if ((index - foundindex) >= MAX_OFFSET)
+ {
+ break;
+ }
+
+ loopcount++;
+ copyCount = 3;
+
+ while ((Data.Length > index + copyCount)&& (Data[index + copyCount] == Data[foundindex + copyCount]) && (copyCount < MAX_COPY_COUNT))
+ {
+ copyCount++;
+ }
+
+ if (copyCount > offsetCopyCount)
+ {
+ offsetCopyCount = copyCount;
+ copyOffset = index - foundindex;
+ }
+ }
+
+ // check if we can compress this
+ // In FSH Tool stand additionally this:
+ if (offsetCopyCount > Data.Length - index)
+ {
+ offsetCopyCount = index - Data.Length;
+ }
+ if (offsetCopyCount <= 2)
+ {
+ offsetCopyCount = 0;
+ }
+ else if ((offsetCopyCount == 3) && (copyOffset > 0x400))
+ { // 1024
+ offsetCopyCount = 0;
+ }
+ else if ((offsetCopyCount == 4) && (copyOffset > 0x4000))
+ { // 16384
+ offsetCopyCount = 0;
+ }
+
+ // this is offset-compressable? so do the compression
+ if (offsetCopyCount > 0)
+ {
+ // plaincopy
+
+ // In FSH Tool stand this (A):
+ while (index - lastReadIndex >= 4)
+ {
+ copyCount = (index - lastReadIndex) / 4 - 1;
+ if (copyCount > 0x1B)
+ {
+ copyCount = 0x1B;
+ }
+ cData[writeIndex++] = (byte)(0xE0 + copyCount);
+ copyCount = 4 * copyCount + 4;
+
+ ArrayCopy2(Data, lastReadIndex, ref cData, writeIndex, copyCount);
+ lastReadIndex += copyCount;
+ writeIndex += copyCount;
+ }
+
+ // offsetcopy
+ copyCount = index - lastReadIndex;
+ copyOffset--;
+ if ((offsetCopyCount <= 0x0A) && (copyOffset < 0x400))
+ {
+ cData[writeIndex++] = (byte) (((copyOffset >> 8) << 5)
+ + ((offsetCopyCount - 3) << 2) + copyCount);
+ cData[writeIndex++] = (byte)(copyOffset & 0xff);
+ }
+ else if ((offsetCopyCount <= 0x43) && (copyOffset < 0x4000))
+ {
+ cData[writeIndex++] = (byte)(0x80 + (offsetCopyCount - 4));
+ cData[writeIndex++] = (byte) ((copyCount << 6) + (copyOffset >> 8));
+ cData[writeIndex++] = (byte) (copyOffset & 0xff);
+ }
+ else if ((offsetCopyCount <= MAX_COPY_COUNT) && (copyOffset < MAX_OFFSET))
+ {
+ cData[writeIndex++] = (byte)(0xc0
+ + ((copyOffset >> 16) << 4)
+ + (((offsetCopyCount - 5) >> 8) << 2) + copyCount);
+ cData[writeIndex++] = (byte)((copyOffset >> 8) & 0xff);
+ cData[writeIndex++] = (byte)(copyOffset & 0xff);
+ cData[writeIndex++] = (byte)((offsetCopyCount - 5) & 0xff);
+ }
+
+ // do the offset copy
+ ArrayCopy2(Data, lastReadIndex, ref cData, writeIndex, copyCount);
+ writeIndex += copyCount;
+ lastReadIndex += copyCount;
+ lastReadIndex += offsetCopyCount;
+ }
+ }
+
+ // add the End Record
+ index = Data.Length;
+ // in FSH Tool stand the same as above (A)
+ while (index - lastReadIndex >= 4)
+ {
+ copyCount = (index - lastReadIndex) / 4 - 1;
+
+ if (copyCount > 0x1B)
+ copyCount = 0x1B;
+
+ cData[writeIndex++] = (byte)(0xE0 + copyCount);
+ copyCount = 4 * copyCount + 4;
+
+ ArrayCopy2(Data, lastReadIndex, ref cData, writeIndex, copyCount);
+ lastReadIndex += copyCount;
+ writeIndex += copyCount;
+ }
+
+ copyCount = index - lastReadIndex;
+ cData[writeIndex++] = (byte) (0xfc + copyCount);
+ ArrayCopy2(Data, lastReadIndex, ref cData, writeIndex, copyCount);
+ writeIndex += copyCount;
+ lastReadIndex += copyCount;
+
+ MemoryStream DataStream = new MemoryStream();
+ BinaryWriter Writer = new BinaryWriter(DataStream);
+
+ // write the header for the compressed data
+ // set the compressed size
+ Writer.Write((uint)writeIndex);
+ m_CompressedSize = writeIndex;
+ // set the MAGICNUMBER
+ Writer.Write((ushort)0xFB10);
+ // set the decompressed size
+ byte[] revData = BitConverter.GetBytes(Data.Length);
+ Writer.Write((revData[2] << 16) | (revData[1] << 8) | revData[0]);
+ Writer.Write(cData);
+
+ //Avoid nasty swearing here!
+ Writer.Flush();
+
+ m_DecompressedSize = Data.Length;
+
+ return DataStream.ToArray();
+ }
+
+ return Data;
+ }
+
+ ///
+ /// Decompresses data and returns it as an
+ /// uncompressed array of bytes.
+ ///
+ /// The data to decompress.
+ /// An uncompressed array of bytes.
+ public byte[] Decompress(byte[] Data)
+ {
+
+ MemoryStream MemData = new MemoryStream(Data);
+ BinaryReader Reader = new BinaryReader(MemData);
+
+ if (Data.Length > 6)
+ {
+ byte[] DecompressedData = new byte[(int)m_DecompressedSize];
+ int DataPos = 0;
+
+ int Pos = 0;
+ long Control1 = 0;
+
+ while (Control1 != 0xFC && Pos < Data.Length)
+ {
+ Control1 = Data[Pos];
+ Pos++;
+
+ if (Pos == Data.Length)
+ break;
+
+ if (Control1 >= 0 && Control1 <= 127)
+ {
+ // 0x00 - 0x7F
+ long control2 = Data[Pos];
+ Pos++;
+ long numberOfPlainText = (Control1 & 0x03);
+ ArrayCopy2(Data, Pos, ref DecompressedData, DataPos, numberOfPlainText);
+ DataPos += (int)numberOfPlainText;
+ Pos += (int)numberOfPlainText;
+
+ if (DataPos == (DecompressedData.Length))
+ break;
+
+ int offset = (int)(((Control1 & 0x60) << 3) + (control2) + 1);
+ long numberToCopyFromOffset = ((Control1 & 0x1C) >> 2) + 3;
+ OffsetCopy(ref DecompressedData, offset, DataPos, numberToCopyFromOffset);
+ DataPos += (int)numberToCopyFromOffset;
+
+ if (DataPos == (DecompressedData.Length))
+ break;
+ }
+ else if ((Control1 >= 128 && Control1 <= 191))
+ {
+ // 0x80 - 0xBF
+ long control2 = Data[Pos];
+ Pos++;
+ long control3 = Data[Pos];
+ Pos++;
+
+ long numberOfPlainText = (control2 >> 6) & 0x03;
+ ArrayCopy2(Data, Pos, ref DecompressedData, DataPos, numberOfPlainText);
+ DataPos += (int)numberOfPlainText;
+ Pos += (int)numberOfPlainText;
+
+ if (DataPos == (DecompressedData.Length))
+ break;
+
+ int offset = (int)(((control2 & 0x3F) << 8) + (control3) + 1);
+ long numberToCopyFromOffset = (Control1 & 0x3F) + 4;
+ OffsetCopy(ref DecompressedData, offset, DataPos, numberToCopyFromOffset);
+ DataPos += (int)numberToCopyFromOffset;
+
+ if (DataPos == (DecompressedData.Length))
+ break;
+ }
+ else if (Control1 >= 192 && Control1 <= 223)
+ {
+ // 0xC0 - 0xDF
+ long numberOfPlainText = (Control1 & 0x03);
+ long control2 = Data[Pos];
+ Pos++;
+ long control3 = Data[Pos];
+ Pos++;
+ long control4 = Data[Pos];
+ Pos++;
+ ArrayCopy2(Data, Pos, ref DecompressedData, DataPos, numberOfPlainText);
+ DataPos += (int)numberOfPlainText;
+ Pos += (int)numberOfPlainText;
+
+ if (DataPos == (DecompressedData.Length))
+ break;
+
+ int offset = (int)(((Control1 & 0x10) << 12) + (control2 << 8) + (control3) + 1);
+ long numberToCopyFromOffset = ((Control1 & 0x0C) << 6) + (control4) + 5;
+ OffsetCopy(ref DecompressedData, offset, DataPos, numberToCopyFromOffset);
+ DataPos += (int)numberToCopyFromOffset;
+
+ if (DataPos == (DecompressedData.Length))
+ break;
+ }
+ else if (Control1 >= 224 && Control1 <= 251)
+ {
+ // 0xE0 - 0xFB
+ long numberOfPlainText = ((Control1 & 0x1F) << 2) + 4;
+ ArrayCopy2(Data, Pos, ref DecompressedData, DataPos, numberOfPlainText);
+ DataPos += (int)numberOfPlainText;
+ Pos += (int)numberOfPlainText;
+
+ if (DataPos == (DecompressedData.Length))
+ break;
+ }
+ else
+ {
+ long numberOfPlainText = (Control1 & 0x03);
+ ArrayCopy2(Data, Pos, ref DecompressedData, DataPos, numberOfPlainText);
+
+ DataPos += (int)numberOfPlainText;
+ Pos += (int)numberOfPlainText;
+
+ if (DataPos == (DecompressedData.Length))
+ break;
+ }
+ }
+
+ return DecompressedData;
+ }
+
+ //No data to decompress
+ return Data;
+ }
+ }
+}
\ No newline at end of file
diff --git a/server/tso.files/FAR3/FAR3Archive.cs b/server/tso.files/FAR3/FAR3Archive.cs
new file mode 100755
index 0000000..ddb392c
--- /dev/null
+++ b/server/tso.files/FAR3/FAR3Archive.cs
@@ -0,0 +1,243 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.IO;
+
+namespace FSO.Files.FAR3
+{
+ ///
+ /// Represents a single FAR3 archive.
+ ///
+ public class FAR3Archive : IDisposable
+ {
+ private BinaryReader m_Reader;
+ public static bool isReadingSomething = false;
+
+ private string m_ArchivePath;
+ private Dictionary m_Entries = new Dictionary();
+ private List m_EntriesList = new List();
+ private Dictionary m_EntryByID = new Dictionary();
+ private uint m_ManifestOffset;
+
+ ///
+ /// Creates a new FAR3Archive instance from a path.
+ ///
+ /// The path to the archive.
+ public FAR3Archive(string Path)
+ {
+ m_ArchivePath = Path;
+
+ if (isReadingSomething == false)
+ {
+ isReadingSomething = true;
+
+ try
+ {
+ m_Reader = new BinaryReader(File.Open(Path, FileMode.Open, FileAccess.Read, FileShare.Read));
+ }
+ catch (Exception ex)
+ {
+ throw new FAR3Exception("Could not open the specified archive - " + Path + "! (FAR3Archive())");
+ }
+
+ string Header = Encoding.ASCII.GetString(m_Reader.ReadBytes(8));
+ uint Version = m_Reader.ReadUInt32();
+
+ if ((Header != "FAR!byAZ") || (Version != 3))
+ {
+ throw new FAR3Exception("Archive wasn't a valid FAR V.3 archive! (FAR3Archive())");
+ }
+
+ uint ManifestOffset = m_Reader.ReadUInt32();
+ m_ManifestOffset = ManifestOffset;
+
+ m_Reader.BaseStream.Seek(ManifestOffset, SeekOrigin.Begin);
+
+ uint NumFiles = m_Reader.ReadUInt32();
+
+ for (int i = 0; i < NumFiles; i++)
+ {
+ Far3Entry Entry = new Far3Entry();
+ Entry.DecompressedFileSize = m_Reader.ReadUInt32();
+ byte dummy0 = m_Reader.ReadByte();
+ byte dummy1 = m_Reader.ReadByte();
+ byte dummy2 = m_Reader.ReadByte();
+ Entry.CompressedFileSize = (uint)((dummy0 << 0) | (dummy1 << 8) | (dummy2) << 16);
+ Entry.DataType = m_Reader.ReadByte();
+ Entry.DataOffset = m_Reader.ReadUInt32();
+ //Entry.HasFilename = m_Reader.ReadUInt16();
+ Entry.IsCompressed = m_Reader.ReadByte();
+ Entry.AccessNumber = m_Reader.ReadByte();
+ Entry.FilenameLength = m_Reader.ReadUInt16();
+ Entry.TypeID = m_Reader.ReadUInt32();
+ Entry.FileID = m_Reader.ReadUInt32();
+ Entry.Filename = Encoding.ASCII.GetString(m_Reader.ReadBytes(Entry.FilenameLength));
+
+ if (!m_Entries.ContainsKey(Entry.Filename))
+ m_Entries.Add(Entry.Filename, Entry);
+ m_EntriesList.Add(Entry);
+
+ m_EntryByID.Add(Entry.FileID, Entry); //isn't this a bad idea? i have a feeling this is a bad idea...
+ }
+
+ //Keep the stream open, it helps peformance.
+ //m_Reader.Close();
+ isReadingSomething = false;
+ }
+ }
+
+ ///
+ /// Gets an entry's data from a Far3Entry instance.
+ ///
+ /// The Far3Entry instance.
+ /// The entry's data.
+ public byte[] GetEntry(Far3Entry Entry)
+ {
+ lock (m_Reader)
+ {
+ m_Reader.BaseStream.Seek((long)Entry.DataOffset, SeekOrigin.Begin);
+
+ isReadingSomething = true;
+
+ if (Entry.IsCompressed == 0x01)
+ {
+ m_Reader.BaseStream.Seek(9, SeekOrigin.Current);
+ uint Filesize = m_Reader.ReadUInt32();
+ ushort CompressionID = m_Reader.ReadUInt16();
+
+ if (CompressionID == 0xFB10)
+ {
+ byte dummy0 = m_Reader.ReadByte();
+ byte dummy1 = m_Reader.ReadByte();
+ byte dummy2 = m_Reader.ReadByte();
+ uint DecompressedSize = (uint)((dummy0 << 0x10) | (dummy1 << 0x08) | +dummy2);
+
+ Decompresser Dec = new Decompresser();
+ Dec.CompressedSize = Filesize;
+ Dec.DecompressedSize = DecompressedSize;
+
+ byte[] DecompressedData = Dec.Decompress(m_Reader.ReadBytes((int)Filesize));
+ //m_Reader.Close();
+
+ isReadingSomething = false;
+
+ return DecompressedData;
+ }
+ else
+ {
+ m_Reader.BaseStream.Seek((m_Reader.BaseStream.Position - 15), SeekOrigin.Begin);
+
+ byte[] Data = m_Reader.ReadBytes((int)Entry.DecompressedFileSize);
+ //m_Reader.Close();
+
+ isReadingSomething = false;
+
+ return Data;
+ }
+ }
+ else
+ {
+ byte[] Data = m_Reader.ReadBytes((int)Entry.DecompressedFileSize);
+ //m_Reader.Close();
+
+ isReadingSomething = false;
+
+ return Data;
+ }
+ }
+
+ throw new FAR3Exception("FAR3Entry didn't exist in archive - FAR3Archive.GetEntry()");
+ }
+
+ ///
+ /// Returns the entries of this FAR3Archive as byte arrays together with their corresponding FileIDs.
+ ///
+ /// A List of KeyValuePair instances.
+ public List> GetAllEntries()
+ {
+ List> toReturn = new List>();
+ foreach (Far3Entry Entry in m_EntriesList)
+ {
+ toReturn.Add(new KeyValuePair(Entry.FileID, GetEntry(Entry)));
+ }
+ return toReturn;
+ }
+
+ ///
+ /// Returns the entries of this FAR3Archive as FAR3Entry instances in a List.
+ ///
+ /// Returns the entries of this FAR3Archive as FAR3Entry instances in a List.
+ public List GetAllFAR3Entries()
+ {
+ List Entries = new List();
+
+ foreach (KeyValuePair KVP in m_Entries)
+ Entries.Add(KVP.Value);
+
+ return Entries;
+ }
+
+ ///
+ /// Gets an entry from a FileID.
+ ///
+ /// The entry's FileID.
+ /// The entry's data.
+ public byte[] GetItemByID(uint FileID)
+ {
+ var item = m_EntryByID[FileID];
+ if (item == null)
+ {
+ throw new FAR3Exception("Didn't find entry!");
+ }
+ return GetEntry(item);
+ }
+
+ ///
+ /// Gets an entry from its ID (TypeID + FileID).
+ ///
+ /// The ID of the entry.
+ /// The entry's data.
+ public byte[] GetItemByID(ulong ID)
+ {
+ byte[] Bytes = BitConverter.GetBytes(ID);
+ uint FileID = BitConverter.ToUInt32(Bytes, 4);
+ uint TypeID = BitConverter.ToUInt32(Bytes, 0);
+
+ var item = m_EntryByID[FileID];
+ if (item == null || item.TypeID != TypeID)
+ {
+ throw new FAR3Exception("Didn't find entry!");
+ }
+
+ return GetEntry(item);
+ }
+
+ ///
+ /// Gets an entry's data from a filename.
+ ///
+ /// The filename of the entry.
+ /// The entry's data.
+ public byte[] this[string Filename]
+ {
+ get
+ {
+ return GetEntry(m_Entries[Filename]);
+ }
+ }
+
+ #region IDisposable Members
+
+ ///
+ /// Disposes this FAR3Archive instance.
+ ///
+ public void Dispose()
+ {
+ if (m_Reader != null)
+ {
+ m_Reader.Close();
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/server/tso.files/FAR3/FAR3Exception.cs b/server/tso.files/FAR3/FAR3Exception.cs
new file mode 100755
index 0000000..8fa13db
--- /dev/null
+++ b/server/tso.files/FAR3/FAR3Exception.cs
@@ -0,0 +1,15 @@
+using System;
+
+namespace FSO.Files.FAR3
+{
+ ///
+ /// Represents an exception thrown by a FAR3Archive instance.
+ ///
+ public class FAR3Exception : Exception
+ {
+ public FAR3Exception(string Message)
+ : base(Message)
+ {
+ }
+ }
+}
diff --git a/server/tso.files/FAR3/Far3Entry.cs b/server/tso.files/FAR3/Far3Entry.cs
new file mode 100755
index 0000000..f3f48cf
--- /dev/null
+++ b/server/tso.files/FAR3/Far3Entry.cs
@@ -0,0 +1,32 @@
+namespace FSO.Files.FAR3
+{
+ ///
+ /// Represents an entry in a FAR3 archive.
+ ///
+ public class Far3Entry
+ {
+ //A 4-byte unsigned integer specifying the uncompressed size of the file.
+ public uint DecompressedFileSize;
+ // A 3-byte unsigned integer specifying the compressed size of the file (including
+ //the Persist header); if the data is raw, this field is ignored (though TSO's game
+ //files have this set to the same first three bytes as the previous field).
+ public uint CompressedFileSize;
+ //Data type - A single byte used to describe what type of data is pointed to by the Data offset field.
+ //The value can be 0x80 to denote that the data is a Persist container or 0x00 to denote that it is raw data.
+ public byte DataType;
+ //A 4-byte unsigned integer specifying the offset of the file from the beginning of the archive.
+ public uint DataOffset;
+ //A byte (can be either 0 or 1) specifying if this file is compressed.
+ public byte IsCompressed;
+ //A byte specifying the number of files this time has been accessed?
+ public byte AccessNumber;
+ //A 2-byte unsigned integer specifying the length of the filename field.
+ public ushort FilenameLength;
+ //A 4-byte integer describing what type of file is held.
+ public uint TypeID;
+ //A 4-byte ID assigned to the file which, together with the Type ID, is assumed to be unique all throughout the game.
+ public uint FileID;
+ //The name of the archived file; size depends on the filename length field.
+ public string Filename;
+ }
+}
diff --git a/server/tso.files/FSO.Files.csproj b/server/tso.files/FSO.Files.csproj
new file mode 100755
index 0000000..6c47203
--- /dev/null
+++ b/server/tso.files/FSO.Files.csproj
@@ -0,0 +1,265 @@
+
+
+
+ Debug
+ AnyCPU
+ 9.0.30729
+ 2.0
+ {18583453-A970-4AC5-83B1-2D6BFDF94C24}
+ Library
+ Properties
+ FSO.Files
+ FSO.Files
+ v4.5
+ 512
+
+
+
+
+ 3.5
+ publish\
+ true
+ Disk
+ false
+ Foreground
+ 7
+ Days
+ false
+ false
+ true
+ 0
+ 1.0.0.%2a
+ false
+ false
+ true
+
+
+
+ true
+ bin\x86\Debug\
+ DEBUG;TRACE
+ true
+ full
+ x86
+ prompt
+ MinimumRecommendedRules.ruleset
+ true
+
+
+ bin\x86\Release\
+ TRACE
+ true
+ true
+ pdbonly
+ x86
+ prompt
+ MinimumRecommendedRules.ruleset
+
+
+ true
+ bin\Debug\
+ DEBUG;TRACE
+ true
+ full
+ AnyCPU
+ prompt
+ MinimumRecommendedRules.ruleset
+
+
+ bin\Release\
+ TRACE
+ true
+ true
+ pdbonly
+ AnyCPU
+ prompt
+ MinimumRecommendedRules.ruleset
+ true
+
+
+ bin\x86\ServerRelease\
+ TRACE
+ true
+ true
+ pdbonly
+ x86
+ prompt
+ MinimumRecommendedRules.ruleset
+
+
+ bin\ServerRelease\
+ TRACE
+ true
+ true
+ pdbonly
+ AnyCPU
+ prompt
+ MinimumRecommendedRules.ruleset
+
+
+
+ ..\packages\bz2portable.1.0.1\lib\bz2portable.dll
+
+
+ ..\packages\deltaq.1.0.1\lib\deltaq.dll
+
+
+ ..\packages\MonoGame.Framework.Portable.3.6.0.1625\lib\portable-net45+win8+wpa81\MonoGame.Framework.dll
+ False
+
+
+
+
+
+
+
+ ..\packages\xxHashSharp.1.0.0\lib\net45\xxHashSharp.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Code
+
+
+ Code
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ False
+ Microsoft .NET Framework 4 %28x86 and x64%29
+ true
+
+
+ False
+ .NET Framework 3.5 SP1 Client Profile
+ false
+
+
+ False
+ .NET Framework 3.5 SP1
+ false
+
+
+ False
+ Windows Installer 3.1
+ true
+
+
+
+
+ {d8232422-9d79-4200-a981-eb70ed82ccf3}
+ TargaImagePCL
+
+
+ {c42962a1-8796-4f47-9dcd-79ed5904d8ca}
+ FSO.Common
+
+
+
+
+
\ No newline at end of file
diff --git a/server/tso.files/Formats/DBPF/DBPFEntry.cs b/server/tso.files/Formats/DBPF/DBPFEntry.cs
new file mode 100755
index 0000000..90d0d8d
--- /dev/null
+++ b/server/tso.files/Formats/DBPF/DBPFEntry.cs
@@ -0,0 +1,60 @@
+namespace FSO.Files.Formats.DBPF
+{
+ ///
+ /// GroupIDs for DBPF archives, defined in sys\\tsosounddata.ini
+ ///
+ public enum DBPFGroupID : uint
+ {
+ Multiplayer = 0x29dd0888,
+ Custom = 0x29daa4a6,
+ CustomTrks = 0x29d9359d,
+ Tracks = 0xa9c6c89a,
+ TrackDefs = 0xfdbdbf87,
+ tsov2 = 0x69c6c943,
+ Samples = 0x9dbdbf89,
+ HitLists = 0x9dbdbf74,
+ HitListsTemp = 0xc9c6c9b3,
+ Stings = 0xddbdbf8c,
+ HitLabUI = 0x1d6962cf,
+ HitLabTestSamples = 0x1d8a8b4f,
+ HitLabTest = 0xbd6e5937,
+ EP2 = 0xdde8f5c6,
+ EP5Samps = 0x8a6fcc30
+ }
+
+ ///
+ /// TypeIDs for DBPF archives, defined in sys\\tsoaudio.ini
+ ///
+ public enum DBPFTypeID : uint
+ {
+ XA = 0x1d07eb4b,
+ UTK = 0x1b6b9806,
+ WAV = 0xbb7051f5,
+ MP3 = 0x3cec2b47,
+ TRK = 0x5D73A611,
+ HIT = 0x7b1acfcd,
+ SoundFX = 0x2026960b,
+ }
+
+ ///
+ /// Represents an entry in a DBPF archive.
+ ///
+ public class DBPFEntry
+ {
+ //A 4-byte integer describing what type of file is held
+ public DBPFTypeID TypeID;
+
+ //A 4-byte integer identifying what resource group the file belongs to
+ public DBPFGroupID GroupID;
+
+ //A 4-byte ID assigned to the file which, together with the Type ID and the second instance ID (if applicable), is assumed to be unique all throughout the game
+ public uint InstanceID;
+ //too bad we're not using a version with a second instance id!!
+
+ //A 4-byte unsigned integer specifying the offset to the entry's data from the beginning of the archive
+ public uint FileOffset;
+
+ //A 4-byte unsigned integer specifying the size of the entry's data
+ public uint FileSize;
+ }
+}
diff --git a/server/tso.files/Formats/DBPF/DBPFFile.cs b/server/tso.files/Formats/DBPF/DBPFFile.cs
new file mode 100755
index 0000000..a018e9d
--- /dev/null
+++ b/server/tso.files/Formats/DBPF/DBPFFile.cs
@@ -0,0 +1,184 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using FSO.Files.Utils;
+
+namespace FSO.Files.Formats.DBPF
+{
+ ///
+ /// The database-packed file (DBPF) is a format used to store data for pretty much all Maxis games after The Sims,
+ /// including The Sims Online (the first appearance of this format), SimCity 4, The Sims 2, Spore, The Sims 3, and
+ /// SimCity 2013.
+ ///
+ public class DBPFFile : IDisposable
+ {
+ public int DateCreated;
+ public int DateModified;
+
+ private uint IndexMajorVersion;
+ private uint NumEntries;
+ private IoBuffer m_Reader;
+
+ private List m_EntriesList = new List();
+ private Dictionary m_EntryByID = new Dictionary();
+ private Dictionary> m_EntriesByType = new Dictionary>();
+
+ private IoBuffer Io;
+
+ ///
+ /// Constructs a new DBPF instance.
+ ///
+ public DBPFFile()
+ {
+ }
+
+ ///
+ /// Creates a DBPF instance from a path.
+ ///
+ /// The path to an DBPF archive.
+ public DBPFFile(string file)
+ {
+ var stream = File.OpenRead(file);
+ Read(stream);
+ }
+
+ ///
+ /// Reads a DBPF archive from a stream.
+ ///
+ /// The stream to read from.
+ public void Read(Stream stream)
+ {
+ m_EntryByID = new Dictionary();
+ m_EntriesList = new List();
+
+ var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN);
+ m_Reader = io;
+ this.Io = io;
+
+ var magic = io.ReadCString(4);
+ if (magic != "DBPF")
+ {
+ throw new Exception("Not a DBPF file");
+ }
+
+ var majorVersion = io.ReadUInt32();
+ var minorVersion = io.ReadUInt32();
+ var version = majorVersion + (((double)minorVersion)/10.0);
+
+ /** Unknown, set to 0 **/
+ io.Skip(12);
+
+ if (version == 1.0)
+ {
+ this.DateCreated = io.ReadInt32();
+ this.DateModified = io.ReadInt32();
+ }
+
+ if (version < 2.0)
+ {
+ IndexMajorVersion = io.ReadUInt32();
+ }
+
+ NumEntries = io.ReadUInt32();
+ uint indexOffset = 0;
+ if (version < 2.0)
+ {
+ indexOffset = io.ReadUInt32();
+ }
+ var indexSize = io.ReadUInt32();
+
+ if (version < 2.0)
+ {
+ var trashEntryCount = io.ReadUInt32();
+ var trashIndexOffset = io.ReadUInt32();
+ var trashIndexSize = io.ReadUInt32();
+ var indexMinor = io.ReadUInt32();
+ }
+ else if (version == 2.0)
+ {
+ var indexMinor = io.ReadUInt32();
+ indexOffset = io.ReadUInt32();
+ io.Skip(4);
+ }
+
+ /** Padding **/
+ io.Skip(32);
+
+ io.Seek(SeekOrigin.Begin, indexOffset);
+ for (int i = 0; i < NumEntries; i++)
+ {
+ var entry = new DBPFEntry();
+ entry.TypeID = (DBPFTypeID)io.ReadUInt32();
+ entry.GroupID = (DBPFGroupID)io.ReadUInt32();
+ entry.InstanceID = io.ReadUInt32();
+ entry.FileOffset = io.ReadUInt32();
+ entry.FileSize = io.ReadUInt32();
+
+ m_EntriesList.Add(entry);
+ ulong id = (((ulong)entry.InstanceID) << 32) + (ulong)entry.TypeID;
+ if (!m_EntryByID.ContainsKey(id))
+ m_EntryByID.Add(id, entry);
+
+ if (!m_EntriesByType.ContainsKey(entry.TypeID))
+ m_EntriesByType.Add(entry.TypeID, new List());
+
+ m_EntriesByType[entry.TypeID].Add(entry);
+ }
+ }
+
+ ///
+ /// Gets a DBPFEntry's data from this DBPF instance.
+ ///
+ /// Entry to retrieve data for.
+ /// Data for entry.
+ public byte[] GetEntry(DBPFEntry entry)
+ {
+ m_Reader.Seek(SeekOrigin.Begin, entry.FileOffset);
+
+ return m_Reader.ReadBytes((int)entry.FileSize);
+ }
+
+ ///
+ /// Gets an entry from its ID (TypeID + FileID).
+ ///
+ /// The ID of the entry.
+ /// The entry's data.
+ public byte[] GetItemByID(ulong ID)
+ {
+ if (m_EntryByID.ContainsKey(ID))
+ return GetEntry(m_EntryByID[ID]);
+ else
+ return null;
+ }
+
+ ///
+ /// Gets all entries of a specific type.
+ ///
+ /// The Type of the entry.
+ /// The entry data, paired with its instance id.
+ public List> GetItemsByType(DBPFTypeID Type)
+ {
+
+ var result = new List>();
+
+ var entries = m_EntriesByType[Type];
+ for (int i = 0; i < entries.Count; i++)
+ {
+ result.Add(new KeyValuePair(entries[i].InstanceID, GetEntry(entries[i])));
+ }
+ return result;
+ }
+
+ #region IDisposable Members
+
+ ///
+ /// Disposes this DBPF instance.
+ ///
+ public void Dispose()
+ {
+ Io.Dispose();
+ }
+
+ #endregion
+ }
+}
diff --git a/server/tso.files/Formats/IFF/AbstractIffChunk.cs b/server/tso.files/Formats/IFF/AbstractIffChunk.cs
new file mode 100755
index 0000000..666ac0d
--- /dev/null
+++ b/server/tso.files/Formats/IFF/AbstractIffChunk.cs
@@ -0,0 +1,51 @@
+using System.IO;
+
+namespace FSO.Files.Formats.IFF
+{
+ ///
+ /// An IFF is made up of chunks.
+ ///
+ public abstract class IffChunk
+ {
+ public ushort ChunkID;
+ public ushort ChunkFlags;
+ public string ChunkType; //just making it easier to access
+ public string ChunkLabel;
+ public bool ChunkProcessed;
+ public byte[] OriginalData; //IDE ONLY: always contains base data for any chunk.
+ public ushort OriginalID;
+ public bool AddedByPatch;
+ public string OriginalLabel;
+ public byte[] ChunkData;
+ public IffFile ChunkParent;
+
+ public ChunkRuntimeState RuntimeInfo = ChunkRuntimeState.Normal;
+
+ ///
+ /// Reads this chunk from an IFF.
+ ///
+ /// The IFF to read from.
+ /// The stream to read from.
+ public abstract void Read(IffFile iff, Stream stream);
+
+ ///
+ /// The name of this chunk.
+ ///
+ /// The name of this chunk as a string.
+ public override string ToString()
+ {
+ return "#" + ChunkID.ToString() + " " + ChunkLabel;
+ }
+
+ ///
+ /// Attempts to write this chunk to a stream (presumably an IFF or PIFF)
+ ///
+ ///
+ ///
+ /// True if data has been written, false if not.
+ public virtual bool Write(IffFile iff, Stream stream) { return false; }
+
+ public virtual void Dispose() { }
+
+ }
+}
diff --git a/server/tso.files/Formats/IFF/ChunkRuntimeInfo.cs b/server/tso.files/Formats/IFF/ChunkRuntimeInfo.cs
new file mode 100755
index 0000000..0faac15
--- /dev/null
+++ b/server/tso.files/Formats/IFF/ChunkRuntimeInfo.cs
@@ -0,0 +1,10 @@
+namespace FSO.Files.Formats.IFF
+{
+ public enum ChunkRuntimeState
+ {
+ Normal,
+ Patched, //unmodified, but still save when outputting PIFF
+ Modified, //modified. save when outputting PIFF
+ Delete //this chunk should not be saved, or should be saved as a deletion.
+ }
+}
\ No newline at end of file
diff --git a/server/tso.files/Formats/IFF/Chunks/ARRY.cs b/server/tso.files/Formats/IFF/Chunks/ARRY.cs
new file mode 100755
index 0000000..0f92c3d
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/ARRY.cs
@@ -0,0 +1,142 @@
+using FSO.Files.Utils;
+using System.IO;
+using System.Text;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ public class ARRY : IffChunk
+ {
+ public int Width;
+ public int Height;
+ public ARRYType Type;
+ public byte[] Data;
+ public byte[] TransposeData
+ {
+ get
+ {
+ var stride = (int)Type;
+ byte[] result = new byte[Data.Length];
+ for (int i = 0; i < Data.Length; i += stride)
+ {
+ var divI = i / stride;
+ var x = divI % Width;
+ var y = divI / Width;
+ int targetIndex = y * stride + x * stride * Width;
+ for (int j = 0; j < stride; j++)
+ {
+ result[targetIndex++] = Data[i + j];
+ }
+ }
+ return result;
+ }
+ }
+
+ public override void Read(IffFile iff, Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ var zero = io.ReadInt32();
+ Width = io.ReadInt32();
+ Height = io.ReadInt32();
+ Type = (ARRYType)io.ReadInt32();
+ var dataByteSize = ByteSize();
+ Data = new byte[Width * Height * dataByteSize];
+ var unknown = io.ReadInt32();
+
+
+ int currentPosition = 0;
+ while (io.HasMore)
+ {
+ switch (Type)
+ {
+ default:
+ //bit format:
+ //1000 yyyy yyxx xxxx
+
+ var flre = io.ReadUInt16();
+ if (flre == 0) break;
+ bool rawFill = (flre & 0x8000) == 0;
+ if (rawFill)
+ {
+ for (int i=0; i Data.Length) return;
+ }
+ if ((flre & 1) == 1) io.ReadByte();
+ continue;
+ }
+
+ var lastPosition = currentPosition;
+ currentPosition += flre & 0x7FFF;
+ currentPosition %= Data.Length; //wrap to data size
+ if (currentPosition == 0) return;
+
+ var pad = io.ReadByte();//ReadElement(io); //pad the previous entries with this data
+ //if ((dataByteSize & 1) == 1)
+ io.ReadByte(); //padded to 16 bits
+
+ while (lastPosition < currentPosition)
+ {
+ Data[lastPosition++] = pad;
+ }
+
+ if (!io.HasMore) return;
+
+ var size = io.ReadInt16();
+ if ((size & 0x8000) != 0)
+ {
+ io.Seek(SeekOrigin.Current, -2);
+ continue;
+ }
+ for (int i = 0; i < size; i++)
+ {
+ Data[currentPosition++] = io.ReadByte();//ReadElement(io);
+ currentPosition %= Data.Length;
+ }
+
+ if ((size & 1) == 1) io.ReadByte(); //16-bit pad
+
+ currentPosition %= Data.Length; //12 bit wrap
+ break;
+ }
+
+ }
+ }
+ }
+
+ public int ByteSize()
+ {
+ return (int)Type;
+ }
+
+
+ public string DebugPrint()
+ {
+ var dataByteSize = ByteSize();
+ var result = new StringBuilder();
+ int index = 0;
+ for (int y = 0; y < Height; y++)
+ {
+ for (int x = 0; x < Width; x++)
+ {
+ var item = Data[index];
+ var str = item.ToString().PadLeft(3, ' ');
+ result.Append((str == " 0") ? " ." : str);
+ index += dataByteSize;
+ }
+ result.AppendLine();
+ }
+ return result.ToString();
+ }
+ }
+
+ public enum ARRYType : int
+ {
+ RLEAlt = 4, //altitude
+ RLEFloor = 1, //floors, "ground", grass, flags, pool "ARRY(9)", water
+ RLEWalls = 8, //walls
+ Objects = 2, //objects "ARRY(3)"
+ }
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/BCON.cs b/server/tso.files/Formats/IFF/Chunks/BCON.cs
new file mode 100755
index 0000000..b698522
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/BCON.cs
@@ -0,0 +1,50 @@
+using System.IO;
+using FSO.Files.Utils;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ ///
+ /// This chunk type holds a number of constants that behavior code can refer to.
+ /// Labels may be provided for them in a TRCN chunk with the same ID.
+ ///
+ public class BCON : IffChunk
+ {
+ public byte Flags;
+ public ushort[] Constants = new ushort[0];
+
+ ///
+ /// Reads a BCON chunk from a stream.
+ ///
+ /// An Iff instance.
+ /// A Stream instance holding a BCON.
+ public override void Read(IffFile iff, Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ var num = io.ReadByte();
+ Flags = io.ReadByte();
+
+ Constants = new ushort[num];
+ for (var i = 0; i < num; i++)
+ {
+ Constants[i] = io.ReadUInt16();
+ }
+ }
+ }
+
+ public override bool Write(IffFile iff, Stream stream)
+ {
+ using (var io = IoWriter.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ io.WriteByte((byte)Constants.Length);
+ io.WriteByte(Flags);
+ foreach (var c in Constants)
+ {
+ io.WriteUInt16(c);
+ }
+
+ return true;
+ }
+ }
+ }
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/BHAV.cs b/server/tso.files/Formats/IFF/Chunks/BHAV.cs
new file mode 100755
index 0000000..d08be65
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/BHAV.cs
@@ -0,0 +1,128 @@
+using System.IO;
+using FSO.Files.Utils;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ ///
+ /// This chunk type holds Behavior code in SimAntics.
+ ///
+ public class BHAV : IffChunk
+ {
+ public TREE RuntimeTree;
+ public BHAVInstruction[] Instructions = new BHAVInstruction[0];
+ public byte Type;
+ public byte Args;
+ public ushort Locals;
+ public ushort Flags;
+
+ public uint RuntimeVer;
+
+ ///
+ /// Reads a BHAV from a stream.
+ ///
+ /// Iff instance.
+ /// A Stream instance holding a BHAV chunk.
+ public override void Read(IffFile iff, System.IO.Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ var version = io.ReadUInt16();
+ uint count = 0;
+
+ if (version == 0x8000)
+ {
+ count = io.ReadUInt16();
+ io.Skip(8);
+ }
+ else if (version == 0x8001)
+ {
+ count = io.ReadUInt16();
+ var unknown = io.ReadBytes(8);
+ }
+ else if (version == 0x8002)
+ {
+ count = io.ReadUInt16();
+ this.Type = io.ReadByte();
+ this.Args = io.ReadByte();
+ this.Locals = io.ReadUInt16();
+ this.Flags = io.ReadUInt16();
+ io.Skip(2);
+ }
+ else if (version == 0x8003)
+ {
+ this.Type = io.ReadByte();
+ this.Args = io.ReadByte();
+ this.Locals = io.ReadByte();
+ io.Skip(2);
+ this.Flags = io.ReadUInt16();
+ count = io.ReadUInt32();
+ }
+
+ Instructions = new BHAVInstruction[count];
+ for (var i = 0; i < count; i++)
+ {
+ var instruction = new BHAVInstruction();
+ instruction.Opcode = io.ReadUInt16();
+ instruction.TruePointer = io.ReadByte();
+ instruction.FalsePointer = io.ReadByte();
+ instruction.Operand = io.ReadBytes(8);
+ Instructions[i] = instruction;
+ }
+ }
+ }
+
+ public override bool Write(IffFile iff, Stream stream)
+ {
+ using (var io = IoWriter.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ if (IffFile.TargetTS1)
+ { //version 0x8002
+ io.WriteUInt16(0x8002);
+ io.WriteUInt16((ushort)Instructions.Length);
+ io.WriteByte(Type);
+ io.WriteByte(Args);
+ io.WriteUInt16(Locals);
+ io.WriteUInt16(Flags);
+ io.WriteBytes(new byte[] { 0, 0 });
+
+ foreach (var inst in Instructions)
+ {
+ io.WriteUInt16(inst.Opcode);
+ io.WriteByte(inst.TruePointer);
+ io.WriteByte(inst.FalsePointer);
+ io.WriteBytes(inst.Operand);
+ }
+ }
+ else
+ {
+ io.WriteUInt16(0x8003);
+ io.WriteByte(Type);
+ io.WriteByte(Args);
+ io.WriteByte((byte)Locals);
+ io.WriteBytes(new byte[] { 0, 0 });
+ io.WriteUInt16(Flags);
+ io.WriteUInt32((ushort)Instructions.Length);
+
+ foreach (var inst in Instructions)
+ {
+ io.WriteUInt16(inst.Opcode);
+ io.WriteByte(inst.TruePointer);
+ io.WriteByte(inst.FalsePointer);
+ io.WriteBytes(inst.Operand);
+ }
+ }
+ }
+ return true;
+ }
+
+ }
+
+ public class BHAVInstruction
+ {
+ public ushort Opcode;
+ public byte TruePointer;
+ public byte FalsePointer;
+ public byte[] Operand;
+ public bool Breakpoint; //only used at runtime
+ }
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/BMP.cs b/server/tso.files/Formats/IFF/Chunks/BMP.cs
new file mode 100755
index 0000000..f9b4826
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/BMP.cs
@@ -0,0 +1,39 @@
+using System.IO;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ ///
+ /// This chunk type holds an image in BMP format.
+ ///
+ public class BMP : IffChunk
+ {
+ public byte[] data;
+
+ ///
+ /// Reads a BMP chunk from a stream.
+ ///
+ /// An Iff instance.
+ /// A Stream object holding a BMP chunk.
+ public override void Read(IffFile iff, Stream stream)
+ {
+ data = new byte[stream.Length];
+ stream.Read(data, 0, (int)stream.Length);
+ }
+
+ public Texture2D GetTexture(GraphicsDevice device)
+ {
+ var tex = ImageLoader.FromStream(device, new MemoryStream(data));
+ return tex;
+ //return Texture2D.FromStream(device, new MemoryStream(data));
+ }
+
+ public override bool Write(IffFile iff, Stream stream)
+ {
+ stream.Write(data, 0, data.Length);
+ return true;
+ }
+ }
+
+
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/CARR.cs b/server/tso.files/Formats/IFF/Chunks/CARR.cs
new file mode 100755
index 0000000..7df45ef
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/CARR.cs
@@ -0,0 +1,98 @@
+using FSO.Files.Utils;
+using System;
+using System.IO;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ public class CARR : IffChunk
+ {
+ public string Name;
+ public JobLevel[] JobLevels;
+
+ public override void Read(IffFile iff, Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ io.ReadUInt32(); //pad
+ var version = io.ReadUInt32();
+
+ var MjbO = io.ReadUInt32();
+
+ var compressionCode = io.ReadByte();
+ if (compressionCode != 1) throw new Exception("hey what!!");
+
+ Name = io.ReadNullTerminatedString();
+ if (Name.Length % 2 == 1) io.ReadByte();
+ var iop = new IffFieldEncode(io);
+
+
+ var numLevels = iop.ReadInt32();
+
+ JobLevels = new JobLevel[numLevels];
+ for (int i=0; i
+ /// Catalog text strings; equivalent in format to STR#.
+ ///
+ public class CTSS : STR
+ {
+ }
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/DGRP.cs b/server/tso.files/Formats/IFF/Chunks/DGRP.cs
new file mode 100755
index 0000000..fcc35da
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/DGRP.cs
@@ -0,0 +1,324 @@
+using System;
+using System.IO;
+using FSO.Files.Utils;
+using Microsoft.Xna.Framework;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ ///
+ /// This chunk type collects SPR# and SPR2 resources into a "drawing group" which
+ /// can be used to display one tile of an object from all directions and zoom levels.
+ /// Objects which span across multiple tiles have a separate DGRP chunk for each tile.
+ /// A DGRP chunk always consists of 12 images (one for every direction/zoom level combination),
+ /// which in turn contain info about one or more sprites.
+ ///
+ public class DGRP : IffChunk
+ {
+ public DGRPImage[] Images { get; set; }
+
+ ///
+ /// Gets a DGRPImage instance from this DGRP instance.
+ ///
+ /// The direction the DGRP is facing.
+ /// Zoom level DGRP is drawn at.
+ /// Current rotation of world.
+ /// A DGRPImage instance.
+ public DGRPImage GetImage(uint direction, uint zoom, uint worldRotation){
+
+ uint rotatedDirection = 0;
+
+ /**LeftFront = 0x10,
+ LeftBack = 0x40,
+ RightFront = 0x04,
+ RightBack = 0x01**/
+ int rotateBits = (int)direction << ((int)worldRotation * 2);
+ rotatedDirection = (uint)((rotateBits & 255) | (rotateBits >> 8));
+
+ foreach(DGRPImage image in Images)
+ {
+ if (image.Direction == rotatedDirection && image.Zoom == zoom)
+ {
+ return image;
+ }
+ }
+ return null;
+ }
+
+ ///
+ /// Reads a DGRP from a stream instance.
+ ///
+ /// An Iff instance.
+ /// A Stream instance holding a DGRP chunk.
+ public override void Read(IffFile iff, Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ var version = io.ReadUInt16();
+ uint imageCount = version < 20003 ? io.ReadUInt16() : io.ReadUInt32();
+ Images = new DGRPImage[imageCount];
+
+ for (var i = 0; i < imageCount; i++)
+ {
+ var image = new DGRPImage(this);
+ image.Read(version, io);
+ Images[i] = image;
+ }
+ }
+ }
+
+ public override bool Write(IffFile iff, Stream stream)
+ {
+ using (var io = IoWriter.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ io.WriteUInt16(20004);
+ io.WriteUInt32((uint)Images.Length);
+
+ foreach (var img in Images)
+ {
+ img.Write(io);
+ }
+ }
+ return true;
+ }
+ }
+
+ ///
+ /// A DGRP is made up of multiple DGRPImages,
+ /// which are made up of multiple DGRPSprites.
+ ///
+ public class DGRPImage
+ {
+ private DGRP Parent;
+ public uint Direction;
+ public uint Zoom;
+ public DGRPSprite[] Sprites;
+
+ public DGRPImage(DGRP parent)
+ {
+ this.Parent = parent;
+ }
+
+ ///
+ /// Reads a DGRPImage from a stream.
+ ///
+ /// An Iff instance.
+ /// A Stream object holding a DGRPImage.
+ public void Read(uint version, IoBuffer io)
+ {
+ uint spriteCount = 0;
+ if (version < 20003){
+ spriteCount = io.ReadUInt16();
+ Direction = io.ReadByte();
+ Zoom = io.ReadByte();
+ }else{
+ Direction = io.ReadUInt32();
+ Zoom = io.ReadUInt32();
+ spriteCount = io.ReadUInt32();
+ }
+
+ this.Sprites = new DGRPSprite[spriteCount];
+ for (var i = 0; i < spriteCount; i++){
+ var sprite = new DGRPSprite(Parent);
+ sprite.Read(version, io);
+ this.Sprites[i] = sprite;
+ }
+ }
+
+ public void Write(IoWriter io)
+ {
+ io.WriteUInt32(Direction);
+ io.WriteUInt32(Zoom);
+ io.WriteUInt32((uint)Sprites.Length);
+ foreach (var spr in Sprites)
+ {
+ spr.Write(io);
+ }
+ }
+ }
+
+ [Flags]
+ public enum DGRPSpriteFlags
+ {
+ Flip = 0x1,
+ Unknown = 0x2, //set for end table
+ Luminous = 0x4,
+ Unknown2 = 0x8,
+ Unknown3 = 0x10 //set for end table
+ }
+
+ ///
+ /// Makes up a DGRPImage.
+ ///
+ public class DGRPSprite : ITextureProvider, IWorldTextureProvider
+ {
+ private DGRP Parent;
+ public uint SpriteID;
+ public uint SpriteFrameIndex;
+ public DGRPSpriteFlags Flags;
+
+ public Vector2 SpriteOffset;
+ public Vector3 ObjectOffset;
+
+ public bool Flip {
+ get { return (Flags & DGRPSpriteFlags.Flip) > 0; }
+ set {
+ Flags = Flags & (~DGRPSpriteFlags.Flip);
+ if (value) Flags |= DGRPSpriteFlags.Flip;
+ }
+ }
+
+ public bool Luminous
+ {
+ get { return (Flags & DGRPSpriteFlags.Luminous) > 0; }
+ set
+ {
+ Flags = Flags & (~DGRPSpriteFlags.Luminous);
+ if (value) Flags |= DGRPSpriteFlags.Luminous;
+ }
+ }
+
+ public DGRPSprite(DGRP parent)
+ {
+ this.Parent = parent;
+ }
+
+ ///
+ /// Reads a DGRPSprite from a stream.
+ ///
+ /// An Iff instance.
+ /// A Stream object holding a DGRPSprite.
+ public void Read(uint version, IoBuffer io)
+ {
+ if (version < 20003)
+ {
+ //Unknown ignored "Type" field
+ var type = io.ReadUInt16();
+ SpriteID = io.ReadUInt16();
+ SpriteFrameIndex = io.ReadUInt16();
+
+ var flagsRaw = io.ReadUInt16();
+ Flags = (DGRPSpriteFlags)flagsRaw;
+
+ SpriteOffset.X = io.ReadInt16();
+ SpriteOffset.Y = io.ReadInt16();
+
+ if (version == 20001)
+ {
+ ObjectOffset.Z = io.ReadFloat();
+ }
+ }
+ else
+ {
+ SpriteID = io.ReadUInt32();
+ SpriteFrameIndex = io.ReadUInt32();
+ SpriteOffset.X = io.ReadInt32();
+ SpriteOffset.Y = io.ReadInt32();
+ ObjectOffset.Z = io.ReadFloat();
+ Flags = (DGRPSpriteFlags)io.ReadUInt32();
+ if (version == 20004)
+ {
+ ObjectOffset.X = io.ReadFloat();
+ ObjectOffset.Y = io.ReadFloat();
+ }
+ }
+ }
+
+ public void Write(IoWriter io)
+ {
+ io.WriteUInt32(SpriteID);
+ io.WriteUInt32(SpriteFrameIndex);
+ io.WriteInt32((int)SpriteOffset.X);
+ io.WriteInt32((int)SpriteOffset.Y);
+ io.WriteFloat(ObjectOffset.Z);
+ io.WriteUInt32((uint)Flags);
+ io.WriteFloat(ObjectOffset.X);
+ io.WriteFloat(ObjectOffset.Y);
+ }
+
+ ///
+ /// Gets position of this sprite.
+ ///
+ /// A Vector2 instance holding position of this sprite.
+ public Vector2 GetPosition()
+ {
+ var iff = Parent.ChunkParent;
+ var spr2 = iff.Get((ushort)this.SpriteID);
+ if (spr2 != null)
+ {
+ return spr2.Frames[this.SpriteFrameIndex].Position;
+ }
+ return new Vector2(0, 0);
+ }
+
+ #region ITextureProvider Members
+
+ public Microsoft.Xna.Framework.Graphics.Texture2D GetTexture(Microsoft.Xna.Framework.Graphics.GraphicsDevice device){
+ var iff = Parent.ChunkParent;
+ var spr2 = iff.Get((ushort)this.SpriteID);
+ if (spr2 != null){
+ return spr2.Frames[this.SpriteFrameIndex].GetTexture(device);
+ }
+ var spr1 = iff.Get((ushort)this.SpriteID);
+ if (spr1 != null){
+ return spr1.Frames[(int)this.SpriteFrameIndex].GetTexture(device);
+ }
+ return null;
+ }
+
+ #endregion
+
+ #region IWorldTextureProvider Members
+
+ public byte[] GetDepth()
+ {
+ var iff = Parent.ChunkParent;
+ var spr2 = iff.Get((ushort)this.SpriteID);
+ if (spr2 != null)
+ {
+ spr2.Frames[this.SpriteFrameIndex].DecodeIfRequired(true);
+ var buf = spr2.Frames[this.SpriteFrameIndex].ZBufferData;
+ return buf;
+ }
+ return null;
+ }
+
+ public WorldTexture GetWorldTexture(Microsoft.Xna.Framework.Graphics.GraphicsDevice device)
+ {
+ var iff = Parent.ChunkParent;
+ var spr2 = iff.Get((ushort)this.SpriteID);
+ if (spr2 != null)
+ {
+ return spr2.Frames[this.SpriteFrameIndex].GetWorldTexture(device);
+ }
+ var spr1 = iff.Get((ushort)this.SpriteID);
+ if (spr1 != null)
+ {
+ var result = new WorldTexture();
+ result.Pixel = spr1.Frames[(int)this.SpriteFrameIndex].GetTexture(device);
+ return result;
+ }
+ return null;
+ }
+
+ public Point GetDimensions()
+ {
+ var iff = Parent.ChunkParent;
+ var spr2 = iff.Get((ushort)this.SpriteID);
+ if (spr2 != null)
+ {
+ var frame = spr2.Frames[this.SpriteFrameIndex];
+ return new Point(frame.Width, frame.Height);
+ }
+ var spr1 = iff.Get((ushort)this.SpriteID);
+ if (spr1 != null)
+ {
+ var result = new WorldTexture();
+ var frame = spr1.Frames[(int)this.SpriteFrameIndex];
+ return new Point(frame.Width, frame.Height);
+ }
+ return new Point(1, 1);
+ }
+
+ #endregion
+ }
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/FAMI.cs b/server/tso.files/Formats/IFF/Chunks/FAMI.cs
new file mode 100755
index 0000000..2596dc8
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/FAMI.cs
@@ -0,0 +1,102 @@
+using FSO.Files.Utils;
+using System.IO;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ ///
+ /// This class defines a single family in the neighbourhood, and various properties such as their
+ /// budget and house assignment. These can be modified using GenericTS1Calls, but are mainly
+ /// defined within CAS.
+ ///
+ public class FAMI : IffChunk
+ {
+ public uint Version = 0x9;
+
+ public int HouseNumber;
+ //this is not a typical family number - it is unique between user created families, but -1 for townies.
+ //i believe it is an alternate family UID that basically runs on an auto increment to obtain its value.
+ //(in comparison with the ChunkID as family that is used ingame, which appears to fill spaces as they are left)
+ public int FamilyNumber;
+ public int Budget;
+ public int ValueInArch;
+ public int FamilyFriends;
+ public int Unknown; //19, 17 or 1? could be flags, (1, 16, 2) ... 0 for townies. 24 for CAS created (new 16+8?)
+ //1: in house
+ //2: unknown, but is set sometimes
+ //4: unknown
+ //8: user created?
+ //16: in cas
+
+ public uint[] FamilyGUIDs = new uint[] { };
+
+ public uint[] RuntimeSubset = new uint[] { }; //the members of this family currently active. don't save!
+
+ public void SelectWholeFamily()
+ {
+ RuntimeSubset = FamilyGUIDs;
+ }
+
+ public void SelectOneMember(uint guid)
+ {
+ RuntimeSubset = new uint[] { guid };
+ }
+
+ ///
+ /// Reads a FAMI chunk from a stream.
+ ///
+ /// An Iff instance.
+ /// A Stream object holding a OBJf chunk.
+ public override void Read(IffFile iff, Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ io.ReadUInt32(); //pad
+ Version = io.ReadUInt32(); //0x9 for latest game
+ string magic = io.ReadCString(4); //IMAF
+
+ HouseNumber = io.ReadInt32();
+ FamilyNumber = io.ReadInt32();
+ Budget = io.ReadInt32();
+ ValueInArch = io.ReadInt32();
+ FamilyFriends = io.ReadInt32();
+ Unknown = io.ReadInt32();
+ FamilyGUIDs = new uint[io.ReadInt32()];
+ for (int i=0; i
+ /// Duplicate of STR chunk, instead used for simulator constants.
+ ///
+ public class FCNS : STR
+ {
+ //no difference!
+
+ public override void Read(IffFile iff, Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ var zero = io.ReadInt32();
+ var version = io.ReadInt32(); //2 in tso
+ string magic = io.ReadCString(4); //NSCF
+ var count = io.ReadInt32();
+
+ LanguageSets[0].Strings = new STRItem[count];
+ for (int i=0; i
+ /// Iff chunk wrapper for an FSOM file.
+ ///
+ public class FSOM : IffChunk
+ {
+ private byte[] data;
+ private DGRP3DMesh Cached;
+
+ ///
+ /// Reads a BMP chunk from a stream.
+ ///
+ /// An Iff instance.
+ /// A Stream object holding a BMP chunk.
+ public override void Read(IffFile iff, Stream stream)
+ {
+ data = new byte[stream.Length];
+ stream.Read(data, 0, (int)stream.Length);
+ }
+
+ public override bool Write(IffFile iff, Stream stream)
+ {
+ if (data == null)
+ {
+ using (var cstream = new GZipStream(stream, CompressionMode.Compress))
+ Cached.Save(cstream);
+ } else
+ {
+ stream.Write(data, 0, data.Length);
+ }
+ return true;
+ }
+
+ public DGRP3DMesh Get(DGRP dgrp, GraphicsDevice device)
+ {
+ if (Cached == null) {
+ using (var stream = new MemoryStream(data)) {
+ Cached = new DGRP3DMesh(dgrp, stream, device);
+ }
+ }
+ data = null;
+ return Cached;
+ }
+
+ public void SetMesh(DGRP3DMesh mesh)
+ {
+ Cached = mesh;
+ data = null;
+ }
+ }
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/FSOR.cs b/server/tso.files/Formats/IFF/Chunks/FSOR.cs
new file mode 100755
index 0000000..9a8ce31
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/FSOR.cs
@@ -0,0 +1,35 @@
+using FSO.Files.RC;
+using FSO.Files.Utils;
+using System.IO;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ ///
+ /// Metadata for an object's mesh reconstruction. Currently only supports file-wise parameters.
+ ///
+ public class FSOR : IffChunk
+ {
+ public static int CURRENT_VERSION = 1;
+ public int Version = CURRENT_VERSION;
+ public DGRPRCParams Params = new DGRPRCParams();
+
+ public override void Read(IffFile iff, Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ Version = io.ReadInt32();
+ Params = new DGRPRCParams(io, Version);
+ }
+ }
+
+ public override bool Write(IffFile iff, Stream stream)
+ {
+ using (var io = IoWriter.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ io.WriteInt32(Version);
+ Params.Save(io);
+ }
+ return true;
+ }
+ }
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/FSOV.cs b/server/tso.files/Formats/IFF/Chunks/FSOV.cs
new file mode 100755
index 0000000..b330357
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/FSOV.cs
@@ -0,0 +1,36 @@
+using FSO.Files.Utils;
+using System.IO;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ ///
+ /// A simple container for FSOV data within an iff. If this exists, normal TS1 iff loading is subverted.
+ ///
+ public class FSOV : IffChunk
+ {
+ public static int CURRENT_VERSION = 1;
+ public int Version = CURRENT_VERSION;
+ public byte[] Data;
+
+ public override void Read(IffFile iff, Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ Version = io.ReadInt32();
+ var length = io.ReadInt32();
+ Data = io.ReadBytes(length);
+ }
+ }
+
+ public override bool Write(IffFile iff, Stream stream)
+ {
+ using (var io = IoWriter.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ io.WriteInt32(Version);
+ io.WriteInt32(Data.Length);
+ io.WriteBytes(Data);
+ }
+ return true;
+ }
+ }
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/FWAV.cs b/server/tso.files/Formats/IFF/Chunks/FWAV.cs
new file mode 100755
index 0000000..4d3e91e
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/FWAV.cs
@@ -0,0 +1,35 @@
+using System.IO;
+using FSO.Files.Utils;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ ///
+ /// This chunk type holds the name of a sound event that this object uses.
+ ///
+ public class FWAV : IffChunk
+ {
+ public string Name;
+
+ ///
+ /// Reads a FWAV chunk from a stream.
+ ///
+ /// An Iff instance.
+ /// A Stream object holding a FWAV chunk.
+ public override void Read(IffFile iff, Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ Name = io.ReadNullTerminatedString();
+ }
+ }
+
+ public override bool Write(IffFile iff, Stream stream)
+ {
+ using (var io = IoWriter.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ io.WriteNullTerminatedString(Name);
+ }
+ return true;
+ }
+ }
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/GLOB.cs b/server/tso.files/Formats/IFF/Chunks/GLOB.cs
new file mode 100755
index 0000000..b81bfbd
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/GLOB.cs
@@ -0,0 +1,52 @@
+using System.Text;
+using System.IO;
+using FSO.Files.Utils;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ ///
+ /// This chunk type holds the filename of a semi-global iff file used by this object.
+ ///
+ public class GLOB : IffChunk
+ {
+ public string Name;
+
+ ///
+ /// Reads a GLOB chunk from a stream.
+ ///
+ /// An Iff instance.
+ /// A Stream object holding a GLOB chunk.
+ public override void Read(IffFile iff, Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ StringBuilder temp = new StringBuilder();
+ var num = io.ReadByte();
+ if (num < 48)
+ { //less than smallest ASCII value for valid filename character, so assume this is a pascal string
+ temp.Append(io.ReadCString(num));
+ }
+ else
+ { //we're actually a null terminated string!
+ temp.Append((char)num);
+ while (stream.Position < stream.Length)
+ {
+ char read = (char)io.ReadByte();
+ if (read == 0) break;
+ else temp.Append(read);
+ }
+ }
+ Name = temp.ToString();
+ }
+ }
+
+ public override bool Write(IffFile iff, Stream stream)
+ {
+ using (var io = IoWriter.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ io.WriteNullTerminatedString(Name);
+ }
+ return true;
+ }
+ }
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/HOUS.cs b/server/tso.files/Formats/IFF/Chunks/HOUS.cs
new file mode 100755
index 0000000..d664edd
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/HOUS.cs
@@ -0,0 +1,38 @@
+using FSO.Files.Utils;
+using System.IO;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ public class HOUS : IffChunk
+ {
+ public int Version;
+ public int UnknownFlag;
+ public int UnknownOne;
+ public int UnknownNumber;
+ public int UnknownNegative;
+ public short CameraDir;
+ public short UnknownOne2;
+ public short UnknownFlag2;
+ public uint GUID;
+ public string RoofName;
+
+ public override void Read(IffFile iff, Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ var zero = io.ReadInt32();
+ Version = io.ReadInt32();
+ var suoh = io.ReadCString(4);
+ UnknownFlag = io.ReadInt32();
+ UnknownOne = io.ReadInt32();
+ UnknownNumber = io.ReadInt32();
+ UnknownNegative = io.ReadInt32();
+ CameraDir = io.ReadInt16();
+ UnknownOne2 = io.ReadInt16();
+ UnknownFlag2 = io.ReadInt16();
+ GUID = io.ReadUInt32();
+ RoofName = io.ReadNullTerminatedString();
+ }
+ }
+ }
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/ISPR.cs b/server/tso.files/Formats/IFF/Chunks/ISPR.cs
new file mode 100755
index 0000000..f8a4300
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/ISPR.cs
@@ -0,0 +1,18 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
+ * If a copy of the MPL was not distributed with this file, You can obtain one at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace tso.files.formats.iff.chunks
+{
+ public interface ISPR
+ {
+
+ }
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/IffFieldEncode.cs b/server/tso.files/Formats/IFF/Chunks/IffFieldEncode.cs
new file mode 100755
index 0000000..e6d6eda
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/IffFieldEncode.cs
@@ -0,0 +1,152 @@
+using FSO.Files.Utils;
+using System;
+using System.IO;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ ///
+ /// Used to read values from field encoded stream.
+ ///
+ public class IffFieldEncode : IOProxy
+ {
+ private byte bitPos = 0;
+ private byte curByte = 0;
+ private bool odd = false;
+ public byte[] widths = { 5, 8, 13, 16 };
+ public byte[] widths2 = { 6, 11, 21, 32 };
+ public bool StreamEnd;
+
+ public void setBytePos(int n)
+ {
+ io.Seek(SeekOrigin.Begin, n);
+ curByte = io.ReadByte();
+ bitPos = 0;
+ }
+
+ public override ushort ReadUInt16()
+ {
+ return (ushort)ReadField(false);
+ }
+
+ public override short ReadInt16()
+ {
+ return (short)ReadField(false);
+ }
+
+ public override int ReadInt32()
+ {
+ return (int)ReadField(true);
+ }
+
+ public override uint ReadUInt32()
+ {
+ return (uint)ReadField(true);
+ }
+
+ public override float ReadFloat()
+ {
+ return (float)ReadField(true);
+ //this is incredibly wrong
+ }
+
+ private long ReadField(bool big)
+ {
+ if (ReadBit() == 0) return 0;
+
+ uint code = ReadBits(2);
+ byte width = (big) ? widths2[code] : widths[code];
+ long value = ReadBits(width);
+ value |= -(value & (1 << (width - 1)));
+
+ return value;
+ }
+
+ public Tuple DebugReadField(bool big)
+ {
+ if (ReadBit() == 0) return new Tuple(0, 0);
+
+ uint code = ReadBits(2);
+ byte width = (big) ? widths2[code] : widths[code];
+ long value = ReadBits(width);
+ value |= -(value & (1 << (width - 1)));
+
+ return new Tuple(value, width);
+ }
+
+ public Tuple MarkStream()
+ {
+ return new Tuple(bitPos, curByte, odd, io.Position);
+ }
+
+ public void RevertToMark(Tuple mark)
+ {
+ StreamEnd = false;
+ bitPos = mark.Item1;
+ curByte = mark.Item2;
+ odd = mark.Item3;
+ io.Seek(SeekOrigin.Begin, mark.Item4);
+ }
+
+ public uint ReadBits(int n)
+ {
+ uint total = 0;
+ for (int i = 0; i < n; i++)
+ {
+ total += (uint)(ReadBit() << ((n - i) - 1));
+ }
+ return total;
+ }
+
+ private byte ReadBit()
+ {
+ byte result = (byte)((curByte & (1 << (7 - bitPos))) >> (7 - bitPos));
+ if (++bitPos > 7)
+ {
+ bitPos = 0;
+ try
+ {
+ curByte = io.ReadByte();
+ odd = !odd;
+ }
+ catch (Exception)
+ {
+ curByte = 0; //no more data, read 0
+ odd = !odd;
+ StreamEnd = true;
+ }
+ }
+ return result;
+ }
+
+ public string ReadString(bool nextField)
+ {
+ if (bitPos == 0)
+ {
+ io.Seek(SeekOrigin.Current, -1);
+ odd = !odd;
+ }
+ var str = io.ReadNullTerminatedString();
+ if ((str.Length % 2 == 0) == !odd) io.ReadByte(); //2 byte pad
+
+ bitPos = 8;
+ if (nextField && io.HasMore)
+ {
+ curByte = io.ReadByte();
+ odd = true;
+ bitPos = 0;
+ } else
+ {
+ odd = false;
+ }
+
+ return str;
+ }
+
+ public IffFieldEncode(IoBuffer io) : base(io)
+ {
+ curByte = io.ReadByte();
+ odd = !odd;
+ bitPos = 0;
+ }
+ }
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/MTEX.cs b/server/tso.files/Formats/IFF/Chunks/MTEX.cs
new file mode 100755
index 0000000..513f095
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/MTEX.cs
@@ -0,0 +1,59 @@
+using FSO.Common;
+using FSO.Common.Utils;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using System.IO;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ ///
+ /// Texture for a 3D Mesh. Can be jpg, png or bmp.
+ ///
+ public class MTEX : IffChunk
+ {
+ private byte[] data;
+ private Texture2D Cached;
+
+ ///
+ /// Reads a BMP chunk from a stream.
+ ///
+ /// An Iff instance.
+ /// A Stream object holding a BMP chunk.
+ public override void Read(IffFile iff, Stream stream)
+ {
+ data = new byte[stream.Length];
+ stream.Read(data, 0, (int)stream.Length);
+ }
+
+ public override bool Write(IffFile iff, Stream stream)
+ {
+ stream.Write(data, 0, data.Length);
+ return true;
+ }
+
+ public Texture2D GetTexture(GraphicsDevice device)
+ {
+ if (Cached == null)
+ {
+ Cached = ImageLoader.FromStream(device, new MemoryStream(data));
+ if (FSOEnvironment.EnableNPOTMip)
+ {
+ var data = new Color[Cached.Width * Cached.Height];
+ Cached.GetData(data);
+ var n = new Texture2D(device, Cached.Width, Cached.Height, true, SurfaceFormat.Color);
+ TextureUtils.UploadWithMips(n, device, data);
+ Cached.Dispose();
+ Cached = n;
+ }
+ }
+ if (!IffFile.RETAIN_CHUNK_DATA) data = null;
+ return Cached;
+ }
+
+ public void SetData(byte[] data)
+ {
+ this.data = data;
+ Cached = null;
+ }
+ }
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/NBRS.cs b/server/tso.files/Formats/IFF/Chunks/NBRS.cs
new file mode 100755
index 0000000..bb1e928
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/NBRS.cs
@@ -0,0 +1,224 @@
+using FSO.Files.Utils;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ ///
+ /// This chunk defines all neighbours in a neighbourhood.
+ /// A neighbour is a specific version of a sim object with associated relationships and person data. (skills, person type)
+ ///
+ /// These can be read within SimAntics without the avatar actually present. This is used to find and spawn suitable sims on
+ /// ped portals as visitors, and also drive phone calls to other sims in the neighbourhood.
+ /// When neighbours are spawned, they assume the attributes saved here. A TS1 global call allows the game to save these attributes.
+ ///
+ public class NBRS : IffChunk
+ {
+ public List Entries = new List();
+ public Dictionary NeighbourByID = new Dictionary();
+ public Dictionary DefaultNeighbourByGUID = new Dictionary();
+
+ public uint Version;
+
+ ///
+ /// Reads a NBRS chunk from a stream.
+ ///
+ /// An Iff instance.
+ /// A Stream object holding a NBRS chunk.
+ public override void Read(IffFile iff, Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ io.ReadUInt32(); //pad
+ Version = io.ReadUInt32(); //0x49 for latest game
+ string magic = io.ReadCString(4); //SRBN
+ var count = io.ReadUInt32();
+
+ for (int i=0; i 0)
+ {
+ NeighbourByID.Add(neigh.NeighbourID, neigh);
+ DefaultNeighbourByGUID[neigh.GUID] = neigh.NeighbourID;
+ }
+ }
+ }
+ Entries = Entries.OrderBy(x => x.NeighbourID).ToList();
+ foreach (var entry in Entries)
+ entry.RuntimeIndex = Entries.IndexOf(entry);
+ }
+
+ ///
+ /// Writes a NBRS chunk to a stream.
+ ///
+ /// An Iff instance.
+ /// A destination stream.
+ public override bool Write(IffFile iff, Stream stream)
+ {
+ using (var io = IoWriter.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ io.WriteUInt32(0);
+ io.WriteUInt32(0x49);
+ io.WriteCString("SRBN", 4);
+ io.WriteInt32(Entries.Count);
+ foreach (var n in NeighbourByID.Values)
+ {
+ n.Save(io);
+ }
+ }
+ return true;
+ }
+
+ public void AddNeighbor(Neighbour nb) {
+ Entries.Add(nb);
+ Entries = Entries.OrderBy(x => x.NeighbourID).ToList();
+ foreach (var entry in Entries)
+ entry.RuntimeIndex = Entries.IndexOf(entry);
+
+ NeighbourByID.Add(nb.NeighbourID, nb);
+ DefaultNeighbourByGUID[nb.GUID] = nb.NeighbourID;
+ }
+
+ public short GetFreeID()
+ {
+ //find the lowest id that is free
+ short newID = 1;
+ for (int i = 0; i < Entries.Count; i++)
+ {
+ if (Entries[i].NeighbourID == newID) newID++;
+ else if (Entries[i].NeighbourID < newID) continue;
+ else break;
+ }
+ return newID;
+ }
+ }
+
+ public class Neighbour
+ {
+ public int Unknown1 = 1; //1
+ public int Version = 0xA; //0x4, 0xA
+ //if 0xA, unknown3 follows
+ //0x4 indicates person data size of 0xa0.. (160 bytes, or 80 entries)
+ public int Unknown3 = 9; //9
+ public string Name;
+ public int MysteryZero = 0;
+ public int PersonMode; //0/5/9
+ public short[] PersonData; //can be null
+
+ public short NeighbourID;
+ public uint GUID;
+ public int UnknownNegOne = -1; //negative 1 usually
+
+ public Dictionary> Relationships;
+
+ public int RuntimeIndex; //used for fast continuation of Set to Next
+
+ public Neighbour() { }
+
+ public Neighbour(IoBuffer io)
+ {
+ Unknown1 = io.ReadInt32();
+ if (Unknown1 != 1) { return; }
+ Version = io.ReadInt32();
+ if (Version == 0xA)
+ {
+ //TODO: what version does this truly start?
+ Unknown3 = io.ReadInt32();
+ if (Unknown3 != 9) { }
+ }
+ Name = io.ReadNullTerminatedString();
+ if (Name.Length % 2 == 0) io.ReadByte();
+ MysteryZero = io.ReadInt32();
+ if (MysteryZero != 0) { }
+ PersonMode = io.ReadInt32();
+ if (PersonMode > 0)
+ {
+ var size = (Version == 0x4) ? 0xa0 : 0x200;
+ PersonData = new short[88];
+ int pdi = 0;
+ for (int i=0; i= 88)
+ {
+ io.ReadBytes(size - i);
+ break;
+ }
+ PersonData[pdi++] = io.ReadInt16();
+ }
+ }
+
+ NeighbourID = io.ReadInt16();
+ GUID = io.ReadUInt32();
+ UnknownNegOne = io.ReadInt32();
+ if (UnknownNegOne != -1) { }
+
+ var entries = io.ReadInt32();
+ Relationships = new Dictionary>();
+ for (int i=0; i();
+ var valueCount = io.ReadInt32();
+ for (int j=0; j 0)
+ {
+ var size = (Version == 0x4) ? 0xa0 : 0x200;
+ int pdi = 0;
+ for (int i = 0; i < size; i += 2)
+ {
+ if (pdi >= 88)
+ {
+ io.WriteInt16(0);
+ }
+ else
+ {
+ io.WriteInt16(PersonData[pdi++]);
+ }
+ }
+ }
+
+ io.WriteInt16(NeighbourID);
+ io.WriteUInt32(GUID);
+ io.WriteInt32(UnknownNegOne);
+
+ io.WriteInt32(Relationships.Count);
+ foreach (var rel in Relationships)
+ {
+ io.WriteInt32(1); //keycount (1)
+ io.WriteInt32(rel.Key);
+ io.WriteInt32(rel.Value.Count);
+ foreach (var val in rel.Value)
+ {
+ io.WriteInt32(val);
+ }
+ }
+ }
+ }
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/NGBH.cs b/server/tso.files/Formats/IFF/Chunks/NGBH.cs
new file mode 100755
index 0000000..528704e
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/NGBH.cs
@@ -0,0 +1,117 @@
+using FSO.Files.Utils;
+using System.Collections.Generic;
+using System.IO;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ ///
+ /// This chunk type contains general neighbourhood data within a neighbourhood.iff file.
+ /// The only thing this was used for initially was tracking the tutorial.
+ ///
+ /// As of hot date, it also includes inventory data, which was added as something of an afterthought.
+ ///
+ public class NGBH : IffChunk
+ {
+ public short[] NeighborhoodData = new short[16];
+ public Dictionary> InventoryByID = new Dictionary>();
+
+ public uint Version;
+
+ ///
+ /// Reads a NGBH chunk from a stream.
+ ///
+ /// An Iff instance.
+ /// A Stream object holding a OBJf chunk.
+ public override void Read(IffFile iff, Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ io.ReadUInt32(); //pad
+ Version = io.ReadUInt32(); //0x49 for latest game
+ string magic = io.ReadCString(4); //HBGN
+
+ for (int i=0; i<16; i++)
+ {
+ NeighborhoodData[i] = io.ReadInt16();
+ }
+
+ if (!io.HasMore) return; //no inventory present (yet)
+ var count = io.ReadInt32();
+ for (int i = 0; i < count; i++)
+ {
+ if (io.ReadInt32() != 1) { }
+ var neighID = io.ReadInt16();
+ var inventoryCount = io.ReadInt32();
+ var inventory = new List();
+
+ for (int j=0; j
+ /// Type of OBJD.
+ ///
+ [Serializable()]
+ public enum OBJDType
+ {
+ Unknown = 0,
+ //Character or NPC
+ Person = 2,
+ //Buyable objects
+ Normal = 4,
+ //Roaches, Stoves2, TrClownGen, AnimTester, HelpSystem, JobFinder, NPCController, Stoves,
+ //Tutorial, VisitGenerator, phonecall, unsnacker, CCPhonePlugin, EStove
+ SimType = 7,
+ //Stairs, doors, pool diving board & ladder, windows(?)
+ Portal = 8,
+ Cursor = 9,
+ PrizeToken = 10,
+ //Temporary location for drop or shoo
+ Internal = 11,
+ //these are mysteriously set as global sometimes.
+ GiftToken = 12,
+ Food = 34
+ }
+
+ ///
+ /// This is an object definition, the main chunk for an object and the first loaded by the VM.
+ /// There can be multiple master OBJDs in an IFF, meaning that one IFF file can define multiple objects.
+ ///
+ public class OBJD : IffChunk
+ {
+ public uint Version;
+
+ public static string[] VERSION_142_Fields = new string[]
+ {
+ "StackSize",
+ "BaseGraphicID",
+ "NumGraphics",
+ "BHAV_MainID",
+ "BHAV_GardeningID",
+ "TreeTableID",
+ "InteractionGroupID",
+ "ObjectType",
+ "MasterID",
+ "SubIndex",
+ "BHAV_WashHandsID",
+ "AnimationTableID",
+ "GUID1",
+ "GUID2",
+ "Disabled",
+ "BHAV_Portal",
+ "Price",
+ "BodyStringID",
+ "SlotID",
+ "BHAV_AllowIntersectionID",
+ "UsesFnTable",
+ "BitField1",
+ "BHAV_PrepareFoodID",
+ "BHAV_CookFoodID",
+ "BHAV_PlaceSurfaceID",
+ "BHAV_DisposeID",
+ "BHAV_EatID",
+ "BHAV_PickupFromSlotID",
+ "BHAV_WashDishID",
+ "BHAV_EatSurfaceID",
+ "BHAV_SitID",
+ "BHAV_StandID",
+
+ "SalePrice",
+ "InitialDepreciation",
+ "DailyDepreciation",
+ "SelfDepreciating",
+ "DepreciationLimit",
+ "RoomFlags",
+ "FunctionFlags",
+ "CatalogStringsID",
+
+ "Global",
+ "BHAV_Init",
+ "BHAV_Place",
+ "BHAV_UserPickup",
+ "WallStyle",
+ "BHAV_Load",
+ "BHAV_UserPlace",
+ "ObjectVersion",
+ "BHAV_RoomChange",
+ "MotiveEffectsID",
+ "BHAV_Cleanup",
+ "BHAV_LevelInfo",
+ "CatalogID",
+ "BHAV_ServingSurface",
+ "LevelOffset",
+ "Shadow",
+ "NumAttributes",
+
+ "BHAV_Clean",
+ "BHAV_QueueSkipped",
+ "FrontDirection",
+ "BHAV_WallAdjacencyChanged",
+ "MyLeadObject",
+ "DynamicSpriteBaseId",
+ "NumDynamicSprites",
+
+ "ChairEntryFlags",
+ "TileWidth",
+ "LotCategories",
+ "BuildModeType",
+ "OriginalGUID1",
+ "OriginalGUID2",
+ "SuitGUID1",
+ "SuitGUID2",
+ "BHAV_Pickup",
+ "ThumbnailGraphic",
+ "ShadowFlags",
+ "FootprintMask",
+ "BHAV_DynamicMultiTileUpdate",
+ "ShadowBrightness",
+ "BHAV_Repair",
+
+ "WallStyleSpriteID",
+ "RatingHunger",
+ "RatingComfort",
+ "RatingHygiene",
+ "RatingBladder",
+ "RatingEnergy",
+ "RatingFun",
+ "RatingRoom",
+ "RatingSkillFlags",
+
+ "NumTypeAttributes",
+ "MiscFlags",
+ "TypeAttrGUID1",
+ "TypeAttrGUID2"
+ };
+
+ public static string[] VERSION_138b_Extra_Fields = new string[]
+ {
+ "FunctionSubsort",
+ "DTSubsort",
+ "KeepBuying",
+ "VacationSubsort",
+ "ResetLotAction",
+ "CommunitySubsort",
+ "DreamFlags",
+ "RenderFlags",
+ "VitaboyFlags",
+ "STSubsort",
+ "MTSubsort"
+ };
+
+ public ushort GUID1
+ {
+ get { return (ushort)(GUID); }
+ set { GUID = (GUID & 0xFFFF0000) | value; }
+ }
+ public ushort GUID2
+ {
+ get { return (ushort)(GUID>>16); }
+ set { GUID = (GUID & 0x0000FFFF) | ((uint)value<<16); }
+ }
+
+ public ushort StackSize { get; set; }
+ public ushort BaseGraphicID { get; set; }
+ public ushort NumGraphics { get; set; }
+ public ushort TreeTableID { get; set; }
+ public short InteractionGroupID { get; set; }
+ public OBJDType ObjectType { get; set; }
+ public ushort MasterID { get; set; }
+ public short SubIndex { get; set; }
+ public ushort AnimationTableID { get; set; }
+ public uint GUID { get; set; }
+ public ushort Disabled { get; set; }
+ public ushort BHAV_Portal { get; set; }
+ public ushort Price { get; set; }
+ public ushort BodyStringID { get; set; }
+ public ushort SlotID { get; set; }
+ public ushort SalePrice { get; set; }
+ public ushort InitialDepreciation { get; set; }
+ public ushort DailyDepreciation { get; set; }
+ public ushort SelfDepreciating { get; set; }
+ public ushort DepreciationLimit { get; set; }
+ public ushort RoomFlags { get; set; }
+ public ushort FunctionFlags { get; set; }
+ public ushort CatalogStringsID { get; set; }
+
+ public ushort BHAV_MainID { get; set; }
+ public ushort BHAV_GardeningID { get; set; }
+ public ushort BHAV_WashHandsID { get; set; }
+ public ushort BHAV_AllowIntersectionID { get; set; }
+ public ushort UsesFnTable { get; set; }
+ public ushort BitField1 { get; set; }
+
+ public ushort BHAV_PrepareFoodID { get; set; }
+ public ushort BHAV_CookFoodID { get; set; }
+ public ushort BHAV_PlaceSurfaceID { get; set; }
+ public ushort BHAV_DisposeID { get; set; }
+ public ushort BHAV_EatID { get; set; }
+ public ushort BHAV_PickupFromSlotID { get; set; }
+ public ushort BHAV_WashDishID { get; set; }
+ public ushort BHAV_EatSurfaceID { get; set; }
+ public ushort BHAV_SitID { get; set; }
+ public ushort BHAV_StandID { get; set; }
+
+ public ushort Global { get; set; }
+ public ushort BHAV_Init { get; set; }
+ public ushort BHAV_Place { get; set; }
+ public ushort BHAV_UserPickup { get; set; }
+ public ushort WallStyle { get; set; }
+ public ushort BHAV_Load { get; set; }
+ public ushort BHAV_UserPlace { get; set; }
+ public ushort ObjectVersion { get; set; }
+ public ushort BHAV_RoomChange { get; set; }
+ public ushort MotiveEffectsID { get; set; }
+ public ushort BHAV_Cleanup { get; set; }
+ public ushort BHAV_LevelInfo { get; set; }
+ public ushort CatalogID { get; set; }
+
+ public ushort BHAV_ServingSurface { get; set; }
+ public ushort LevelOffset { get; set; }
+ public ushort Shadow { get; set; }
+ public ushort NumAttributes { get; set; }
+
+ public ushort BHAV_Clean { get; set; }
+ public ushort BHAV_QueueSkipped { get; set; }
+ public ushort FrontDirection { get; set; }
+ public ushort BHAV_WallAdjacencyChanged { get; set; }
+ public ushort MyLeadObject { get; set; }
+ public ushort DynamicSpriteBaseId { get; set; }
+ public ushort NumDynamicSprites { get; set; }
+
+ public ushort ChairEntryFlags { get; set; }
+ public ushort TileWidth { get; set; }
+ public ushort LotCategories { get; set; }
+ public ushort BuildModeType { get; set; }
+ public ushort OriginalGUID1 { get; set; }
+ public ushort OriginalGUID2 { get; set; }
+ public ushort SuitGUID1 { get; set; }
+ public ushort SuitGUID2 { get; set; }
+ public ushort BHAV_Pickup { get; set; }
+ public ushort ThumbnailGraphic { get; set; }
+ public ushort ShadowFlags { get; set; }
+ public ushort FootprintMask { get; set; }
+ public ushort BHAV_DynamicMultiTileUpdate { get; set; }
+ public ushort ShadowBrightness { get; set; }
+ public ushort BHAV_Repair { get; set; }
+
+ public ushort WallStyleSpriteID { get; set; }
+ public short RatingHunger { get; set; }
+ public short RatingComfort { get; set; }
+ public short RatingHygiene { get; set; }
+ public short RatingBladder { get; set; }
+ public short RatingEnergy { get; set; }
+ public short RatingFun { get; set; }
+ public short RatingRoom { get; set; }
+ public ushort RatingSkillFlags { get; set; }
+
+ public ushort[] RawData;
+ public ushort NumTypeAttributes { get; set; }
+ public ushort MiscFlags { get; set; }
+ public uint TypeAttrGUID;
+
+ public ushort FunctionSubsort { get; set; }
+ public ushort DTSubsort { get; set; }
+ public ushort KeepBuying { get; set; }
+ public ushort VacationSubsort { get; set; }
+ public ushort ResetLotAction { get; set; }
+ public ushort CommunitySubsort { get; set; }
+ public ushort DreamFlags { get; set; }
+ public ushort RenderFlags { get; set; }
+ public ushort VitaboyFlags { get; set; }
+ public ushort STSubsort { get; set; }
+ public ushort MTSubsort { get; set; }
+
+ public ushort FootprintNorth
+ {
+ get
+ {
+ return (ushort)(FootprintMask & 0xF);
+ }
+ set
+ {
+ FootprintMask &= 0xFFF0;
+ FootprintMask |= (ushort)(value & 0xF);
+ }
+ }
+
+
+ public ushort FootprintEast
+ {
+ get
+ {
+ return (ushort)((FootprintMask >> 4) & 0xF);
+ }
+ set
+ {
+ FootprintMask &= 0xFF0F;
+ FootprintMask |= (ushort)((value & 0xF) << 4);
+ }
+ }
+
+
+ public ushort FootprintSouth
+ {
+ get
+ {
+ return (ushort)((FootprintMask >> 8) & 0xF);
+ }
+ set
+ {
+ FootprintMask &= 0xF0FF;
+ FootprintMask |= (ushort)((value & 0xF) << 8);
+ }
+ }
+
+
+ public ushort FootprintWest
+ {
+ get
+ {
+ return (ushort)((FootprintMask >> 12) & 0xF);
+ }
+ set
+ {
+ FootprintMask &= 0x0FFF;
+ FootprintMask |= (ushort)((value & 0xF) << 12);
+ }
+ }
+
+ public ushort TypeAttrGUID1
+ {
+ get { return (ushort)(TypeAttrGUID); }
+ set { TypeAttrGUID = (TypeAttrGUID & 0xFFFF0000) | value; }
+ }
+ public ushort TypeAttrGUID2
+ {
+ get { return (ushort)(TypeAttrGUID >> 16); }
+ set { TypeAttrGUID = (TypeAttrGUID & 0x0000FFFF) | ((uint)value << 16); }
+ }
+
+ public bool IsMaster
+ {
+ get
+ {
+ return SubIndex == -1;
+ }
+ }
+
+ public bool IsMultiTile
+ {
+ get {
+ return MasterID != 0;
+ }
+ }
+
+ public T GetPropertyByName(string name)
+ {
+ Type me = typeof(OBJD);
+ var prop = me.GetProperty(name);
+ return (T)Convert.ChangeType(prop.GetValue(this, null), typeof(T));
+ }
+
+ public void SetPropertyByName(string name, object value)
+ {
+ Type me = typeof(OBJD);
+ var prop = me.GetProperty(name);
+ try
+ {
+ value = Convert.ChangeType(value, prop.PropertyType);
+ } catch {
+ value = Enum.Parse(prop.PropertyType, value.ToString());
+ }
+ prop.SetValue(this, value, null);
+ }
+
+ public override void Read(IffFile iff, Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ this.Version = io.ReadUInt32();
+
+ /**136 (80 fields)
+ 138a (95 fields) - Used for The Sims 1 base game objects?
+ 138b (108 fields) - Used for The Sims 1 expansion objects?
+ 139 (96 fields)
+ 140 (97 fields)
+ 141 (97 fields)
+ 142 (105 fields)**/
+ var numFields = 80;
+ if (Version == 138)
+ {
+ numFields = 95;
+ }
+ else if (Version == 139)
+ {
+ numFields = 96;
+ }
+ else if (Version == 140)
+ {
+ numFields = 97;
+ }
+ else if (Version == 141)
+ {
+ numFields = 97;
+ }
+ else if (Version == 142)
+ {
+ numFields = 105;
+ }
+
+ numFields -= 2;
+ RawData = new ushort[numFields];
+ io.Mark();
+
+ for (var i = 0; i < numFields; i++)
+ {
+ RawData[i] = io.ReadUInt16();
+ }
+
+ io.SeekFromMark(0);
+
+ this.StackSize = io.ReadUInt16();
+ this.BaseGraphicID = io.ReadUInt16();
+ this.NumGraphics = io.ReadUInt16();
+ this.BHAV_MainID = io.ReadUInt16();
+ this.BHAV_GardeningID = io.ReadUInt16();
+ this.TreeTableID = io.ReadUInt16();
+ this.InteractionGroupID = io.ReadInt16();
+ this.ObjectType = (OBJDType)io.ReadUInt16();
+ this.MasterID = io.ReadUInt16();
+ this.SubIndex = io.ReadInt16();
+ this.BHAV_WashHandsID = io.ReadUInt16();
+ this.AnimationTableID = io.ReadUInt16();
+ this.GUID = io.ReadUInt32();
+ this.Disabled = io.ReadUInt16();
+ this.BHAV_Portal = io.ReadUInt16();
+ this.Price = io.ReadUInt16();
+ this.BodyStringID = io.ReadUInt16();
+ this.SlotID = io.ReadUInt16();
+ this.BHAV_AllowIntersectionID = io.ReadUInt16();
+ this.UsesFnTable = io.ReadUInt16();
+ this.BitField1 = io.ReadUInt16();
+ this.BHAV_PrepareFoodID = io.ReadUInt16();
+ this.BHAV_CookFoodID = io.ReadUInt16();
+ this.BHAV_PlaceSurfaceID = io.ReadUInt16();
+ this.BHAV_DisposeID = io.ReadUInt16();
+ this.BHAV_EatID = io.ReadUInt16();
+ this.BHAV_PickupFromSlotID = io.ReadUInt16();
+ this.BHAV_WashDishID = io.ReadUInt16();
+ this.BHAV_EatSurfaceID = io.ReadUInt16();
+ this.BHAV_SitID = io.ReadUInt16();
+ this.BHAV_StandID = io.ReadUInt16();
+
+ this.SalePrice = io.ReadUInt16();
+ this.InitialDepreciation = io.ReadUInt16();
+ this.DailyDepreciation = io.ReadUInt16();
+ this.SelfDepreciating = io.ReadUInt16();
+ this.DepreciationLimit = io.ReadUInt16();
+ this.RoomFlags = io.ReadUInt16();
+ this.FunctionFlags = io.ReadUInt16();
+ this.CatalogStringsID = io.ReadUInt16();
+
+ this.Global = io.ReadUInt16();
+ this.BHAV_Init = io.ReadUInt16();
+ this.BHAV_Place = io.ReadUInt16();
+ this.BHAV_UserPickup = io.ReadUInt16();
+ this.WallStyle = io.ReadUInt16();
+ this.BHAV_Load = io.ReadUInt16();
+ this.BHAV_UserPlace = io.ReadUInt16();
+ this.ObjectVersion = io.ReadUInt16();
+ this.BHAV_RoomChange = io.ReadUInt16();
+ this.MotiveEffectsID = io.ReadUInt16();
+ this.BHAV_Cleanup = io.ReadUInt16();
+ this.BHAV_LevelInfo = io.ReadUInt16();
+ this.CatalogID = io.ReadUInt16();
+ this.BHAV_ServingSurface = io.ReadUInt16();
+ this.LevelOffset = io.ReadUInt16();
+ this.Shadow = io.ReadUInt16();
+ this.NumAttributes = io.ReadUInt16();
+
+ this.BHAV_Clean = io.ReadUInt16();
+ this.BHAV_QueueSkipped = io.ReadUInt16();
+ this.FrontDirection = io.ReadUInt16();
+ this.BHAV_WallAdjacencyChanged = io.ReadUInt16();
+ this.MyLeadObject = io.ReadUInt16();
+ this.DynamicSpriteBaseId = io.ReadUInt16();
+ this.NumDynamicSprites = io.ReadUInt16();
+
+ this.ChairEntryFlags = io.ReadUInt16();
+ this.TileWidth = io.ReadUInt16();
+ this.LotCategories = io.ReadUInt16();
+ this.BuildModeType = io.ReadUInt16();
+ this.OriginalGUID1 = io.ReadUInt16();
+ this.OriginalGUID2 = io.ReadUInt16();
+ this.SuitGUID1 = io.ReadUInt16();
+ this.SuitGUID2 = io.ReadUInt16();
+ this.BHAV_Pickup = io.ReadUInt16();
+ this.ThumbnailGraphic = io.ReadUInt16();
+ this.ShadowFlags = io.ReadUInt16();
+ this.FootprintMask = io.ReadUInt16();
+ this.BHAV_DynamicMultiTileUpdate = io.ReadUInt16();
+ this.ShadowBrightness = io.ReadUInt16();
+
+ if (numFields > 78)
+ {
+ this.BHAV_Repair = io.ReadUInt16();
+ this.WallStyleSpriteID = io.ReadUInt16();
+ this.RatingHunger = io.ReadInt16();
+ this.RatingComfort = io.ReadInt16();
+ this.RatingHygiene = io.ReadInt16();
+ this.RatingBladder = io.ReadInt16();
+ this.RatingEnergy = io.ReadInt16();
+ this.RatingFun = io.ReadInt16();
+ this.RatingRoom = io.ReadInt16();
+ this.RatingSkillFlags = io.ReadUInt16();
+ if (numFields > 90)
+ {
+ this.NumTypeAttributes = io.ReadUInt16();
+ this.MiscFlags = io.ReadUInt16();
+ this.TypeAttrGUID = io.ReadUInt32();
+ try
+ {
+ this.FunctionSubsort = io.ReadUInt16();
+ this.DTSubsort = io.ReadUInt16();
+ this.KeepBuying = io.ReadUInt16();
+ this.VacationSubsort = io.ReadUInt16();
+ this.ResetLotAction = io.ReadUInt16();
+ this.CommunitySubsort = io.ReadUInt16();
+ this.DreamFlags = io.ReadUInt16();
+ this.RenderFlags = io.ReadUInt16();
+ this.VitaboyFlags = io.ReadUInt16();
+ this.STSubsort = io.ReadUInt16();
+ this.MTSubsort = io.ReadUInt16();
+ } catch (Exception)
+ {
+ //past this point if these fields are here is really a mystery
+ }
+ }
+ if (this.TypeAttrGUID == 0) this.TypeAttrGUID = GUID;
+ }
+ }
+ }
+
+ public override bool Write(IffFile iff, Stream stream)
+ {
+ using (var io = IoWriter.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ if (IffFile.TargetTS1)
+ {
+ //version 138
+ io.WriteUInt32(138);
+ var fields = VERSION_142_Fields.Concat(VERSION_138b_Extra_Fields);
+ foreach (var prop in fields)
+ {
+ io.WriteUInt16((ushort)GetPropertyByName(prop));
+ }
+ for (int i = fields.Count(); i < 108; i++)
+ {
+ io.WriteUInt16(0);
+ }
+ }
+ else
+ {
+ //tso version 142
+ io.WriteUInt32(142);
+ foreach (var prop in VERSION_142_Fields)
+ {
+ io.WriteUInt16((ushort)GetPropertyByName(prop));
+ }
+ for (int i = VERSION_142_Fields.Length; i < 105; i++)
+ {
+ io.WriteUInt16(0);
+ }
+ }
+ }
+ return true;
+ }
+ }
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/OBJM.cs b/server/tso.files/Formats/IFF/Chunks/OBJM.cs
new file mode 100755
index 0000000..9540188
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/OBJM.cs
@@ -0,0 +1,138 @@
+using FSO.Files.Utils;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ public class OBJM : IffChunk
+ {
+ //work in progress
+
+ //data body starts with 0x01, but what is after that is unknown.
+
+ //empty body from house 0:
+ // 01 00 00 00 | 00 00 00
+ //
+
+ public ushort[] IDToOBJT;
+
+ public Dictionary ObjectData;
+
+ public override void Read(IffFile iff, Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ io.ReadUInt32(); //pad
+ var version = io.ReadUInt32();
+
+ //house 00: 33 00 00 00
+ //house 03: 3E 00 00 00
+ //house 79: 45 00 00 00
+ //completec:49 00 00 00
+ //corresponds to house version?
+
+ var MjbO = io.ReadUInt32();
+
+ var compressionCode = io.ReadByte();
+ if (compressionCode != 1) throw new Exception("hey what!!");
+
+ var iop = new IffFieldEncode(io);
+
+ /*
+ var test1 = iop.ReadInt16();
+ var testas = new ushort[test1*2];
+ for (int i=0; i();
+ while (io.HasMore)
+ {
+ var value = iop.ReadUInt16();
+ if (value == 0) break;
+ table.Add(value);
+ }
+ IDToOBJT = table.ToArray();
+
+ var list = new List();
+ while (io.HasMore)
+ {
+ list.Add(iop.ReadInt16());
+ }
+
+ var offsets = SearchForObjectData(list);
+ for (int i=1; i();
+ int lastOff = 0;
+ foreach (var off in offsets)
+ {
+ // 58 behind the object data...
+ // [-12, 0, -12, 0, -4, 0, -4, 0, -8, 0, -8, 0, 0, 210, 0, 0, 0, 0, 146, 0, -1, -1, 0, 0, 164, 13, 0, 1, 0, 0, 0, 1, 1, 0, 99, 0, 0, 0, 9, 9, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
+ // [-12, 0, -12, 0, -4, 0, -4, 0, -8, 0, -8, 0, 0, 210, 0, 0, 0, 0, 146, 0, -1, -1, 0, 0, 197, 13, 0, 1, 0, 0, 0, 1, 1, 0, 79, 0, 4, 2, 9, 9, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
+ // [-12, 0, -12, 0, -4, 0, -4, 0, -8, 0, -8, 0, 0, 210, 0, 0, 0, 0, 146, 0, -1, -1, 0, 0, 197, 13, 0, 1, 0, 0, 0, 1, 1, 0, 71, 0, 3, 2, 9, 9, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0]
+ // [ 1, 0, 1, 0, 0, 0,256, 0, 48, 0, 0,-1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 0, 19493, 0, 0, -122, -8, 83, 0, 0, 0, 0, -23174, 0, 0, 0, 0, 0, 0, 196, 0, 2, 0, 0, 0, 0, 0,
+ var endOff = off + 72;
+ var size = endOff - lastOff;
+ var data = list.Skip(lastOff).Take(size).ToArray();
+
+ var bas = size - 72;
+ var objID = data[bas+11]; //object id
+ var dir = data[bas + 1];
+ var parent = data[bas + 26];
+ var containerid = data[bas + 2];
+ var containerslot = data[bas + 2];
+
+ ObjectData[objID] = new MappedObject() { ObjectID = objID, Direction = dir, Data = data, ParentID = parent, ContainerID = containerid, ContainerSlot = containerslot };
+
+ lastOff = endOff;
+ }
+ }
+ }
+
+ public class MappedObject {
+ public string Name;
+ public uint GUID;
+ public int ObjectID;
+ public int Direction;
+ public int ParentID;
+
+ public int ContainerID;
+ public int ContainerSlot;
+
+ public short[] Data;
+
+ public int ArryX;
+ public int ArryY;
+ public int ArryLevel;
+
+ public override string ToString()
+ {
+ return Name ?? "(unreferenced)";
+ }
+ }
+
+ public List SearchForObjectData(List data)
+ {
+ //we don't know exactly where the object data is in the format...
+ //but we know objects should have a birth date, basically always 1997 or (1997-36) for npcs.
+ //this should let us extract some important attributes like the structure of the data and object directions.
+
+ var offsets = new List();
+ for (int i=0; i 0 && data[i+1] < 13) && (data[i + 2] > 0 && data[i + 2] < 32)) {
+ offsets.Add(i - 45);
+ }
+ }
+
+ return offsets;
+ }
+ }
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/OBJT.cs b/server/tso.files/Formats/IFF/Chunks/OBJT.cs
new file mode 100755
index 0000000..4e413d6
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/OBJT.cs
@@ -0,0 +1,64 @@
+using FSO.Files.Utils;
+using System.Collections.Generic;
+using System.IO;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ public class OBJT : IffChunk
+ {
+ //another sims 1 masterpiece. A list of object info.
+ public List Entries;
+
+ public override void Read(IffFile iff, Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ var zero = io.ReadInt32();
+ var version = io.ReadInt32(); //should be 2/3
+ var objt = io.ReadInt32(); //tjbo
+
+ Entries = new List();
+ //single tile objects are named. multitile objects arent.
+
+ while (io.HasMore)
+ {
+ Entries.Add(new OBJTEntry(io, version));
+ }
+ }
+ }
+ }
+
+ public class OBJTEntry
+ {
+ public uint GUID;
+ public ushort Unknown1a;
+ public ushort Unknown1b;
+ public ushort Unknown2a;
+ public ushort Unknown2b;
+ public ushort TypeID;
+ public OBJDType OBJDType;
+ public string Name;
+ public OBJTEntry(IoBuffer io, int version)
+ {
+ //16 bytes of data
+ GUID = io.ReadUInt32();
+ if (GUID == 0) return;
+ Unknown1a = io.ReadUInt16();
+ Unknown1b = io.ReadUInt16();
+ Unknown2a = io.ReadUInt16();
+ Unknown2b = io.ReadUInt16();
+ //increases by one each time, one based, essentially an ID for this loaded type. Mostly matches index in array, but I guess it can possibly be different.
+ TypeID = io.ReadUInt16();
+ OBJDType = (OBJDType)io.ReadUInt16();
+ //then the name, null terminated
+ Name = io.ReadNullTerminatedString();
+ if (Name.Length%2 == 0) io.ReadByte(); //pad to short width
+ if (version > 2) io.ReadInt32(); //not sure what this is
+ }
+
+ public override string ToString()
+ {
+ return $"{TypeID}: {Name} ({GUID.ToString("x8")}): [{Unknown1a}, {Unknown1b}, {Unknown2a}, {Unknown2b}, {OBJDType}]";
+ }
+ }
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/OBJf.cs b/server/tso.files/Formats/IFF/Chunks/OBJf.cs
new file mode 100755
index 0000000..2f1ba46
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/OBJf.cs
@@ -0,0 +1,59 @@
+using System.IO;
+using FSO.Files.Utils;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ ///
+ /// This chunk type assigns BHAV subroutines to a number of events that occur in
+ /// (or outside of?) the object, which are described in behavior.iff chunk 00F5.
+ ///
+ public class OBJf : IffChunk
+ {
+ public OBJfFunctionEntry[] functions;
+ public uint Version;
+
+ ///
+ /// Reads a OBJf chunk from a stream.
+ ///
+ /// An Iff instance.
+ /// A Stream object holding a OBJf chunk.
+ public override void Read(IffFile iff, Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ io.ReadUInt32(); //pad
+ Version = io.ReadUInt32();
+ string magic = io.ReadCString(4);
+ functions = new OBJfFunctionEntry[io.ReadUInt32()];
+ for (int i=0; i
+ /// This chunk type holds a color palette.
+ ///
+ public class PALT : IffChunk
+ {
+ public PALT()
+ {
+ }
+
+ public PALT(Color color)
+ {
+ Colors = new Color[256];
+ for (int i = 0; i < 256; i++)
+ {
+ Colors[i] = color;
+ }
+ }
+
+ public Color[] Colors;
+ public int References = 0;
+
+ ///
+ /// Reads a PALT chunk from a stream.
+ ///
+ /// An Iff instance.
+ /// A Stream object holding a PALT chunk.
+ public override void Read(IffFile iff, Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ var version = io.ReadUInt32();
+ var numEntries = io.ReadUInt32();
+ var reserved = io.ReadBytes(8);
+
+ Colors = new Color[numEntries];
+ for (var i = 0; i < numEntries; i++)
+ {
+ var r = io.ReadByte();
+ var g = io.ReadByte();
+ var b = io.ReadByte();
+ Colors[i] = new Color(r, g, b);
+ }
+ }
+ }
+
+ public override bool Write(IffFile iff, Stream stream)
+ {
+ using (var io = IoWriter.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ io.WriteUInt32(0);
+ io.WriteUInt32((uint)Colors.Length);
+ io.WriteBytes(new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 });
+ foreach (var col in Colors)
+ {
+ io.WriteByte(col.R);
+ io.WriteByte(col.G);
+ io.WriteByte(col.B);
+ }
+ return true;
+ }
+ }
+
+ public bool PalMatch(Color[] data)
+ {
+ for (var i=0; i= data.Length) return true;
+ if (data[i].A != 0 && data[i] != Colors[i]) return false;
+ }
+ return true;
+ }
+ }
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/PART.cs b/server/tso.files/Formats/IFF/Chunks/PART.cs
new file mode 100755
index 0000000..5ed7e5a
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/PART.cs
@@ -0,0 +1,140 @@
+using FSO.Files.Utils;
+using Microsoft.Xna.Framework;
+using System.IO;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ public class PART : IffChunk
+ {
+ public static PART BROKEN = new PART()
+ {
+ Gravity = 0.15f,
+ RandomVel = 0.15f,
+ RandomRotVel = 1f,
+ Size = 0.75f,
+ SizeVel = 2.5f,
+ Duration = 3f,
+ FadeIn = 0.15f,
+ FadeOut = 0.6f,
+ SizeVariation = 0.4f,
+
+ TargetColor = Color.Gray,
+ TargetColorVar = 0.5f,
+
+ Frequency = 6f,
+
+ ChunkID = 256
+ };
+
+ public static int CURRENT_VERSION = 1;
+ public int Version = CURRENT_VERSION;
+ public int Type = 0; // default/manualbounds
+
+ public float Frequency;
+ public ushort TexID; //id for MTEX resource
+ public BoundingBox Bounds;
+
+ public Vector3 Velocity;
+ public float Gravity = -0.8f;
+ public float RandomVel;
+ public float RandomRotVel;
+ public float Size = 1;
+ public float SizeVel;
+ public float Duration = 1;
+ public float FadeIn;
+ public float FadeOut;
+ public float SizeVariation;
+ public Color TargetColor;
+ public float TargetColorVar;
+ public int Particles = 15;
+
+ public Vector4[] Parameters = null;
+
+ //(deltax, deltay, deltaz, gravity)
+ //(deltavar, rotdeltavar, size, sizevel)
+ //(duration, fadein, fadeout, sizevar)
+ //(targetColor.rgb, variation)
+
+ public override void Read(IffFile iff, Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ Version = io.ReadInt32();
+ Type = io.ReadInt32();
+
+ Frequency = io.ReadFloat();
+ TexID = io.ReadUInt16();
+ Particles = io.ReadInt32();
+ if (Type == 1)
+ {
+ Bounds = new BoundingBox(
+ new Vector3(io.ReadFloat(), io.ReadFloat(), io.ReadFloat()),
+ new Vector3(io.ReadFloat(), io.ReadFloat(), io.ReadFloat()));
+ }
+
+ Velocity = new Vector3(io.ReadFloat(), io.ReadFloat(), io.ReadFloat());
+ Gravity = io.ReadFloat();
+ RandomVel = io.ReadFloat();
+ RandomRotVel = io.ReadFloat();
+ Size = io.ReadFloat();
+ SizeVel = io.ReadFloat();
+ Duration = io.ReadFloat();
+ FadeIn = io.ReadFloat();
+ FadeOut = io.ReadFloat();
+ SizeVariation = io.ReadFloat();
+ TargetColor.PackedValue = io.ReadUInt32();
+ TargetColorVar = io.ReadFloat();
+ }
+ }
+
+ public override bool Write(IffFile iff, Stream stream)
+ {
+ using (var io = IoWriter.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ io.WriteInt32(Version);
+ io.WriteInt32(Type);
+
+ io.WriteFloat(Frequency);
+ io.WriteUInt16(TexID);
+ io.WriteInt32(Particles);
+ if (Type == 1)
+ {
+ io.WriteFloat(Bounds.Min.X);
+ io.WriteFloat(Bounds.Min.Y);
+ io.WriteFloat(Bounds.Min.Z);
+
+ io.WriteFloat(Bounds.Max.X);
+ io.WriteFloat(Bounds.Max.Y);
+ io.WriteFloat(Bounds.Max.Z);
+ }
+
+ io.WriteFloat(Velocity.X);
+ io.WriteFloat(Velocity.Y);
+ io.WriteFloat(Velocity.Z);
+
+ io.WriteFloat(Gravity);
+ io.WriteFloat(RandomVel);
+ io.WriteFloat(RandomRotVel);
+ io.WriteFloat(Size);
+ io.WriteFloat(SizeVel);
+ io.WriteFloat(Duration);
+ io.WriteFloat(FadeIn);
+ io.WriteFloat(FadeOut);
+ io.WriteFloat(SizeVariation);
+ io.WriteUInt32(TargetColor.PackedValue);
+ io.WriteFloat(TargetColorVar);
+ }
+ return true;
+ }
+
+ public void BakeParameters()
+ {
+ Parameters = new Vector4[4];
+ Parameters[0] = new Vector4(Velocity, Gravity);
+ Parameters[1] = new Vector4(RandomVel, RandomRotVel, Size, SizeVel);
+ Parameters[2] = new Vector4(Duration, FadeIn, FadeOut, SizeVariation);
+ Parameters[3] = TargetColor.ToVector4();
+ Parameters[3].W = TargetColorVar;
+ }
+ }
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/PIFF.cs b/server/tso.files/Formats/IFF/Chunks/PIFF.cs
new file mode 100755
index 0000000..d9aa39b
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/PIFF.cs
@@ -0,0 +1,187 @@
+using FSO.Files.Utils;
+using System;
+using System.IO;
+using System.Linq;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ public class PIFF : IffChunk
+ {
+ public static ushort CURRENT_VERSION = 2;
+ public ushort Version = CURRENT_VERSION;
+ public string SourceIff;
+ public string Comment = "";
+ public PIFFEntry[] Entries;
+
+ public PIFF()
+ {
+ ChunkType = "PIFF";
+ }
+
+ public void AppendAddedChunks(IffFile file)
+ {
+ foreach (var chunk in file.SilentListAll())
+ {
+ if (chunk == this) continue;
+ var entries = Entries.ToList();
+ entries.Add(new PIFFEntry()
+ {
+ ChunkID = chunk.ChunkID,
+ ChunkLabel = chunk.ChunkLabel,
+ ChunkFlags = chunk.ChunkFlags,
+ EntryType = PIFFEntryType.Add,
+ NewDataSize = (uint)(chunk.ChunkData?.Length ?? 0),
+ Type = chunk.ChunkType
+ });
+ Entries = entries.ToArray();
+ }
+ }
+
+ public override void Read(IffFile iff, Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ Version = io.ReadUInt16();
+ SourceIff = io.ReadVariableLengthPascalString();
+ if (Version > 1) Comment = io.ReadVariableLengthPascalString();
+ Entries = new PIFFEntry[io.ReadUInt16()];
+ for (int i=0; i 1) e.Comment = io.ReadVariableLengthPascalString();
+ e.EntryType = (PIFFEntryType)io.ReadByte();
+
+ if (e.EntryType == PIFFEntryType.Patch)
+ {
+ e.ChunkLabel = io.ReadVariableLengthPascalString();
+ e.ChunkFlags = io.ReadUInt16();
+ if (Version > 0) e.NewChunkID = io.ReadUInt16();
+ else e.NewChunkID = e.ChunkID;
+ e.NewDataSize = io.ReadUInt32();
+
+ var size = io.ReadUInt32();
+ e.Patches = new PIFFPatch[size];
+ uint lastOff = 0;
+
+ for (int j=0; j
+ /// This chunk type holds an image in PNG format.
+ ///
+ public class PNG : BMP
+ {
+ }
+
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/SIMI.cs b/server/tso.files/Formats/IFF/Chunks/SIMI.cs
new file mode 100755
index 0000000..c8b8f14
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/SIMI.cs
@@ -0,0 +1,165 @@
+using FSO.Files.Utils;
+using System.IO;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ public class SIMI : IffChunk
+ {
+ public uint Version;
+ public short[] GlobalData;
+
+ public short Unknown1;
+ public int Unknown2;
+ public int Unknown3;
+ public int GUID1;
+ public int GUID2;
+ public int Unknown4;
+ public int LotValue;
+ public int ObjectsValue;
+ public int ArchitectureValue;
+
+ public SIMIBudgetDay[] BudgetDays;
+
+ public int PurchaseValue
+ {
+ get
+ {
+ return LotValue + ObjectsValue + (ArchitectureValue * 7) / 10;
+ }
+ }
+
+ public override void Read(IffFile iff, Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ io.ReadUInt32(); //pad
+ Version = io.ReadUInt32();
+ string magic = io.ReadCString(4);
+ var items = (Version > 0x3F) ? 0x40 : 0x20;
+
+ GlobalData = new short[38];
+
+ for (int i=0; i 0x3E) ? 0x40 : 0x20;
+
+ GlobalData = new short[38];
+
+ for (int i = 0; i < items; i++)
+ {
+ if (i < GlobalData.Length)
+ io.WriteInt16(GlobalData[i]);
+ else
+ io.WriteInt16(0);
+ }
+
+ io.WriteInt16(Unknown1);
+ io.WriteInt32(Unknown2);
+ io.WriteInt32(Unknown3);
+ io.WriteInt32(GUID1);
+ io.WriteInt32(GUID2);
+ io.WriteInt32(Unknown4);
+ io.WriteInt32(LotValue);
+ io.WriteInt32(ObjectsValue);
+ io.WriteInt32(ArchitectureValue);
+
+ for (int i = 0; i < 6; i++)
+ {
+ BudgetDays[i].Write(io);
+ }
+ }
+ return true;
+ }
+
+ public class SIMIBudgetDay
+ {
+ public int Valid;
+ public int MiscIncome;
+ public int JobIncome;
+
+ public int ServiceExpense;
+ public int FoodExpense;
+ public int BillsExpense;
+
+ public int MiscExpense;
+ public int HouseholdExpense;
+ public int ArchitectureExpense;
+
+ public SIMIBudgetDay()
+ {
+
+ }
+
+ public SIMIBudgetDay(IoBuffer io)
+ {
+ Valid = io.ReadInt32();
+ if (Valid == 0) return;
+ MiscIncome = io.ReadInt32();
+ JobIncome = io.ReadInt32();
+ ServiceExpense = io.ReadInt32();
+ FoodExpense = io.ReadInt32();
+ BillsExpense = io.ReadInt32();
+ MiscExpense = io.ReadInt32();
+ HouseholdExpense = io.ReadInt32();
+ ArchitectureExpense = io.ReadInt32();
+ }
+
+ public void Write(IoWriter io)
+ {
+ io.WriteInt32(Valid);
+ if (Valid == 0) return;
+ io.WriteInt32(MiscIncome);
+ io.WriteInt32(JobIncome);
+ io.WriteInt32(ServiceExpense);
+ io.WriteInt32(FoodExpense);
+ io.WriteInt32(BillsExpense);
+ io.WriteInt32(MiscIncome);
+ io.WriteInt32(HouseholdExpense);
+ io.WriteInt32(ArchitectureExpense);
+ }
+ }
+ }
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/SLOT.cs b/server/tso.files/Formats/IFF/Chunks/SLOT.cs
new file mode 100755
index 0000000..31ceae3
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/SLOT.cs
@@ -0,0 +1,218 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using FSO.Files.Utils;
+using Microsoft.Xna.Framework;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ ///
+ /// This format isn't documented on the wiki! Thanks, Darren!
+ ///
+ public class SLOT : IffChunk
+ {
+
+ public static float[] HeightOffsets = {
+ //NOTE: 1 indexed! to get offset for a height, lookup (SLOT.Height-1)
+ 0, //floor
+ 2.5f, //low table
+ 4, //table
+ 4, //counter
+ 0, //non-standard (appears to use offset height)
+ 0, //in hand (unused probably. we handle avatar hands as a special case.)
+ 7, //sitting (used for chairs)
+ 4, //end table
+ 0 //TODO: unknown
+ };
+
+ public Dictionary> Slots = new Dictionary>();
+ public List Chronological = new List();
+
+ public uint Version;
+
+ public override void Read(IffFile iff, System.IO.Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN)){
+ var zero = io.ReadUInt32();
+ var version = io.ReadUInt32();
+ Version = version;
+ var slotMagic = io.ReadBytes(4);
+ var numSlots = io.ReadUInt32();
+
+ /** The span for version 4 is 34.
+ * The span for version 6 is 54.
+ * The span for version 7 is 58.
+ * The span for version 8 is 62.
+ * The span for version 9 is 66.
+ * The span for version 10 is 70. **/
+ for (var i = 0; i < numSlots; i++){
+ var item = new SLOTItem();
+ item.Type = io.ReadUInt16();
+ item.Offset = new Vector3(
+ io.ReadFloat(),
+ io.ReadFloat(),
+ io.ReadFloat()
+ );
+
+ var standing = io.ReadInt32();
+ var sitting = io.ReadInt32();
+ var ground = io.ReadInt32();
+ var rsflags = io.ReadInt32();
+ var snaptargetslot = io.ReadInt32();
+
+ //bonuses (0 means never)
+ item.Standing = standing; //score bonus for standing destinations
+ item.Sitting = sitting; //score bonus for sitting destinations
+ item.Ground = ground; //score bonus for sitting on ground
+
+ item.Rsflags = (SLOTFlags)rsflags;
+ item.SnapTargetSlot = snaptargetslot;
+
+ if (version >= 6)
+ {
+ var minproximity = io.ReadInt32();
+ var maxproximity = io.ReadInt32();
+ var optimalproximity = io.ReadInt32();
+ var i9 = io.ReadInt32();
+ var i10 = io.ReadInt32();
+
+ item.MinProximity = minproximity;
+ item.MaxProximity = maxproximity;
+ item.OptimalProximity = optimalproximity;
+ item.MaxSize = i9;
+ item.I10 = i10;
+ }
+
+ if (version <= 9) {
+ item.MinProximity *= 16;
+ item.MaxProximity *= 16;
+ item.OptimalProximity *= 16;
+ }
+
+ if (version >= 7) item.Gradient = io.ReadFloat();
+
+ if (version >= 8) item.Height = io.ReadInt32();
+
+ if (item.Height == 0) item.Height = 5; //use offset height, nonstandard.
+
+ if (version >= 9)
+ {
+ item.Facing = (SLOTFacing)io.ReadInt32();
+ }
+
+ if (version >= 10) item.Resolution = io.ReadInt32();
+
+ if (!Slots.ContainsKey(item.Type)) Slots.Add(item.Type, new List());
+ Slots[item.Type].Add(item);
+ Chronological.Add(item);
+ }
+ }
+ }
+
+ public override bool Write(IffFile iff, Stream stream)
+ {
+ using (var io = IoWriter.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ io.WriteInt32(0);
+ io.WriteInt32(10); //version
+ io.WriteCString("TOLS", 4);
+ io.WriteUInt32((uint)Chronological.Count);
+ foreach (var slot in Chronological)
+ {
+ io.WriteUInt16(slot.Type);
+ io.WriteFloat(slot.Offset.X);
+ io.WriteFloat(slot.Offset.Y);
+ io.WriteFloat(slot.Offset.Z);
+
+ io.WriteInt32(slot.Standing);
+ io.WriteInt32(slot.Sitting);
+ io.WriteInt32(slot.Ground);
+ io.WriteInt32((int)slot.Rsflags);
+ io.WriteInt32(slot.SnapTargetSlot);
+
+ io.WriteInt32(slot.MinProximity);
+ io.WriteInt32(slot.MaxProximity);
+ io.WriteInt32(slot.OptimalProximity);
+ io.WriteInt32(slot.MaxSize);
+ io.WriteInt32(slot.I10);
+
+ io.WriteFloat(slot.Gradient);
+ io.WriteInt32(slot.Height);
+ io.WriteInt32((int)slot.Facing);
+ io.WriteInt32(slot.Resolution);
+ }
+ }
+ return true;
+ }
+ }
+
+ [Flags]
+ public enum SLOTFlags : int
+ {
+ NORTH = 1,
+ NORTH_EAST = 2,
+ EAST = 4,
+ SOUTH_EAST = 8,
+ SOUTH = 16,
+ SOUTH_WEST = 32,
+ WEST = 64,
+ NORTH_WEST = 128,
+ AllowAnyRotation = 256, //unknown - used for snap to offset? (but not all the time?)
+ Absolute = 512, //do not rotate goal around object
+ FacingAwayFromObject = 1024, //deprecated. does not appear - replaced by Facing field
+ IgnoreRooms = 2048,
+ SnapToDirection = 4096,
+ RandomScoring = 8192,
+ AllowFailureTrees = 16385,
+ AllowDifferentAlts = 32768,
+ UseAverageObjectLocation = 65536,
+
+ FSOEqualProximityScore = 1 << 29,
+ FSOSquare = 1 << 30
+ }
+
+ public enum SLOTFacing : int
+ {
+ FaceAnywhere = -3,
+ FaceTowardsObject = -2,
+ FaceAwayFromObject = -1,
+ }
+
+ public class SLOTItem
+ {
+ public ushort Type { get; set; }
+ public Vector3 Offset;
+ public int Standing { get; set; } = 1;
+ public int Sitting { get; set; } = 0;
+ public int Ground { get; set; } = 0;
+ public SLOTFlags Rsflags { get; set; }
+ public int SnapTargetSlot { get; set; } = -1;
+ public int MinProximity { get; set; }
+ public int MaxProximity { get; set; } = 0;
+ public int OptimalProximity { get; set; } = 0;
+ public int MaxSize { get; set; } = 100;
+ public int I10;
+ public float Gradient { get; set; }
+ public SLOTFacing Facing { get; set; } = SLOTFacing.FaceTowardsObject;
+ public int Resolution { get; set; } = 16;
+ public int Height { get; set; }
+
+ public float OffsetX
+ {
+ get => Offset.X;
+ set => Offset.X = value;
+ }
+
+ public float OffsetY
+ {
+ get => Offset.Y;
+ set => Offset.Y = value;
+ }
+
+ public float OffsetZ
+ {
+ get => Offset.Z;
+ set => Offset.Z = value;
+ }
+ }
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/SPR.cs b/server/tso.files/Formats/IFF/Chunks/SPR.cs
new file mode 100755
index 0000000..b3a6644
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/SPR.cs
@@ -0,0 +1,299 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using FSO.Files.Utils;
+using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework;
+using FSO.Common.Utils;
+using FSO.Common;
+using FSO.Common.Rendering;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ ///
+ /// This chunk type holds a number of paletted sprites that share a common color palette and lack z-buffers and
+ /// alpha buffers. SPR# chunks can be either big-endian or little-endian, which must be determined by comparing
+ /// the first two bytes to zero (since no version number uses more than two bytes).
+ ///
+ public class SPR : IffChunk
+ {
+ public List Frames { get; internal set; }
+ public ushort PaletteID;
+ private List Offsets;
+ public ByteOrder ByteOrd;
+ public bool WallStyle;
+
+ ///
+ /// Reads a SPR chunk from a stream.
+ ///
+ /// An Iff instance.
+ /// A Stream object holding a SPR chunk.
+ public override void Read(IffFile iff, Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ var version1 = io.ReadUInt16();
+ var version2 = io.ReadUInt16();
+ uint version = 0;
+
+ if (version1 == 0)
+ {
+ io.ByteOrder = ByteOrder.BIG_ENDIAN;
+ version = (uint)(((version2|0xFF00)>>8) | ((version2&0xFF)<<8));
+ }
+ else
+ {
+ version = version1;
+ }
+ ByteOrd = io.ByteOrder;
+
+ var spriteCount = io.ReadUInt32();
+ PaletteID = (ushort)io.ReadUInt32();
+
+ Frames = new List();
+ if (version != 1001)
+ {
+ var offsetTable = new List();
+ for (var i = 0; i < spriteCount; i++)
+ {
+ offsetTable.Add(io.ReadUInt32());
+ }
+ Offsets = offsetTable;
+ for (var i = 0; i < spriteCount; i++)
+ {
+ var frame = new SPRFrame(this);
+ io.Seek(SeekOrigin.Begin, offsetTable[i]);
+ var guessedSize = ((i + 1 < offsetTable.Count) ? offsetTable[i + 1] : (uint)stream.Length) - offsetTable[i];
+ frame.Read(version, io, guessedSize);
+ Frames.Add(frame);
+ }
+ }
+ else
+ {
+ while (io.HasMore)
+ {
+ var frame = new SPRFrame(this);
+ frame.Read(version, io, 0);
+ Frames.Add(frame);
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// The frame (I.E sprite) of a SPR chunk.
+ ///
+ public class SPRFrame : ITextureProvider
+ {
+ public static PALT DEFAULT_PALT = new PALT(Color.Black);
+
+ public uint Version;
+ private SPR Parent;
+ private Texture2D PixelCache;
+ private byte[] ToDecode;
+
+ ///
+ /// Constructs a new SPRFrame instance.
+ ///
+ /// A SPR parent.
+ public SPRFrame(SPR parent)
+ {
+ this.Parent = parent;
+ }
+
+ ///
+ /// Reads a SPRFrame from a stream.
+ ///
+ /// An Iff instance.
+ /// A Stream object holding a SPRFrame.
+ public void Read(uint version, IoBuffer io, uint guessedSize)
+ {
+ if (version == 1001)
+ {
+ var spriteFersion = io.ReadUInt32();
+
+ var size = io.ReadUInt32();
+ this.Version = spriteFersion;
+
+ if (IffFile.RETAIN_CHUNK_DATA) ReadDeferred(1001, io);
+ else ToDecode = io.ReadBytes(size);
+ }
+ else
+ {
+ this.Version = version;
+ if (IffFile.RETAIN_CHUNK_DATA) ReadDeferred(1000, io);
+ else ToDecode = io.ReadBytes(guessedSize);
+ }
+ }
+
+ public void ReadDeferred(uint version, IoBuffer io)
+ {
+ var reserved = io.ReadUInt32();
+ var height = io.ReadUInt16();
+ var width = io.ReadUInt16();
+ this.Init(width, height);
+ this.Decode(io);
+ }
+
+ public void DecodeIfRequired()
+ {
+ if (ToDecode != null)
+ {
+ using (IoBuffer buf = IoBuffer.FromStream(new MemoryStream(ToDecode), Parent.ByteOrd))
+ {
+ ReadDeferred(Version, buf);
+ }
+
+ ToDecode = null;
+ }
+ }
+
+ ///
+ /// Decodes this SPRFrame.
+ ///
+ /// IOBuffer used to read a SPRFrame.
+ private void Decode(IoBuffer io)
+ {
+ var palette = Parent.ChunkParent.Get(Parent.PaletteID);
+ if (palette == null)
+ {
+ palette = DEFAULT_PALT;
+ }
+
+ var y = 0;
+ var endmarker = false;
+
+ while (!endmarker){
+ var command = io.ReadByte();
+ var count = io.ReadByte();
+
+ switch (command){
+ /** Start marker **/
+ case 0x00:
+ case 0x10:
+ break;
+ /** Fill row with pixel data **/
+ case 0x04:
+ var bytes = count - 2;
+ var x = 0;
+
+ while (bytes > 0){
+ var pxCommand = io.ReadByte();
+ var pxCount = io.ReadByte();
+ bytes -= 2;
+
+ switch (pxCommand){
+ /** Next {n} pixels are transparent **/
+ case 0x01:
+ x += pxCount;
+ break;
+ /** Next {n} pixels are the same palette color **/
+ case 0x02:
+ var index = io.ReadByte();
+ var padding = io.ReadByte();
+ bytes -= 2;
+
+ var color = palette.Colors[index];
+ for (var j=0; j < pxCount; j++){
+ this.SetPixel(x, y, color);
+ x++;
+ }
+ break;
+ /** Next {n} pixels are specific palette colours **/
+ case 0x03:
+ for (var j=0; j < pxCount; j++){
+ var index2 = io.ReadByte();
+ var color2 = palette.Colors[index2];
+ this.SetPixel(x, y, color2);
+ x++;
+ }
+ bytes -= pxCount;
+ if (pxCount % 2 != 0){
+ //Padding
+ io.ReadByte();
+ bytes--;
+ }
+ break;
+ }
+ }
+
+ y++;
+ break;
+ /** End marker **/
+ case 0x05:
+ endmarker = true;
+ break;
+ /** Leave next rows transparent **/
+ case 0x09:
+ y += count;
+ continue;
+ }
+
+ }
+ }
+
+ private Color[] Data;
+ public int Width { get; internal set; }
+ public int Height { get; internal set; }
+
+ protected void Init(int width, int height)
+ {
+ this.Width = width;
+ this.Height = height;
+ Data = new Color[Width * Height];
+ }
+
+ public Color GetPixel(int x, int y)
+ {
+ return Data[(y * Width) + x];
+ }
+
+ public void SetPixel(int x, int y, Color color)
+ {
+ Data[(y * Width) + x] = color;
+ }
+
+ public Texture2D GetTexture(GraphicsDevice device)
+ {
+ DecodeIfRequired();
+ if (PixelCache == null)
+ {
+ var mip = !Parent.WallStyle && FSOEnvironment.Enable3D && FSOEnvironment.EnableNPOTMip;
+ var tc = FSOEnvironment.TexCompress;
+
+ if (Width * Height > 0)
+ {
+ var w = Math.Max(1, Width);
+ var h = Math.Max(1, Height);
+ if (mip && TextureUtils.OverrideCompression(w, h)) tc = false;
+ if (tc)
+ {
+ PixelCache = new Texture2D(device, ((w+3)/4)*4, ((h+3)/4)*4, mip, SurfaceFormat.Dxt5);
+ if (mip)
+ TextureUtils.UploadDXT5WithMips(PixelCache, w, h, device, Data);
+ else
+ PixelCache.SetData(TextureUtils.DXT5Compress(Data, w, h).Item1);
+ }
+ else
+ {
+ PixelCache = new Texture2D(device, w, h, mip, SurfaceFormat.Color);
+ if (mip)
+ TextureUtils.UploadWithMips(PixelCache, device, Data);
+ else
+ PixelCache.SetData(this.Data);
+ }
+ }
+ else
+ {
+ PixelCache = new Texture2D(device, Math.Max(1, Width), Math.Max(1, Height), mip, SurfaceFormat.Color);
+ PixelCache.SetData(new Color[] { Color.Transparent });
+ }
+
+ PixelCache.Tag = new TextureInfo(PixelCache, Width, Height);
+ if (!IffFile.RETAIN_CHUNK_DATA) Data = null;
+ }
+ return PixelCache;
+ }
+ }
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/SPR2.cs b/server/tso.files/Formats/IFF/Chunks/SPR2.cs
new file mode 100755
index 0000000..af52a5c
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/SPR2.cs
@@ -0,0 +1,787 @@
+using System;
+using System.Linq;
+using Microsoft.Xna.Framework.Graphics;
+using FSO.Files.Utils;
+using System.IO;
+using Microsoft.Xna.Framework;
+using FSO.Common.Utils;
+using FSO.Common.Rendering;
+using FSO.Common;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ ///
+ /// This chunk type holds a number of paletted sprites that may have z-buffer and/or alpha channels.
+ ///
+ public class SPR2 : IffChunk
+ {
+ public SPR2Frame[] Frames = new SPR2Frame[0];
+ public uint DefaultPaletteID;
+ public bool SpritePreprocessed;
+
+ private bool _ZAsAlpha;
+ public bool ZAsAlpha
+ {
+ get
+ {
+ return _ZAsAlpha;
+ }
+ set
+ {
+ if (value && !_ZAsAlpha)
+ {
+ foreach (var frame in Frames)
+ {
+ if (frame.Decoded && frame.PixelData != null) frame.CopyZToAlpha();
+ }
+ }
+ _ZAsAlpha = value;
+
+ }
+ }
+
+ private int _FloorCopy;
+ public int FloorCopy
+ {
+ get
+ {
+ return _FloorCopy;
+ }
+ set
+ {
+ if (value > 0 && _FloorCopy == 0)
+ {
+ foreach (var frame in Frames)
+ {
+ if (frame.Decoded && frame.PixelData != null)
+ {
+ if (value == 1) frame.FloorCopy();
+ if (value == 2) frame.FloorCopyWater();
+ }
+ }
+ }
+ _FloorCopy = value;
+ }
+ }
+
+ ///
+ /// Reads a SPR2 chunk from a stream.
+ ///
+ /// An Iff instance.
+ /// A Stream object holding a SPR2 chunk.
+ public override void Read(IffFile iff, Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ var version = io.ReadUInt32();
+ uint spriteCount = 0;
+
+ if (version == 1000)
+ {
+ spriteCount = io.ReadUInt32();
+ DefaultPaletteID = io.ReadUInt32();
+ var offsetTable = new uint[spriteCount];
+ for (var i = 0; i < spriteCount; i++)
+ {
+ offsetTable[i] = io.ReadUInt32();
+ }
+
+ Frames = new SPR2Frame[spriteCount];
+ for (var i = 0; i < spriteCount; i++)
+ {
+ var frame = new SPR2Frame(this);
+ io.Seek(SeekOrigin.Begin, offsetTable[i]);
+
+ var guessedSize = ((i + 1 < offsetTable.Length) ? offsetTable[i + 1] : (uint)stream.Length) - offsetTable[i];
+
+ frame.Read(version, io, guessedSize);
+ Frames[i] = frame;
+ }
+ }
+ else if (version == 1001)
+ {
+ DefaultPaletteID = io.ReadUInt32();
+ spriteCount = io.ReadUInt32();
+
+ Frames = new SPR2Frame[spriteCount];
+ for (var i = 0; i < spriteCount; i++)
+ {
+ var frame = new SPR2Frame(this);
+ frame.Read(version, io, 0);
+ Frames[i] = frame;
+ }
+ }
+ }
+ }
+
+ public override bool Write(IffFile iff, Stream stream)
+ {
+ using (var io = IoWriter.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ if (IffFile.TargetTS1)
+ {
+ io.WriteUInt32(1000);
+ uint length = 0;
+ if (Frames != null) length = (uint)Frames.Length;
+ io.WriteUInt32(length);
+ DefaultPaletteID = Frames?.FirstOrDefault()?.PaletteID ?? DefaultPaletteID;
+ io.WriteUInt32(DefaultPaletteID);
+ // begin offset table
+ var offTableStart = stream.Position;
+ for (int i = 0; i < length; i++) io.WriteUInt32(0); //filled in later
+ var offsets = new uint[length];
+ int offInd = 0;
+ if (Frames != null)
+ {
+ foreach (var frame in Frames)
+ {
+ offsets[offInd++] = (uint)stream.Position;
+ frame.Write(io, true);
+ }
+ }
+ io.Seek(SeekOrigin.Begin, offTableStart);
+ foreach (var off in offsets) io.WriteUInt32(off);
+ io.Seek(SeekOrigin.End, 0);
+ }
+ else
+ {
+ io.WriteUInt32(1001);
+ io.WriteUInt32(DefaultPaletteID);
+ if (Frames == null) io.WriteUInt32(0);
+ else
+ {
+ io.WriteUInt32((uint)Frames.Length);
+ foreach (var frame in Frames)
+ {
+ frame.Write(io, false);
+ }
+ }
+ }
+ return true;
+ }
+ }
+
+ public void CopyZToAlpha()
+ {
+ foreach (var frame in Frames)
+ {
+ frame.CopyZToAlpha();
+ }
+ }
+
+ public override void Dispose()
+ {
+ if (Frames == null) return;
+ foreach (var frame in Frames)
+ {
+ var palette = ChunkParent.Get(frame.PaletteID);
+ if (palette != null) palette.References--;
+ }
+ }
+ }
+
+ ///
+ /// The frame (I.E sprite) of a SPR2 chunk.
+ ///
+ public class SPR2Frame : ITextureProvider, IWorldTextureProvider
+ {
+ public Color[] PixelData;
+ public byte[] ZBufferData;
+ public byte[] PalData;
+
+ private WeakReference ZCache = new WeakReference(null);
+ private WeakReference PixelCache = new WeakReference(null);
+ private Texture2D PermaRefZ;
+ private Texture2D PermaRefP;
+
+ public int Width { get; internal set; }
+ public int Height { get; internal set; }
+ public uint Flags { get; internal set; }
+ public ushort PaletteID { get; set; }
+ public ushort TransparentColorIndex { get; internal set; }
+ public Vector2 Position { get; internal set; }
+
+ private SPR2 Parent;
+ private uint Version;
+ private byte[] ToDecode;
+ public bool Decoded
+ {
+ get
+ {
+ return ToDecode == null;
+ }
+ }
+ public bool ContainsNothing = false;
+ public bool ContainsNoZ = false;
+
+ public SPR2Frame(SPR2 parent)
+ {
+ this.Parent = parent;
+ }
+
+ ///
+ /// Reads a SPR2 chunk from a stream.
+ ///
+ /// Version of the SPR2 that this frame belongs to.
+ /// A IOBuffer object used to read a SPR2 chunk.
+ public void Read(uint version, IoBuffer io, uint guessedSize)
+ {
+ Version = version;
+ if (version == 1001)
+ {
+ var spriteVersion = io.ReadUInt32();
+ var spriteSize = io.ReadUInt32();
+ if (IffFile.RETAIN_CHUNK_DATA) ReadDeferred(1001, io);
+ else ToDecode = io.ReadBytes(spriteSize);
+ } else
+ {
+ if (IffFile.RETAIN_CHUNK_DATA) ReadDeferred(1000, io);
+ else ToDecode = io.ReadBytes(guessedSize);
+ }
+ }
+
+ public void ReadDeferred(uint version, IoBuffer io)
+ {
+ this.Width = io.ReadUInt16();
+ this.Height = io.ReadUInt16();
+ this.Flags = io.ReadUInt32();
+ this.PaletteID = io.ReadUInt16();
+
+ if (version == 1000 || this.PaletteID == 0 || this.PaletteID == 0xA3A3)
+ {
+ this.PaletteID = (ushort)Parent.DefaultPaletteID;
+ }
+
+ TransparentColorIndex = io.ReadUInt16();
+
+ var y = io.ReadInt16();
+ var x = io.ReadInt16();
+ this.Position = new Vector2(x, y);
+
+ this.Decode(io);
+ }
+
+ public void DecodeIfRequired(bool z)
+ {
+ if (ToDecode != null && (((this.Flags & 0x02) == 0x02 && z && ZBufferData == null) || (!z && PixelData == null)))
+ {
+ using (IoBuffer buf = IoBuffer.FromStream(new MemoryStream(ToDecode), ByteOrder.LITTLE_ENDIAN))
+ {
+ ReadDeferred(Version, buf);
+ }
+
+ if (TimedReferenceController.CurrentType == CacheType.PERMANENT) ToDecode = null;
+ }
+ }
+
+ public void Write(IoWriter io, bool ts1)
+ {
+ using (var sprStream = new MemoryStream())
+ {
+ var sprIO = IoWriter.FromStream(sprStream, ByteOrder.LITTLE_ENDIAN);
+ sprIO.WriteUInt16((ushort)Width);
+ sprIO.WriteUInt16((ushort)Height);
+ sprIO.WriteUInt32(Flags);
+ sprIO.WriteUInt16(PaletteID);
+ sprIO.WriteUInt16(TransparentColorIndex);
+ sprIO.WriteUInt16((ushort)Position.Y);
+ sprIO.WriteUInt16((ushort)Position.X);
+ SPR2FrameEncoder.WriteFrame(this, sprIO);
+
+ var data = sprStream.ToArray();
+ if (!ts1)
+ {
+ io.WriteUInt32(1001);
+ io.WriteUInt32((uint)data.Length);
+ }
+ io.WriteBytes(data);
+ }
+ }
+
+ ///
+ /// Decodes this SPR2Frame.
+ ///
+ /// An IOBuffer instance used to read a SPR2Frame.
+ private void Decode(IoBuffer io)
+ {
+ var y = 0;
+ var endmarker = false;
+
+ var hasPixels = (this.Flags & 0x01) == 0x01;
+ var hasZBuffer = (this.Flags & 0x02) == 0x02;
+ var hasAlpha = (this.Flags & 0x04) == 0x04;
+
+ var numPixels = this.Width * this.Height;
+ var ow = Width;
+ var fc = Parent.FloorCopy;
+ if (fc > 0)
+ {
+ numPixels += Height;
+ Width++;
+ }
+ if (hasPixels){
+ this.PixelData = new Color[numPixels];
+ this.PalData = new byte[numPixels];
+ }
+ if (hasZBuffer){
+ this.ZBufferData = new byte[numPixels];
+ }
+
+ var palette = Parent.ChunkParent.Get(this.PaletteID);
+ if (palette == null) palette = new PALT() { Colors = new Color[256] };
+ palette.References++;
+ var transparentPixel = palette.Colors[TransparentColorIndex];
+ transparentPixel.A = 0;
+
+ while (!endmarker && io.HasMore)
+ {
+ var marker = io.ReadUInt16();
+ var command = marker >> 13;
+ var count = marker & 0x1FFF;
+
+ switch (command)
+ {
+ /** Fill with pixel data **/
+ case 0x00:
+ var bytes = count;
+ bytes -= 2;
+
+ var x = 0;
+
+ while (bytes > 0)
+ {
+ var pxMarker = io.ReadUInt16();
+ var pxCommand = pxMarker >> 13;
+ var pxCount = pxMarker & 0x1FFF;
+ bytes -= 2;
+
+ switch (pxCommand)
+ {
+ case 0x01:
+ case 0x02:
+ var pxWithAlpha = pxCommand == 0x02;
+ for (var col = 0; col < pxCount; col++)
+ {
+ var zValue = io.ReadByte();
+ var pxValue = io.ReadByte();
+ bytes -= 2;
+
+ var pxColor = palette.Colors[pxValue];
+ if (pxWithAlpha)
+ {
+ var alpha = io.ReadByte();
+ pxColor.A = (byte)(alpha * 8.2258064516129032258064516129032);
+ bytes--;
+ }
+ //this mode draws the transparent colour as solid for some reason.
+ //fixes backdrop theater
+ var offset = (y * Width) + x;
+ this.PixelData[offset] = pxColor;
+ this.PalData[offset] = pxValue;
+ this.ZBufferData[offset] = zValue;
+ x++;
+ }
+ if (pxWithAlpha)
+ {
+ /** Padding? **/
+ if ((pxCount * 3) % 2 != 0){
+ bytes--;
+ io.ReadByte();
+ }
+ }
+ break;
+ case 0x03:
+ for (var col = 0; col < pxCount; col++)
+ {
+ var offset = (y * Width) + x;
+ this.PixelData[offset] = transparentPixel;
+ this.PalData[offset] = (byte)TransparentColorIndex;
+ this.PixelData[offset].A = 0;
+ if (hasZBuffer){
+ this.ZBufferData[offset] = 255;
+ }
+ x++;
+ }
+ break;
+ case 0x06:
+ for (var col = 0; col < pxCount; col++)
+ {
+ var pxIndex = io.ReadByte();
+ bytes--;
+ var offset = (y * Width) + x;
+ var pxColor = palette.Colors[pxIndex];
+ byte z = 0;
+
+ //not sure if this should happen
+ /*if (pxIndex == TransparentColorIndex)
+ {
+ pxColor.A = 0;
+ z = 255;
+ }*/
+ this.PixelData[offset] = pxColor;
+ this.PalData[offset] = pxIndex;
+ if (hasZBuffer)
+ {
+ this.ZBufferData[offset] = z;
+ }
+ x++;
+ }
+ if (pxCount % 2 != 0)
+ {
+ bytes--;
+ io.ReadByte();
+ }
+ break;
+ }
+ }
+
+ /** If row isnt filled in, the rest is transparent **/
+ while (x < ow)
+ {
+ var offset = (y * Width) + x;
+ if (hasZBuffer)
+ {
+ this.ZBufferData[offset] = 255;
+ }
+ x++;
+ }
+ break;
+ /** Leave the next count rows in the color channel filled with the transparent color,
+ * in the z-buffer channel filled with 255, and in the alpha channel filled with 0. **/
+ case 0x04:
+ for (var row = 0; row < count; row++)
+ {
+ for (var col = 0; col < Width; col++)
+ {
+ var offset = ((y+row) * Width) + col;
+ if (hasPixels)
+ {
+ this.PixelData[offset] = transparentPixel;
+ this.PalData[offset] = (byte)TransparentColorIndex;
+ }
+ if (hasAlpha)
+ {
+ this.PixelData[offset].A = 0;
+ }
+ if (hasZBuffer)
+ {
+ ZBufferData[offset] = 255;
+ }
+ }
+ }
+ y += count - 1;
+ break;
+ case 0x05:
+ endmarker = true;
+ break;
+ }
+ y++;
+ }
+ if (!IffFile.RETAIN_CHUNK_DATA) PalData = null;
+ if (Parent.ZAsAlpha) CopyZToAlpha();
+ if (Parent.FloorCopy == 1) FloorCopy();
+ if (Parent.FloorCopy == 2) FloorCopyWater();
+ }
+
+ ///
+ /// Gets a pixel from this SPR2Frame.
+ ///
+ /// X position of pixel.
+ /// Y position of pixel.
+ /// A Color instance with color of pixel.
+ public Color GetPixel(int x, int y)
+ {
+ return PixelData[(y * Width) + x];
+ }
+
+ ///
+ /// Gets a pixel from this SPR2Frame.
+ ///
+ /// X position of pixel.
+ /// Y position of pixel.
+ public void SetPixel(int x, int y, Color color)
+ {
+ PixelData[(y * Width) + x] = color;
+ }
+
+ ///
+ /// Copies the Z buffer into the current sprite's alpha channel. Used by water tile.
+ ///
+ public void CopyZToAlpha()
+ {
+ for (int i=0; i= 254) ndat[idx] = rep;
+ else ndat[idx] = PixelData[idx];
+ idx++;
+ }
+ }
+ PixelData = ndat;
+ }
+
+ public void FloorCopyWater()
+ {
+ if (Width % 2 != 0)
+ {
+ var target = new Color[(Width + 1) * Height];
+ for (int y = 0; y < Height; y++)
+ {
+ Array.Copy(PixelData, y * Width, target, y * (Width + 1), Width);
+ }
+ PixelData = target;
+ Width += 1;
+ }
+ var ndat = new Color[PixelData.Length];
+ int hw = (Width) / 2;
+ int hh = (Height) / 2;
+ int idx = 0;
+
+ var palette = Parent.ChunkParent.Get(this.PaletteID);
+ var transparentPixel = palette.Colors[TransparentColorIndex];
+ transparentPixel.A = 0;
+
+ for (int y = 0; y < Height; y++)
+ {
+ for (int x = 0; x < Width; x++)
+ {
+ var dat = PixelData[x + y * Width];
+ if (dat.PackedValue == 0 || dat.PackedValue == transparentPixel.PackedValue)
+ {
+ if (x < hw)
+ {
+ for (int j = x; j < Width; j++)
+ {
+ var rep = PixelData[j + y * Width];
+ if (!(rep.PackedValue == 0 || rep.PackedValue == transparentPixel.PackedValue))
+ {
+ ndat[idx] = rep;
+ break;
+ }
+ }
+ }
+ else
+ {
+ for (int j = x; j >= 0; j--)
+ {
+ var rep = PixelData[j + y * Width];
+ if (!(rep.PackedValue == 0 || rep.PackedValue == transparentPixel.PackedValue))
+ {
+ ndat[idx] = rep;
+ break;
+ }
+ }
+ }
+ } else
+ {
+ ndat[idx] = PixelData[idx];
+ }
+ idx++;
+ }
+ }
+ PixelData = ndat;
+ }
+
+ ///
+ /// Gets a texture representing this SPR2Frame.
+ ///
+ /// GraphicsDevice instance used for drawing.
+ /// A Texture2D instance holding the texture data.
+ public Texture2D GetTexture(GraphicsDevice device)
+ {
+ return GetTexture(device, true);
+ }
+
+ private Texture2D GetTexture(GraphicsDevice device, bool onlyThis)
+ {
+ if (ContainsNothing) return null;
+ Texture2D result = null;
+ if (!PixelCache.TryGetTarget(out result) || ((CachableTexture2D)result).BeingDisposed || result.IsDisposed)
+ {
+ DecodeIfRequired(false);
+ if (this.Width == 0 || this.Height == 0)
+ {
+ ContainsNothing = true;
+ return null;
+ }
+ var tc = FSOEnvironment.TexCompress;
+ var mip = FSOEnvironment.Enable3D && (FSOEnvironment.EnableNPOTMip || (Width == 128 && Height == 64));
+ if (mip && TextureUtils.OverrideCompression(Width, Height)) tc = false;
+ if (tc)
+ {
+
+ result = new CachableTexture2D(device, ((Width+3)/4)*4, ((Height + 3) / 4) * 4, mip, SurfaceFormat.Dxt5);
+ if (mip) TextureUtils.UploadDXT5WithMips(result, Width, Height, device, this.PixelData);
+ else
+ {
+ var dxt = TextureUtils.DXT5Compress(this.PixelData, this.Width, this.Height);
+ result.SetData(dxt.Item1);
+ }
+ }
+ else
+ {
+ result = new CachableTexture2D(device, this.Width, this.Height, mip, SurfaceFormat.Color);
+ if (mip) TextureUtils.UploadWithMips(result, device, this.PixelData);
+ else result.SetData(this.PixelData);
+ }
+ result.Tag = new TextureInfo(result, Width, Height);
+ PixelCache = new WeakReference(result);
+ if (TimedReferenceController.CurrentType == CacheType.PERMANENT) PermaRefP = result;
+ if (!IffFile.RETAIN_CHUNK_DATA)
+ {
+ PixelData = null;
+ //if (onlyThis && !FSOEnvironment.Enable3D) ZBufferData = null;
+ }
+ }
+ if (TimedReferenceController.CurrentType != CacheType.PERMANENT) TimedReferenceController.KeepAlive(result, KeepAliveType.ACCESS);
+ return result;
+ }
+
+ public Texture2D TryGetCachedZ()
+ {
+ Texture2D result = null;
+ if (ContainsNothing || ContainsNoZ) return null;
+ if (!ZCache.TryGetTarget(out result) || ((CachableTexture2D)result).BeingDisposed || result.IsDisposed)
+ return null;
+ return result;
+ }
+
+ ///
+ /// Gets a z-texture representing this SPR2Frame.
+ ///
+ /// GraphicsDevice instance used for drawing.
+ /// A Texture2D instance holding the texture data.
+ public Texture2D GetZTexture(GraphicsDevice device)
+ {
+ return GetZTexture(device, true);
+ }
+
+ private Texture2D GetZTexture(GraphicsDevice device, bool onlyThis)
+ {
+ Texture2D result = null;
+ if (ContainsNothing || ContainsNoZ) return null;
+ if (!ZCache.TryGetTarget(out result) || ((CachableTexture2D)result).BeingDisposed || result.IsDisposed)
+ {
+ DecodeIfRequired(true);
+ if (this.Width == 0 || this.Height == 0)
+ {
+ ContainsNothing = true;
+ return null;
+ }
+ if (ZBufferData == null)
+ {
+ ContainsNoZ = true;
+ return null;
+ }
+ if (FSOEnvironment.TexCompress)
+ {
+ result = new CachableTexture2D(device, ((Width+3)/4)*4, ((Height+3)/4)*4, false, SurfaceFormat.Alpha8);
+ var tempZ = new byte[result.Width * result.Height];
+ var dind = 0;
+ var sind = 0;
+ for (int i=0; i(tempZ);
+ }
+ else
+ {
+ result = new CachableTexture2D(device, this.Width, this.Height, false, SurfaceFormat.Alpha8);
+ result.SetData(this.ZBufferData);
+ }
+ ZCache = new WeakReference(result);
+ if (TimedReferenceController.CurrentType == CacheType.PERMANENT) PermaRefZ = result;
+ if (!IffFile.RETAIN_CHUNK_DATA)
+ {
+ //if (!FSOEnvironment.Enable3D) ZBufferData = null; disabled right now til we get a clean way of getting this post-world-texture for ultra lighting
+ if (onlyThis) PixelData = null;
+ }
+ }
+ if (TimedReferenceController.CurrentType != CacheType.PERMANENT) TimedReferenceController.KeepAlive(result, KeepAliveType.ACCESS);
+ return result;
+ }
+
+ #region IWorldTextureProvider Members
+
+ public WorldTexture GetWorldTexture(GraphicsDevice device)
+ {
+ var result = new WorldTexture
+ {
+ Pixel = this.GetTexture(device, false)
+ };
+ result.ZBuffer = this.GetZTexture(device, false);
+ if (!IffFile.RETAIN_CHUNK_DATA)
+ {
+ PixelData = null;
+ if (!FSOEnvironment.Enable3D) ZBufferData = null;
+ }
+ return result;
+ }
+
+ #endregion
+
+ public Color[] SetData(Color[] px, byte[] zpx, Rectangle rect)
+ {
+ PixelCache = null; //can't exactly dispose this.. it's likely still in use!
+ ZCache = null;
+ PixelData = px;
+ ZBufferData = zpx;
+ Position = new Vector2(rect.X, rect.Y);
+
+ Width = rect.Width;
+ Height = rect.Height;
+ Flags = 7;
+ TransparentColorIndex = 255;
+
+ var colors = SPR2FrameEncoder.QuantizeFrame(this, out PalData);
+
+ var palt = new Color[256];
+ int i = 0;
+ foreach (var c in colors)
+ palt[i++] = new Color(c.R, c.G, c.B, (byte)255);
+
+ return palt;
+ }
+
+ public void SetPalt(PALT p)
+ {
+ if (this.PaletteID != 0)
+ {
+ var old = Parent.ChunkParent.Get(this.PaletteID);
+ if (old != null) old.References--;
+ }
+ PaletteID = p.ChunkID;
+ p.References++;
+ }
+ }
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/SPR2FrameEncoder.cs b/server/tso.files/Formats/IFF/Chunks/SPR2FrameEncoder.cs
new file mode 100755
index 0000000..404cc11
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/SPR2FrameEncoder.cs
@@ -0,0 +1,124 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using System.IO;
+using FSO.Files.Utils;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ public static class SPR2FrameEncoder
+ {
+
+ public delegate Color[] QuantizerFunction(SPR2Frame frame, out byte[] bytes);
+
+ public static QuantizerFunction QuantizeFrame;
+
+ public static void WriteFrame(SPR2Frame frame, IoWriter output)
+ {
+ var bytes = frame.PalData;
+ var col = frame.PixelData;
+ var zs = frame.ZBufferData;
+
+ int index = 0;
+ int blankLines = 0;
+ for (int y=0; y dataBuf = new List();
+ int rlecount = 0;
+ bool anySolid = false;
+
+ var scanStream = new MemoryStream();
+ var scanOut = IoWriter.FromStream(scanStream, ByteOrder.LITTLE_ENDIAN);
+
+ for (int x=0; x 0)
+ {
+ //add transparent lines before our new command
+ output.WriteUInt16((ushort)((0x4<<13) | blankLines));
+ blankLines = 0;
+ }
+ output.WriteUInt16((ushort)(scanData.Length+2));
+ output.WriteBytes(scanData);
+ }
+
+ }
+
+ if (blankLines > 0)
+ {
+ //add transparent lines before our new command
+ output.WriteUInt16((ushort)((0x4 << 13) | blankLines));
+ blankLines = 0;
+ }
+ output.WriteUInt16((ushort)(0x5<<13));
+
+ return;
+ }
+
+ private static byte getCmd(byte col, byte a, byte z)
+ {
+ if (a == 0) return 0x03; //transparent fill
+ else if (a < 255) return 0x02; // col,a,z
+ else if (z > 0) return 0x01; // col,z
+ else return 0x06; // col
+ }
+ }
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/STR.cs b/server/tso.files/Formats/IFF/Chunks/STR.cs
new file mode 100755
index 0000000..e4f1ab0
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/STR.cs
@@ -0,0 +1,438 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.IO;
+using FSO.Files.Utils;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ ///
+ /// This chunk type holds text strings.
+ /// The first two bytes correspond to the format code, of which there are four types.
+ /// Some chunks in the game do not specify any data after the version number, so be sure to implement bounds checking.
+ ///
+ public class STR : IffChunk
+ {
+ public static string[] LanguageSetNames =
+ {
+ "English (US)",
+ "English (UK)",
+ "French",
+ "German",
+ "Italian",
+ "Spanish",
+ "Dutch",
+ "Danish",
+ "Swedish",
+ "Norwegian",
+ "Finish",
+ "Hebrew",
+ "Russian",
+ "Portuguese",
+ "Japanese",
+ "Polish",
+ "Simplified Chinese",
+ "Traditional Chinese",
+ "Thai",
+ "Korean",
+ "Slovak"
+ };
+
+ public STRLanguageSet[] LanguageSets = new STRLanguageSet[20];
+ public static STRLangCode DefaultLangCode = STRLangCode.EnglishUS;
+
+ public STR()
+ {
+ for (int i = 0; i < 20; i++) LanguageSets[i] = new STRLanguageSet { Strings = new STRItem[0] };
+ }
+
+ ///
+ /// How many strings are in this chunk?
+ ///
+ public int Length
+ {
+ get
+ {
+ return LanguageSets[0]?.Strings.Length ?? 0;
+ }
+ }
+
+ public STRLanguageSet GetLanguageSet(STRLangCode set)
+ {
+ if (set == STRLangCode.Default) set = DefaultLangCode;
+ int code = (int)set;
+ if ((LanguageSets[code-1]?.Strings.Length ?? 0) == 0) return LanguageSets[0]; //if undefined, fallback to English US
+ else return LanguageSets[code-1];
+ }
+
+ public bool IsSetInit(STRLangCode set)
+ {
+ if (set == STRLangCode.Default) set = DefaultLangCode;
+ if (set == STRLangCode.EnglishUS) return true;
+ int code = (int)set;
+ return (LanguageSets[code - 1].Strings.Length > 0);
+ }
+
+ public void InitLanguageSet(STRLangCode set)
+ {
+ if (set == STRLangCode.Default) set = DefaultLangCode;
+ int code = (int)set;
+ var length = LanguageSets[0].Strings.Length;
+ LanguageSets[code - 1].Strings = new STRItem[length];
+ for (int i=0; i< length; i++)
+ {
+ var src = LanguageSets[0].Strings[i];
+ LanguageSets[code - 1].Strings[i] = new STRItem()
+ {
+ LanguageCode = (byte)code,
+ Value = src.Value,
+ Comment = src.Comment
+ };
+ }
+ }
+
+ ///
+ /// Gets a string from this chunk.
+ ///
+ /// Index of string.
+ /// A string at specific index, null if not found.
+ ///
+ public string GetString(int index)
+ {
+ return GetString(index, STRLangCode.Default);
+ }
+ public string GetString(int index, STRLangCode language)
+ {
+ var item = GetStringEntry(index, language);
+ if (item != null)
+ {
+ return item.Value;
+ }
+ return null;
+ }
+
+ public string GetComment(int index)
+ {
+ return GetComment(index, STRLangCode.Default);
+ }
+ public string GetComment(int index, STRLangCode language)
+ {
+ var item = GetStringEntry(index, language);
+ if (item != null)
+ {
+ return item.Comment;
+ }
+ return null;
+ }
+
+ public void SetString(int index, string value)
+ {
+ SetString(index, value, STRLangCode.Default);
+ }
+ public void SetString(int index, string value, STRLangCode language)
+ {
+ var languageSet = GetLanguageSet(language);
+ if (index < languageSet.Strings.Length)
+ {
+ languageSet.Strings[index].Value = value;
+ }
+ }
+
+ public void SwapString(int srcindex, int dstindex)
+ {
+ foreach (var languageSet in LanguageSets)
+ {
+ if (languageSet.Strings.Length == 0) continue; //language not initialized
+ var temp = languageSet.Strings[srcindex];
+ languageSet.Strings[srcindex] = languageSet.Strings[dstindex];
+ languageSet.Strings[dstindex] = temp;
+ }
+ }
+
+ public void InsertString(int index, STRItem item)
+ {
+ byte i = 1;
+ foreach (var languageSet in LanguageSets) {
+ if (languageSet.Strings.Length == 0 && i > 1)
+ {
+ i++;
+ continue; //language not initialized
+ }
+ var newStr = new STRItem[languageSet.Strings.Length + 1];
+ Array.Copy(languageSet.Strings, newStr, index); //copy before strings
+ newStr[index] = new STRItem()
+ {
+ LanguageCode = i,
+ Value = item.Value,
+ Comment = item.Comment
+ };
+ Array.Copy(languageSet.Strings, index, newStr, index + 1, (languageSet.Strings.Length - index));
+ languageSet.Strings = newStr;
+ i++;
+ }
+ }
+
+ public void RemoveString(int index)
+ {
+ foreach (var languageSet in LanguageSets)
+ {
+ if (languageSet.Strings.Length == 0) continue; //language not initialized
+ var newStr = new STRItem[languageSet.Strings.Length - 1];
+ Array.Copy(languageSet.Strings, newStr, index); //copy before strings
+ Array.Copy(languageSet.Strings, index + 1, newStr, index, (languageSet.Strings.Length - (index + 1)));
+ languageSet.Strings = newStr;
+ }
+ }
+
+ ///
+ /// Gets a STRItem instance from this STR chunk.
+ ///
+ /// Index of STRItem.
+ /// STRItem at index, null if not found.
+ public STRItem GetStringEntry(int index)
+ {
+ return GetStringEntry(index, STRLangCode.Default);
+ }
+ public STRItem GetStringEntry(int index, STRLangCode language)
+ {
+ var languageSet = GetLanguageSet(language);
+ if (index < (languageSet?.Strings.Length ?? 0) && index > -1)
+ {
+ return languageSet.Strings[index];
+ }
+ return null;
+ }
+
+ ///
+ /// Reads a STR chunk from a stream.
+ ///
+ /// An Iff instance.
+ /// A Stream object holding a STR chunk.
+ public override void Read(IffFile iff, Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ var formatCode = io.ReadInt16();
+ LanguageSets = new STRLanguageSet[20];
+ if (!io.HasMore){
+ for (int i = 0; i < 20; i++) LanguageSets[i] = new STRLanguageSet { Strings = new STRItem[0] };
+ return;
+ }
+
+ if (formatCode == 0)
+ {
+ var numStrings = io.ReadUInt16();
+ for (int i = 0; i < 20; i++) LanguageSets[i] = new STRLanguageSet { Strings = new STRItem[0] };
+ LanguageSets[0].Strings = new STRItem[numStrings];
+ for (var i = 0; i < numStrings; i++)
+ {
+ LanguageSets[0].Strings[i] = new STRItem
+ {
+ Value = io.ReadPascalString()
+ };
+ }
+ }
+ //This format changed 00 00 to use C strings rather than Pascal strings.
+ else if (formatCode == -1)
+ {
+ var numStrings = io.ReadUInt16();
+ for (int i = 0; i < 20; i++) LanguageSets[i] = new STRLanguageSet { Strings = new STRItem[0] };
+ LanguageSets[0].Strings = new STRItem[numStrings];
+ for (var i = 0; i < numStrings; i++)
+ {
+ LanguageSets[0].Strings[i] = new STRItem
+ {
+ Value = io.ReadNullTerminatedUTF8()
+ };
+ }
+ }
+ //This format changed FF FF to use string pairs rather than single strings.
+ else if (formatCode == -2)
+ {
+ var numStrings = io.ReadUInt16();
+ for (int i = 0; i < 20; i++) LanguageSets[i] = new STRLanguageSet { Strings = new STRItem[0] };
+ LanguageSets[0].Strings = new STRItem[numStrings];
+ for (var i = 0; i < numStrings; i++)
+ {
+ LanguageSets[0].Strings[i] = new STRItem
+ {
+ Value = io.ReadNullTerminatedString(),
+ Comment = io.ReadNullTerminatedString()
+ };
+ }
+ }
+ //This format changed FD FF to use a language code.
+ else if (formatCode == -3)
+ {
+ var numStrings = io.ReadUInt16();
+ for (int i = 0; i < 20; i++) LanguageSets[i] = new STRLanguageSet { Strings = new STRItem[0] };
+ List[] LangSort = new List[20];
+ for (var i = 0; i < numStrings; i++)
+ {
+ var item = new STRItem
+ {
+ LanguageCode = io.ReadByte(),
+ Value = io.ReadNullTerminatedString(),
+ Comment = io.ReadNullTerminatedString()
+ };
+
+ var lang = item.LanguageCode;
+ if (lang == 0) lang = 1;
+ else if (lang < 0 || lang > 20) continue; //???
+ if (LangSort[lang - 1] == null)
+ {
+ LangSort[lang-1] = new List();
+ }
+
+ LangSort[lang - 1].Add(item);
+ }
+ for (int i=0; i x?.Strings?.Length ?? 0);
+ io.WriteInt16(total);
+ foreach (var set in LanguageSets)
+ {
+ if (set?.Strings != null)
+ {
+ foreach (var str in set.Strings)
+ {
+ io.WriteByte((byte)(str.LanguageCode));
+ io.WriteNullTerminatedString(str.Value);
+ io.WriteNullTerminatedString(str.Comment);
+ }
+ }
+ }
+ for (int i=0; i
+ /// Item in a STR chunk.
+ ///
+ public class STRItem
+ {
+ public byte LanguageCode;
+ public string Value;
+ public string Comment;
+
+ public STRItem()
+ {
+
+ }
+
+ public STRItem(string value)
+ {
+ Value = value;
+ Comment = "";
+ }
+ }
+
+ public enum STRLangCode : byte
+ {
+ Default = 0,
+ EnglishUS = 1,
+ EnglishUK = 2,
+ French = 3,
+ German = 4,
+ Italian = 5,
+ Spanish = 6,
+ Dutch = 7,
+ Danish = 8,
+ Swedish = 9,
+ Norwegian = 10,
+ Finish = 11,
+ Hebrew = 12,
+ Russian = 13,
+ Portuguese = 14,
+ Japanese = 15,
+ Polish = 16,
+ SimplifiedChinese = 17,
+ TraditionalChinese = 18,
+ Thai = 19,
+ Korean = 20,
+
+ //begin freeso
+ Slovak = 21
+ }
+
+ ///
+ /// Set of STRItems for a language.
+ ///
+ public class STRLanguageSet
+ {
+ public STRItem[] Strings = new STRItem[0];
+ }
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/THMB.cs b/server/tso.files/Formats/IFF/Chunks/THMB.cs
new file mode 100755
index 0000000..b1ec212
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/THMB.cs
@@ -0,0 +1,26 @@
+using FSO.Files.Utils;
+using System.IO;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ public class THMB : IffChunk
+ {
+ public int Width;
+ public int Height;
+ public int BaseYOff;
+ public int XOff;
+ public int AddYOff; //accounts for difference between roofed and unroofed. relative to the base.
+
+ public override void Read(IffFile iff, Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ Width = io.ReadInt32();
+ Height = io.ReadInt32();
+ BaseYOff = io.ReadInt32();
+ XOff = io.ReadInt32(); //0 in all cases i've found, pretty much?
+ AddYOff = io.ReadInt32();
+ }
+ }
+ }
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/TPRP.cs b/server/tso.files/Formats/IFF/Chunks/TPRP.cs
new file mode 100755
index 0000000..90d50b9
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/TPRP.cs
@@ -0,0 +1,79 @@
+using FSO.Files.Utils;
+using System.IO;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ ///
+ /// Labels for BHAV local variables and parameters.
+ ///
+ public class TPRP : IffChunk
+ {
+ public string[] ParamNames;
+ public string[] LocalNames;
+
+ ///
+ /// Reads a TPRP from a stream.
+ ///
+ /// Iff instance.
+ /// A Stream instance holding a TPRP chunk.
+ public override void Read(IffFile iff, System.IO.Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ var zero = io.ReadInt32();
+ var version = io.ReadInt32();
+ var name = io.ReadCString(4); //"PRPT", or randomly 4 null characters for no good reason
+
+ var pCount = io.ReadInt32();
+ var lCount = io.ReadInt32();
+ ParamNames = new string[pCount];
+ LocalNames = new string[lCount];
+ for (int i = 0; i < pCount; i++)
+ {
+ ParamNames[i] = (version == 5) ? io.ReadPascalString() : io.ReadNullTerminatedString();
+ }
+ for (int i = 0; i < lCount; i++)
+ {
+ LocalNames[i] = (version == 5) ? io.ReadPascalString() : io.ReadNullTerminatedString();
+ }
+
+ for (int i = 0; i < pCount; i++)
+ {
+ //flags for parameters. probably disabled, unused, etc.
+ var flag = io.ReadByte();
+ }
+
+ //what are these?
+ if (version >= 3)
+ io.ReadInt32();
+ if (version >= 4)
+ io.ReadInt32();
+ }
+ }
+
+ public override bool Write(IffFile iff, Stream stream)
+ {
+ using (var io = IoWriter.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ io.WriteInt32(0);
+ io.WriteInt32(5); //version
+ io.WriteCString("PRPT", 4);
+ io.WriteInt32(ParamNames.Length);
+ io.WriteInt32(LocalNames.Length);
+ foreach (var param in ParamNames)
+ io.WritePascalString(param);
+ foreach (var local in LocalNames)
+ io.WritePascalString(local);
+
+ for (int i=0; i
+ /// Provides labels for BCON constants with the same resource ID.
+ ///
+ public class TRCN : IffChunk
+ {
+ public int Version;
+ public TRCNEntry[] Entries;
+
+ ///
+ /// Reads a BCON chunk from a stream.
+ ///
+ /// An Iff instance.
+ /// A Stream instance holding a BCON.
+ public override void Read(IffFile iff, Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ var zero = io.ReadInt32();
+ Version = io.ReadInt32();
+ var magic = io.ReadInt32();
+ var count = io.ReadInt32();
+ Entries = new TRCNEntry[count];
+ for (int i=0; i 0 && Version > 0);
+ Entries[i] = entry;
+ }
+ }
+ }
+
+ public override bool Write(IffFile iff, Stream stream)
+ {
+ using (var io = IoWriter.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ io.WriteInt32(0);
+ io.WriteInt32(2); //we write out version 2
+ io.WriteInt32(0); //todo: NCRT ascii
+ io.WriteInt32(Entries.Length);
+ foreach (var entry in Entries)
+ {
+ entry.Write(io);
+ }
+
+ return true;
+ }
+ }
+ }
+
+ public class TRCNEntry
+ {
+ public int Flags;
+ public int Unknown;
+ public string Label = "";
+ public string Comment = "";
+
+ public byte RangeEnabled; //v1+ only
+ public short LowRange;
+ public short HighRange = 100;
+
+ public void Read(IoBuffer io, int version, bool odd)
+ {
+ Flags = io.ReadInt32();
+ Unknown = io.ReadInt32();
+ Label = (version > 1) ? io.ReadVariableLengthPascalString() : io.ReadNullTerminatedString();
+ if (version < 2 && ((Label.Length % 2 == 0) ^ odd)) io.ReadByte();
+ Comment = (version > 1) ? io.ReadVariableLengthPascalString() : io.ReadNullTerminatedString();
+ if (version < 2 && (Comment.Length % 2 == 0)) io.ReadByte();
+
+ if (version > 0)
+ {
+ RangeEnabled = io.ReadByte();
+ LowRange = io.ReadInt16();
+ HighRange = io.ReadInt16();
+ //io.ReadByte();
+ }
+ }
+
+ public void Write(IoWriter io)
+ {
+ io.WriteInt32(Flags);
+ io.WriteInt32(Unknown);
+ io.WriteVariableLengthPascalString(Label);
+ io.WriteVariableLengthPascalString(Comment);
+
+ io.WriteByte(RangeEnabled);
+ io.WriteInt16(LowRange);
+ io.WriteInt16(HighRange);
+ }
+ }
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/TREE.cs b/server/tso.files/Formats/IFF/Chunks/TREE.cs
new file mode 100755
index 0000000..db77972
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/TREE.cs
@@ -0,0 +1,345 @@
+using FSO.Files.Utils;
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ public class TREE : IffChunk
+ {
+ public static TREE GenerateEmpty(BHAV bhav)
+ {
+ var result = new TREE();
+ result.ChunkLabel = "";
+ result.ChunkID = bhav.ChunkID;
+ result.AddedByPatch = true;
+ result.ChunkProcessed = true;
+ result.RuntimeInfo = ChunkRuntimeState.Modified;
+ result.ChunkType = "TREE";
+
+ result.CorrectConnections(bhav);
+ return result;
+ /*
+ var additionID = bhav.Instructions.Length;
+
+ Func resolveTrueFalse = (byte pointer) =>
+ {
+ switch (pointer)
+ {
+ case 253:
+ return -1;
+ case 255:
+ //generate false
+ case 254:
+ //generate true
+ }
+ if (pointer == 255) return -1;
+ else if (pointer == 2)
+ };
+
+ //make an entry for each instruction. positions and sizes don't matter - we have a runtime flag to indicate they are not valid
+ for (int i=0; i Entries = new List();
+ public int PrimitiveCount => Entries.FindLastIndex(x => x.Type == TREEBoxType.Primitive) + 1;
+
+ //runtime
+ public uint TreeVersion = 0;
+
+ public override void Read(IffFile iff, Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ var zero = io.ReadInt32();
+ var version = io.ReadInt32();
+ if (version > 1) throw new Exception("Unexpected TREE version: " + version);
+ string magic = io.ReadCString(4); //HBGN
+ if (magic != "EERT") throw new Exception("Magic number should be 'EERT', got " + magic);
+ var entryCount = io.ReadInt32();
+ Entries.Clear();
+ for (int i=0; i= after) box.InternalID += (short)delta;
+ if (box.TruePointer >= after) box.TruePointer += (short)delta;
+ if (box.FalsePointer >= after) box.FalsePointer += (short)delta;
+ }
+ }
+
+ public void CorrectConnections(BHAV bhav)
+ {
+ //make sure there are enough primitives for the bhav
+ var realPrimCount = bhav.Instructions.Length;
+ var treePrimCount = Entries.FindLastIndex(x => x.Type == TREEBoxType.Primitive) + 1;
+
+ ApplyPointerDelta(realPrimCount-treePrimCount, treePrimCount);
+ if (realPrimCount > treePrimCount)
+ {
+ //add new treeboxes
+ for (int i=treePrimCount; i realPrimCount)
+ {
+ //remove treeboxes
+ for (int i=treePrimCount; i>realPrimCount; i--)
+ {
+ Entries.RemoveAt(i-1);
+ }
+ }
+
+ //make sure connections for each of the primitives match the BHAV
+ //if they don't, reconnect them or generate new boxes (true/false endpoints, maybe gotos in future)
+
+ for (int i=0; i= Entries.Count) return null;
+ return Entries[pointer];
+ }
+ }
+
+ public class TREEBox
+ {
+ //runtime
+ public short InternalID = -1;
+ public bool PosisionInvalid; //forces a regeneration of position using the default tree algorithm
+ public TREE Parent;
+ public byte TrueID
+ {
+ get
+ {
+ switch (Type)
+ {
+ case TREEBoxType.Primitive:
+ return (byte)InternalID;
+ case TREEBoxType.Goto:
+ return LabelTrueID(new HashSet());
+ case TREEBoxType.Label:
+ return 253; //arrows cannot point to a label
+ case TREEBoxType.True:
+ return 254;
+ case TREEBoxType.False:
+ return 255;
+ }
+ return 253;
+ }
+ }
+
+ public byte LabelTrueID(HashSet visited)
+ {
+ if (Type != TREEBoxType.Goto) return TrueID;
+ if (visited.Contains(InternalID)) return 253; //error
+ visited.Add(InternalID);
+ return Parent?.GetBox(Parent.GetBox(TruePointer)?.TruePointer ?? -1)?.LabelTrueID(visited) ?? 253;
+ }
+
+ //data
+ public TREEBoxType Type;
+ public ushort Unknown;
+ public short Width;
+ public short Height;
+ public short X;
+ public short Y;
+ public short CommentSize = 0x10;
+ public short TruePointer = -1;
+ public short Special; //0 or -1... unknown.
+ public int FalsePointer = -1;
+ public string Comment = "";
+ public int TrailingZero = 0;
+
+ public TREEBox(TREE parent)
+ {
+ Parent = parent;
+ }
+
+ public void Read(IoBuffer io, int version)
+ {
+ Type = (TREEBoxType)io.ReadUInt16();
+ Unknown = io.ReadUInt16();
+ Width = io.ReadInt16();
+ Height = io.ReadInt16();
+ X = io.ReadInt16();
+ Y = io.ReadInt16();
+ CommentSize = io.ReadInt16();
+ TruePointer = io.ReadInt16();
+ Special = io.ReadInt16();
+ FalsePointer = io.ReadInt32();
+ Comment = io.ReadNullTerminatedString();
+ if (Comment.Length % 2 == 0) io.ReadByte(); //padding to 2 byte align
+ if (version > 0) TrailingZero = io.ReadInt32();
+
+ if (!Enum.IsDefined(typeof(TREEBoxType), Type)) throw new Exception("Unexpected TREE box type: " + Type.ToString());
+ if (Special < -1 || Special > 0) throw new Exception("Unexpected TREE special: " + Special);
+ if (Unknown != 0) throw new Exception("Unexpected Unknown: " + Unknown);
+ if (TrailingZero != 0) Console.WriteLine("Unexpected TrailingZero: " + TrailingZero);
+ }
+
+ public void Write(IoWriter io)
+ {
+ io.WriteUInt16((ushort)Type);
+ io.WriteUInt16(Unknown);
+ io.WriteInt16(Width);
+ io.WriteInt16(Height);
+ io.WriteInt16(X);
+ io.WriteInt16(Y);
+ io.WriteInt16(CommentSize);
+ io.WriteInt16(TruePointer);
+ io.WriteInt16(Special);
+ io.WriteInt32(FalsePointer);
+ io.WriteCString(Comment);
+ if (Comment.Length % 2 == 0) io.WriteByte(0xCD); //padding to 2 byte align
+ io.WriteInt32(TrailingZero);
+ }
+
+ public override string ToString()
+ {
+ return Type.ToString() + " (" + TruePointer + ((FalsePointer == -1) ? "" : ("/"+FalsePointer)) + "): " + Comment;
+ }
+ }
+
+ public enum TREEBoxType : ushort
+ {
+ Primitive = 0,
+ True = 1,
+ False = 2,
+ Comment = 3,
+ Label = 4,
+ Goto = 5 //no comment size, roughly primitive sized (180, 48), pointer goes to Label
+ }
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/TTAB.cs b/server/tso.files/Formats/IFF/Chunks/TTAB.cs
new file mode 100755
index 0000000..c117910
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/TTAB.cs
@@ -0,0 +1,433 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.IO;
+using FSO.Files.Utils;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ ///
+ /// This chunk type defines a list of interactions for an object and assigns a BHAV subroutine
+ /// for each interaction. The pie menu labels shown to the user are stored in a TTAs chunk with
+ /// the same ID.
+ ///
+ public class TTAB : IffChunk
+ {
+ public TTABInteraction[] Interactions = new TTABInteraction[0];
+ public Dictionary InteractionByIndex = new Dictionary();
+ public TTABInteraction[] AutoInteractions = new TTABInteraction[0];
+
+ public static float[] AttenuationValues = {
+ 0, //custom
+ 0, //none
+ 0.1f, //low
+ 0.3f, //medium
+ 0.6f, //high
+ };
+
+ public static float[] VisitorAttenuationValues = {
+ 0, //custom
+ 0, //none
+ 0.01f, //low
+ 0.02f, //medium
+ 0.03f, //high
+ };
+
+ ///
+ /// Reads a TTAB chunk from a stream.
+ ///
+ /// An Iff instance.
+ /// A Stream object holding a TTAB chunk.
+ public override void Read(IffFile iff, Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ InteractionByIndex.Clear();
+ Interactions = new TTABInteraction[io.ReadUInt16()];
+ if (Interactions.Length == 0) return; //no interactions, don't bother reading remainder.
+ var version = io.ReadUInt16();
+ IOProxy iop;
+ if (version <= 3)
+ {
+ // DO NOT LOAD THIS TTAB TYPE
+ Interactions = new TTABInteraction[0];
+ return;
+ }
+ if (version < 9 || (version > 10 && !iff.TSBO)) iop = new TTABNormal(io);
+ else
+ {
+ var compressionCode = io.ReadByte();
+ if (compressionCode != 1) iop = new TTABNormal(io);
+ else iop = new IffFieldEncode(io);
+ }
+ for (int i = 0; i < Interactions.Length; i++)
+ {
+ var result = new TTABInteraction();
+ result.ActionFunction = iop.ReadUInt16();
+ result.TestFunction = iop.ReadUInt16();
+ result.MotiveEntries = new TTABMotiveEntry[iop.ReadUInt32()];
+ result.Flags = (TTABFlags)iop.ReadUInt32();
+ result.TTAIndex = iop.ReadUInt32();
+ if (version > 6) result.AttenuationCode = iop.ReadUInt32();
+ result.AttenuationValue = iop.ReadFloat();
+ result.AutonomyThreshold = iop.ReadUInt32();
+ result.JoiningIndex = iop.ReadInt32();
+ for (int j = 0; j < result.MotiveEntries.Length; j++)
+ {
+ var motive = new TTABMotiveEntry();
+ motive.MotiveIndex = j;
+ if (version > 6) motive.EffectRangeMinimum = iop.ReadInt16();
+ motive.EffectRangeDelta = iop.ReadInt16();
+ if (version > 6) motive.PersonalityModifier = iop.ReadUInt16();
+ result.MotiveEntries[j] = motive;
+ }
+ if (version > 9 && !iff.TSBO)
+ {
+ result.Flags2 = (TSOFlags)iop.ReadUInt32();
+ }
+ Interactions[i] = result;
+ InteractionByIndex.Add(result.TTAIndex, result);
+ }
+ }
+ InitAutoInteractions();
+ }
+
+ public override bool Write(IffFile iff, Stream stream)
+ {
+ using (var io = IoWriter.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ io.WriteUInt16((ushort)Interactions.Length);
+ io.WriteUInt16((ushort)((IffFile.TargetTS1) ? 8 : 10)); //version. we save version 10 which uses the IO proxy
+ //...but we can't write out to that yet so write with compression code 0
+ if (!IffFile.TargetTS1) io.WriteByte(0);
+ for (int i = 0; i < Interactions.Length; i++)
+ {
+ var action = Interactions[i];
+ io.WriteUInt16(action.ActionFunction);
+ io.WriteUInt16(action.TestFunction);
+ io.WriteUInt32((uint)action.MotiveEntries.Length);
+ io.WriteUInt32((uint)action.Flags);
+ io.WriteUInt32(action.TTAIndex);
+ io.WriteUInt32(action.AttenuationCode);
+ io.WriteFloat(action.AttenuationValue);
+ io.WriteUInt32(action.AutonomyThreshold);
+ io.WriteInt32(action.JoiningIndex);
+ for (int j=0; j < action.MotiveEntries.Length; j++)
+ {
+ var mot = action.MotiveEntries[j];
+ io.WriteInt16(mot.EffectRangeMinimum);
+ io.WriteInt16(mot.EffectRangeDelta);
+ io.WriteUInt16(mot.PersonalityModifier);
+ }
+ if (!IffFile.TargetTS1) io.WriteUInt32((uint)action.Flags2);
+ }
+ }
+ return true;
+ }
+
+ public void InsertInteraction(TTABInteraction action, int index)
+ {
+ var newInt = new TTABInteraction[Interactions.Length + 1];
+ if (index == -1) index = 0;
+ Array.Copy(Interactions, newInt, index); //copy before strings
+ newInt[index] = action;
+ Array.Copy(Interactions, index, newInt, index + 1, (Interactions.Length - index));
+ Interactions = newInt;
+
+ if (!InteractionByIndex.ContainsKey(action.TTAIndex)) InteractionByIndex.Add(action.TTAIndex, action);
+ InitAutoInteractions();
+ }
+
+ public void DeleteInteraction(int index)
+ {
+ var action = Interactions[index];
+ var newInt = new TTABInteraction[Interactions.Length - 1];
+ if (index == -1) index = 0;
+ Array.Copy(Interactions, newInt, index); //copy before strings
+ Array.Copy(Interactions, index + 1, newInt, index, (Interactions.Length - (index + 1)));
+ Interactions = newInt;
+
+ if (InteractionByIndex.ContainsKey(action.TTAIndex)) InteractionByIndex.Remove(action.TTAIndex);
+ InitAutoInteractions();
+ }
+
+ public void InitAutoInteractions()
+ {
+ foreach (var interaction in Interactions)
+ {
+ interaction.ActiveMotiveEntries = interaction.MotiveEntries.Where(x => x.EffectRangeDelta != 0).ToArray();
+ }
+ AutoInteractions = Interactions.Where(interaction => interaction.ActiveMotiveEntries.Length > 0).ToArray();
+ }
+ }
+
+ public abstract class IOProxy
+ {
+ public abstract ushort ReadUInt16();
+ public abstract short ReadInt16();
+ public abstract int ReadInt32();
+ public abstract uint ReadUInt32();
+ public abstract float ReadFloat();
+
+ public IoBuffer io;
+ public IOProxy(IoBuffer io)
+ {
+ this.io = io;
+ }
+ }
+
+ class TTABNormal : IOProxy
+ {
+ public override ushort ReadUInt16() { return io.ReadUInt16(); }
+ public override short ReadInt16() { return io.ReadInt16(); }
+ public override int ReadInt32() { return io.ReadInt32(); }
+ public override uint ReadUInt32() { return io.ReadUInt32(); }
+ public override float ReadFloat() { return io.ReadFloat(); }
+
+ public TTABNormal(IoBuffer io) : base(io) { }
+ }
+
+ ///
+ /// Represents an interaction in a TTAB chunk.
+ ///
+ public class TTABInteraction
+ {
+ public ushort ActionFunction;
+ public ushort TestFunction;
+ public TTABMotiveEntry[] MotiveEntries;
+ public TTABFlags Flags;
+ public uint TTAIndex;
+ public uint AttenuationCode;
+ public float AttenuationValue;
+ public uint AutonomyThreshold;
+ public int JoiningIndex;
+ public TSOFlags Flags2 = (TSOFlags)0x1e; //allow a lot of things
+
+ public TTABMotiveEntry[] ActiveMotiveEntries; //populated when asking for auto interactions.
+
+ public InteractionMaskFlags MaskFlags {
+ get {
+ return (InteractionMaskFlags)(((int)Flags >> 16) & 0xF);
+ }
+ set
+ {
+ Flags = (TTABFlags)(((int)Flags & 0xFFFF) | ((int)value << 16));
+ }
+ }
+
+ //ALLOW
+ public bool AllowVisitors
+ {
+ get { return (Flags & TTABFlags.AllowVisitors) > 0 || (Flags2 & TSOFlags.AllowVisitors) > 0; }
+ set {
+ Flags &= ~(TTABFlags.AllowVisitors); if (value) Flags |= TTABFlags.AllowVisitors;
+ Flags2 &= ~(TSOFlags.AllowVisitors); if (value) Flags2 |= TSOFlags.AllowVisitors;
+ }
+ }
+ public bool AllowFriends
+ {
+ get { return (Flags2 & TSOFlags.AllowFriends) > 0; }
+ set { Flags2 &= ~(TSOFlags.AllowFriends); if (value) Flags2 |= TSOFlags.AllowFriends; }
+ }
+ public bool AllowRoommates
+ {
+ get { return (Flags2 & TSOFlags.AllowRoommates) > 0; }
+ set { Flags2 &= ~(TSOFlags.AllowRoommates); if (value) Flags2 |= TSOFlags.AllowRoommates; }
+ }
+ public bool AllowObjectOwner
+ {
+ get { return (Flags2 & TSOFlags.AllowObjectOwner) > 0; }
+ set { Flags2 &= ~(TSOFlags.AllowObjectOwner); if (value) Flags2 |= TSOFlags.AllowObjectOwner; }
+ }
+ public bool UnderParentalControl
+ {
+ get { return (Flags2 & TSOFlags.UnderParentalControl) > 0; }
+ set { Flags2 &= ~(TSOFlags.UnderParentalControl); if (value) Flags2 |= TSOFlags.UnderParentalControl; }
+ }
+ public bool AllowCSRs
+ {
+ get { return (Flags2 & TSOFlags.AllowCSRs) > 0; }
+ set { Flags2 &= ~(TSOFlags.AllowCSRs); if (value) Flags2 |= TSOFlags.AllowCSRs; }
+ }
+ public bool AllowGhosts
+ {
+ get { return (Flags2 & TSOFlags.AllowGhost) > 0; }
+ set { Flags2 &= ~(TSOFlags.AllowGhost); if (value) Flags2 |= TSOFlags.AllowGhost; }
+ }
+
+ public bool AllowCats
+ {
+ get { return (Flags & TTABFlags.AllowCats) > 0; }
+ set { Flags &= ~(TTABFlags.AllowCats); if (value) Flags |= TTABFlags.AllowCats; }
+ }
+ public bool AllowDogs
+ {
+ get { return (Flags & TTABFlags.AllowDogs) > 0; }
+ set { Flags &= ~(TTABFlags.AllowDogs); if (value) Flags |= TTABFlags.AllowDogs; }
+ }
+
+ //TS1
+
+ public bool TS1AllowCats
+ {
+ get { return (Flags & TTABFlags.TS1AllowCats) > 0; }
+ set { Flags &= ~(TTABFlags.TS1AllowCats); if (value) Flags |= TTABFlags.TS1AllowCats; }
+ }
+ public bool TS1AllowDogs
+ {
+ get { return (Flags & TTABFlags.TS1AllowDogs) > 0; }
+ set { Flags &= ~(TTABFlags.TS1AllowDogs); if (value) Flags |= TTABFlags.TS1AllowDogs; }
+ }
+
+ public bool TS1AllowAdults
+ {
+ get { return (Flags & TTABFlags.TS1NoAdult) == 0; }
+ set { Flags &= ~(TTABFlags.TS1NoAdult); if (!value) Flags |= TTABFlags.TS1NoAdult; }
+ }
+
+ public bool TS1AllowChild
+ {
+ get { return (Flags & TTABFlags.TS1NoChild) == 0; }
+ set { Flags &= ~(TTABFlags.TS1NoChild); if (!value) Flags |= TTABFlags.TS1NoChild; }
+ }
+
+ public bool TS1AllowDemoChild
+ {
+ get { return (Flags & TTABFlags.TS1NoDemoChild) == 0; }
+ set { Flags &= ~(TTABFlags.TS1NoDemoChild); if (!value) Flags |= TTABFlags.TS1NoDemoChild; }
+ }
+
+ public bool Joinable
+ {
+ get { return (Flags & TTABFlags.Joinable) > 0; }
+ set { Flags &= ~(TTABFlags.Joinable); if (value) Flags |= TTABFlags.Joinable; }
+ }
+
+ //FLAGS
+ public bool Debug
+ {
+ get { return (Flags & TTABFlags.Debug) > 0; }
+ set { Flags &= ~(TTABFlags.Debug); if (value) Flags |= TTABFlags.Debug; }
+ }
+
+ public bool Leapfrog {
+ get { return (Flags & TTABFlags.Leapfrog) > 0; }
+ set { Flags &= ~(TTABFlags.Leapfrog); if (value) Flags |= TTABFlags.Leapfrog; }
+ }
+ public bool MustRun
+ {
+ get { return (Flags & TTABFlags.MustRun) > 0; }
+ set { Flags &= ~(TTABFlags.MustRun); if (value) Flags |= TTABFlags.MustRun; }
+ }
+ public bool AutoFirst
+ {
+ get { return (Flags & TTABFlags.AutoFirstSelect) > 0; }
+ set { Flags &= ~(TTABFlags.AutoFirstSelect); if (value) Flags |= TTABFlags.AutoFirstSelect; }
+ }
+ public bool RunImmediately
+ {
+ get { return (Flags & TTABFlags.RunImmediately) > 0; }
+ set { Flags &= ~(TTABFlags.RunImmediately); if (value) Flags |= TTABFlags.RunImmediately; }
+ }
+ public bool AllowConsecutive
+ {
+ get { return (Flags & TTABFlags.AllowConsecutive) > 0; }
+ set { Flags &= ~(TTABFlags.AllowConsecutive); if (value) Flags |= TTABFlags.AllowConsecutive; }
+ }
+
+
+ public bool Carrying
+ {
+ get { return (MaskFlags & InteractionMaskFlags.AvailableWhenCarrying) > 0; }
+ set { MaskFlags &= ~(InteractionMaskFlags.AvailableWhenCarrying); if (value) MaskFlags |= InteractionMaskFlags.AvailableWhenCarrying; }
+ }
+ public bool Repair
+ {
+ get { return (MaskFlags & InteractionMaskFlags.IsRepair) > 0; }
+ set { MaskFlags &= ~(InteractionMaskFlags.IsRepair); if (value) MaskFlags |= InteractionMaskFlags.IsRepair; }
+ }
+ public bool AlwaysCheck
+ {
+ get { return (MaskFlags & InteractionMaskFlags.RunCheckAlways) > 0; }
+ set { MaskFlags &= ~(InteractionMaskFlags.RunCheckAlways); if (value) MaskFlags |= InteractionMaskFlags.RunCheckAlways; }
+ }
+ public bool WhenDead
+ {
+ get { return (MaskFlags & InteractionMaskFlags.AvailableWhenDead) > 0; }
+ set { MaskFlags &= ~(InteractionMaskFlags.AvailableWhenDead); if (value) MaskFlags |= InteractionMaskFlags.AvailableWhenDead; }
+ }
+
+ public void InitMotiveEntries()
+ {
+ for (int i=0; i
+ /// Represents a motive entry in a TTAB chunk.
+ ///
+ public struct TTABMotiveEntry
+ {
+ public int MotiveIndex; // don't save this
+
+ public short EffectRangeMinimum;
+ public short EffectRangeDelta;
+ public ushort PersonalityModifier;
+ }
+
+ public enum TTABFlags
+ {
+ AllowVisitors = 1, //COVERED, TODO for no TSOFlags? (default to only roomies, unless this flag set)
+ Joinable = 1 << 1, //TODO
+ RunImmediately = 1 << 2, //COVERED
+ AllowConsecutive = 1 << 3, //TODO
+
+ TS1NoChild = 1 << 4,
+ TS1NoDemoChild = 1 << 5,
+ TS1NoAdult = 1 << 6,
+
+ Debug = 1 << 7, //COVERED: only available to roomies for now
+ AutoFirstSelect = 1 << 8, //COVERED
+
+ TS1AllowCats = 1 << 9,
+ TS1AllowDogs = 1 << 10,
+
+ Leapfrog = 1 << 9, //COVERED
+ MustRun = 1 << 10, //COVERED
+ AllowDogs = 1 << 11, //COVERED
+ AllowCats = 1 << 12, //COVERED
+
+ TSOAvailableCarrying = 1 << 16, //COVERED
+ TSOIsRepair = 1 << 17, //TODO (only available when wear = 0)
+ TSORunCheckAlways = 1 << 18, //TODO
+ TSOAvailableWhenDead = 1<<19, //COVERED
+
+ FSOPushTail = 1<<30,
+ FSOPushHead = 1<<29,
+ FSOSkipPermissions = 1<<28,
+ FSODirectControl = 1<<27
+ }
+
+ public enum TSOFlags
+ {
+ NonEmpty = 1, //if this is the only flag set, flags aren't empty intentionally. force Owner, Roommates, Friends to on
+ AllowObjectOwner = 1 << 1, //COVERED
+ AllowRoommates = 1 << 2, //COVERED
+ AllowFriends = 1 << 3, //TODO
+ AllowVisitors = 1 << 4, //COVERED
+ AllowGhost = 1 << 5, //COVERED
+ UnderParentalControl = 1 << 6, //TODO: interactions always available
+ AllowCSRs = 1 << 7 //COVERED: only available to admins
+ }
+
+ public enum InteractionMaskFlags
+ {
+ AvailableWhenCarrying = 1,
+ IsRepair = 1<<1,
+ RunCheckAlways = 1 << 2,
+ AvailableWhenDead = 1 << 3,
+ }
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/TTAT.cs b/server/tso.files/Formats/IFF/Chunks/TTAT.cs
new file mode 100755
index 0000000..c318ea6
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/TTAT.cs
@@ -0,0 +1,65 @@
+using FSO.Files.Utils;
+using System.Collections.Generic;
+using System.IO;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ public class TATT : IffChunk
+ {
+ public Dictionary TypeAttributesByGUID = new Dictionary();
+
+ public override void Read(IffFile iff, Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ io.ReadUInt32(); //pad
+ var version = io.ReadUInt32(); //zero
+
+ var TTAT = io.ReadCString(4);
+
+ IOProxy iop;
+ var compressionCode = io.ReadByte();
+ //HACK: for freeso we don't run the field encoding coompression
+ //since fso neighbourhoods are not compatible with ts1, it does not matter too much
+ if (compressionCode != 1) iop = new TTABNormal(io);
+ else iop = new IffFieldEncode(io);
+
+ var total = iop.ReadInt32();
+ for (int i=0; i
+ /// Duplicate of STR chunk, instead used for pie menu strings.
+ ///
+ public class TTAs : STR
+ {
+ //no difference!
+ }
+}
diff --git a/server/tso.files/Formats/IFF/Chunks/WALm.cs b/server/tso.files/Formats/IFF/Chunks/WALm.cs
new file mode 100755
index 0000000..a7b1252
--- /dev/null
+++ b/server/tso.files/Formats/IFF/Chunks/WALm.cs
@@ -0,0 +1,61 @@
+using FSO.Files.Utils;
+using System.Collections.Generic;
+using System.IO;
+
+namespace FSO.Files.Formats.IFF.Chunks
+{
+ ///
+ /// WALm and FLRm chunks, used for mapping walls and floors in ARRY chunks to walls and floors in resource files (outwith floors.iff)
+ ///
+ public class WALm : IffChunk
+ {
+ public List Entries;
+
+ public override void Read(IffFile iff, Stream stream)
+ {
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
+ {
+ var zero = io.ReadInt32();
+ var version = io.ReadInt32(); //should be 0
+ var walm = io.ReadInt32(); //mLAW/mRLF
+
+ var count = io.ReadInt32();
+ Entries = new List();
+
+ for (int i=0; i
+ /// Interchange File Format (IFF) is a chunk-based file format for binary resource data
+ /// intended to promote a common model for store and use by an executable.
+ ///
+ public class IffFile : IFileInfoUtilizer, ITimedCachable
+ {
+ ///
+ /// Set to true to force the game to retain a copy of all chunk data at time of loading (used to generate piffs)
+ /// Should really only be set when the user wants to use the IDE, as it uses a lot more memory.
+ ///
+ public static bool RETAIN_CHUNK_DATA = false;
+ public static bool TargetTS1 = false;
+ public bool TSBO = false;
+ public bool RetainChunkData = RETAIN_CHUNK_DATA;
+ public object CachedJITModule; //for JIT and AOT modes
+ public uint ExecutableHash; //hash of BHAV and BCON chunks
+
+ public string Filename;
+
+ public static Dictionary CHUNK_TYPES = new Dictionary()
+ {
+ {"STR#", typeof(STR)},
+ {"CTSS", typeof(CTSS)},
+ {"PALT", typeof(PALT)},
+ {"OBJD", typeof(OBJD)},
+ {"DGRP", typeof(DGRP)},
+ {"SPR#", typeof(SPR)},
+ {"SPR2", typeof(SPR2)},
+ {"BHAV", typeof(BHAV)},
+ {"TPRP", typeof(TPRP)},
+ {"SLOT", typeof(SLOT)},
+ {"GLOB", typeof(GLOB)},
+ {"BCON", typeof(BCON)},
+ {"TTAB", typeof(TTAB)},
+ {"OBJf", typeof(OBJf)},
+ {"TTAs", typeof(TTAs)},
+ {"FWAV", typeof(FWAV)},
+ {"BMP_", typeof(BMP)},
+ {"PIFF", typeof(PIFF) },
+ {"TRCN", typeof(TRCN) },
+
+ {"objt", typeof(OBJT) },
+ {"Arry", typeof(ARRY) },
+ {"ObjM", typeof(OBJM) },
+ {"WALm", typeof(WALm) },
+ {"FLRm", typeof(FLRm) },
+ {"CARR", typeof(CARR) },
+
+ {"NBRS", typeof(NBRS) },
+ {"FAMI", typeof(FAMI) },
+ {"NGBH", typeof(NGBH) },
+ {"FAMs", typeof(FAMs) },
+ {"THMB", typeof(THMB) },
+ {"SIMI", typeof(SIMI) },
+ {"TATT", typeof(TATT) },
+ {"HOUS", typeof(HOUS) },
+ //todo: FAMh (family motives ("family house"?)) field encoded.
+
+ {"TREE", typeof(TREE) },
+ {"FCNS", typeof(FCNS) },
+
+ {"FSOR", typeof(FSOR) },
+ {"FSOM", typeof(FSOM) },
+ {"MTEX", typeof(MTEX) },
+ {"FSOV", typeof(FSOV) },
+ {"PNG_", typeof(PNG) }
+ };
+
+ public IffRuntimeInfo RuntimeInfo = new IffRuntimeInfo();
+ private Dictionary> ByChunkId;
+ private Dictionary> ByChunkType;
+ public List RemovedOriginal = new List();
+ public PIFF CurrentPIFF;
+
+ ///
+ /// Constructs a new IFF instance.
+ ///
+ public IffFile()
+ {
+ ByChunkId = new Dictionary>();
+ ByChunkType = new Dictionary>();
+ }
+
+ ///
+ /// Constructs an IFF instance from a filepath.
+ ///
+ /// Path to the IFF.
+ public IffFile(string filepath) : this()
+ {
+ using (var stream = File.Open(filepath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
+ {
+ this.Read(stream);
+ SetFilename(Path.GetFileName(filepath));
+ }
+ }
+
+ ///
+ /// Constructs an IFF instance from a filepath.
+ ///
+ /// Path to the IFF.
+ public IffFile(string filepath, bool retainData) : this()
+ {
+ RetainChunkData = retainData;
+ using (var stream = File.Open(filepath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
+ {
+ this.Read(stream);
+ SetFilename(Path.GetFileName(filepath));
+ }
+ }
+
+
+ private bool WasReferenced = true;
+ ~IffFile()
+ {
+ if (WasReferenced)
+ {
+ TimedReferenceController.KeepAlive(this, KeepAliveType.DEREFERENCED);
+ WasReferenced = false;
+ GC.ReRegisterForFinalize(this);
+ } else
+ {
+ var all = SilentListAll();
+ foreach (var chunk in all)
+ {
+ chunk.Dispose();
+ }
+ }
+ }
+ public void Rereferenced(bool saved)
+ {
+ WasReferenced = saved;
+ }
+
+ public void MarkThrowaway()
+ {
+ WasReferenced = false;
+ }
+
+ ///
+ /// Reads an IFF from a stream.
+ ///
+ /// The stream to read from.
+ public void Read(Stream stream)
+ {
+
+ using (var io = IoBuffer.FromStream(stream, ByteOrder.BIG_ENDIAN))
+ {
+ var identifier = io.ReadCString(60, false).Replace("\0", "");
+ if (identifier != "IFF FILE 2.5:TYPE FOLLOWED BY SIZE JAMIE DOORNBOS & MAXIS 1")
+ {
+ if (identifier != "IFF FILE 2.0:TYPE FOLLOWED BY SIZE JAMIE DOORNBOS & MAXIS 1") //house11.iff, seems to read fine
+ throw new Exception("Invalid iff file!");
+ }
+
+ var rsmpOffset = io.ReadUInt32();
+
+ while (io.HasMore)
+ {
+ var newChunk = AddChunk(stream, io, true);
+ }
+ }
+ }
+
+ public void InitHash()
+ {
+ if (ExecutableHash != 0) return;
+ if (ByChunkType.ContainsKey(typeof(BHAV)))
+ {
+ IEnumerable