mirror of
https://github.com/simtactics/mysimulation.git
synced 2025-07-04 21:50:35 -04:00
Added FSO.Files for use with the API server
Don't ask me. FreeSO is the prime example of dependency hell.
This commit is contained in:
parent
4b5e584eeb
commit
8fec258215
104 changed files with 14653 additions and 163 deletions
60
server/tso.files/Formats/DBPF/DBPFEntry.cs
Executable file
60
server/tso.files/Formats/DBPF/DBPFEntry.cs
Executable file
|
@ -0,0 +1,60 @@
|
|||
namespace FSO.Files.Formats.DBPF
|
||||
{
|
||||
/// <summary>
|
||||
/// GroupIDs for DBPF archives, defined in sys\\tsosounddata.ini
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TypeIDs for DBPF archives, defined in sys\\tsoaudio.ini
|
||||
/// </summary>
|
||||
public enum DBPFTypeID : uint
|
||||
{
|
||||
XA = 0x1d07eb4b,
|
||||
UTK = 0x1b6b9806,
|
||||
WAV = 0xbb7051f5,
|
||||
MP3 = 0x3cec2b47,
|
||||
TRK = 0x5D73A611,
|
||||
HIT = 0x7b1acfcd,
|
||||
SoundFX = 0x2026960b,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an entry in a DBPF archive.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
184
server/tso.files/Formats/DBPF/DBPFFile.cs
Executable file
184
server/tso.files/Formats/DBPF/DBPFFile.cs
Executable file
|
@ -0,0 +1,184 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using FSO.Files.Utils;
|
||||
|
||||
namespace FSO.Files.Formats.DBPF
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class DBPFFile : IDisposable
|
||||
{
|
||||
public int DateCreated;
|
||||
public int DateModified;
|
||||
|
||||
private uint IndexMajorVersion;
|
||||
private uint NumEntries;
|
||||
private IoBuffer m_Reader;
|
||||
|
||||
private List<DBPFEntry> m_EntriesList = new List<DBPFEntry>();
|
||||
private Dictionary<ulong, DBPFEntry> m_EntryByID = new Dictionary<ulong, DBPFEntry>();
|
||||
private Dictionary<DBPFTypeID, List<DBPFEntry>> m_EntriesByType = new Dictionary<DBPFTypeID, List<DBPFEntry>>();
|
||||
|
||||
private IoBuffer Io;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new DBPF instance.
|
||||
/// </summary>
|
||||
public DBPFFile()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a DBPF instance from a path.
|
||||
/// </summary>
|
||||
/// <param name="file">The path to an DBPF archive.</param>
|
||||
public DBPFFile(string file)
|
||||
{
|
||||
var stream = File.OpenRead(file);
|
||||
Read(stream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a DBPF archive from a stream.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to read from.</param>
|
||||
public void Read(Stream stream)
|
||||
{
|
||||
m_EntryByID = new Dictionary<ulong,DBPFEntry>();
|
||||
m_EntriesList = new List<DBPFEntry>();
|
||||
|
||||
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<DBPFEntry>());
|
||||
|
||||
m_EntriesByType[entry.TypeID].Add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a DBPFEntry's data from this DBPF instance.
|
||||
/// </summary>
|
||||
/// <param name="entry">Entry to retrieve data for.</param>
|
||||
/// <returns>Data for entry.</returns>
|
||||
public byte[] GetEntry(DBPFEntry entry)
|
||||
{
|
||||
m_Reader.Seek(SeekOrigin.Begin, entry.FileOffset);
|
||||
|
||||
return m_Reader.ReadBytes((int)entry.FileSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an entry from its ID (TypeID + FileID).
|
||||
/// </summary>
|
||||
/// <param name="ID">The ID of the entry.</param>
|
||||
/// <returns>The entry's data.</returns>
|
||||
public byte[] GetItemByID(ulong ID)
|
||||
{
|
||||
if (m_EntryByID.ContainsKey(ID))
|
||||
return GetEntry(m_EntryByID[ID]);
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all entries of a specific type.
|
||||
/// </summary>
|
||||
/// <param name="Type">The Type of the entry.</param>
|
||||
/// <returns>The entry data, paired with its instance id.</returns>
|
||||
public List<KeyValuePair<uint, byte[]>> GetItemsByType(DBPFTypeID Type)
|
||||
{
|
||||
|
||||
var result = new List<KeyValuePair<uint, byte[]>>();
|
||||
|
||||
var entries = m_EntriesByType[Type];
|
||||
for (int i = 0; i < entries.Count; i++)
|
||||
{
|
||||
result.Add(new KeyValuePair<uint, byte[]>(entries[i].InstanceID, GetEntry(entries[i])));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
#region IDisposable Members
|
||||
|
||||
/// <summary>
|
||||
/// Disposes this DBPF instance.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Io.Dispose();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
51
server/tso.files/Formats/IFF/AbstractIffChunk.cs
Executable file
51
server/tso.files/Formats/IFF/AbstractIffChunk.cs
Executable file
|
@ -0,0 +1,51 @@
|
|||
using System.IO;
|
||||
|
||||
namespace FSO.Files.Formats.IFF
|
||||
{
|
||||
/// <summary>
|
||||
/// An IFF is made up of chunks.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Reads this chunk from an IFF.
|
||||
/// </summary>
|
||||
/// <param name="iff">The IFF to read from.</param>
|
||||
/// <param name="stream">The stream to read from.</param>
|
||||
public abstract void Read(IffFile iff, Stream stream);
|
||||
|
||||
/// <summary>
|
||||
/// The name of this chunk.
|
||||
/// </summary>
|
||||
/// <returns>The name of this chunk as a string.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return "#" + ChunkID.ToString() + " " + ChunkLabel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to write this chunk to a stream (presumably an IFF or PIFF)
|
||||
/// </summary>
|
||||
/// <param name="iff"></param>
|
||||
/// <param name="stream"></param>
|
||||
/// <returns>True if data has been written, false if not. </returns>
|
||||
public virtual bool Write(IffFile iff, Stream stream) { return false; }
|
||||
|
||||
public virtual void Dispose() { }
|
||||
|
||||
}
|
||||
}
|
10
server/tso.files/Formats/IFF/ChunkRuntimeInfo.cs
Executable file
10
server/tso.files/Formats/IFF/ChunkRuntimeInfo.cs
Executable file
|
@ -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.
|
||||
}
|
||||
}
|
142
server/tso.files/Formats/IFF/Chunks/ARRY.cs
Executable file
142
server/tso.files/Formats/IFF/Chunks/ARRY.cs
Executable file
|
@ -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<flre; i++)
|
||||
{
|
||||
if (!io.HasMore) return;
|
||||
Data[currentPosition++] = io.ReadByte();
|
||||
if (currentPosition > 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)"
|
||||
}
|
||||
}
|
50
server/tso.files/Formats/IFF/Chunks/BCON.cs
Executable file
50
server/tso.files/Formats/IFF/Chunks/BCON.cs
Executable file
|
@ -0,0 +1,50 @@
|
|||
using System.IO;
|
||||
using FSO.Files.Utils;
|
||||
|
||||
namespace FSO.Files.Formats.IFF.Chunks
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class BCON : IffChunk
|
||||
{
|
||||
public byte Flags;
|
||||
public ushort[] Constants = new ushort[0];
|
||||
|
||||
/// <summary>
|
||||
/// Reads a BCON chunk from a stream.
|
||||
/// </summary>
|
||||
/// <param name="iff">An Iff instance.</param>
|
||||
/// <param name="stream">A Stream instance holding a BCON.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
128
server/tso.files/Formats/IFF/Chunks/BHAV.cs
Executable file
128
server/tso.files/Formats/IFF/Chunks/BHAV.cs
Executable file
|
@ -0,0 +1,128 @@
|
|||
using System.IO;
|
||||
using FSO.Files.Utils;
|
||||
|
||||
namespace FSO.Files.Formats.IFF.Chunks
|
||||
{
|
||||
/// <summary>
|
||||
/// This chunk type holds Behavior code in SimAntics.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Reads a BHAV from a stream.
|
||||
/// </summary>
|
||||
/// <param name="iff">Iff instance.</param>
|
||||
/// <param name="stream">A Stream instance holding a BHAV chunk.</param>
|
||||
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
|
||||
}
|
||||
}
|
39
server/tso.files/Formats/IFF/Chunks/BMP.cs
Executable file
39
server/tso.files/Formats/IFF/Chunks/BMP.cs
Executable file
|
@ -0,0 +1,39 @@
|
|||
using System.IO;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
|
||||
namespace FSO.Files.Formats.IFF.Chunks
|
||||
{
|
||||
/// <summary>
|
||||
/// This chunk type holds an image in BMP format.
|
||||
/// </summary>
|
||||
public class BMP : IffChunk
|
||||
{
|
||||
public byte[] data;
|
||||
|
||||
/// <summary>
|
||||
/// Reads a BMP chunk from a stream.
|
||||
/// </summary>
|
||||
/// <param name="iff">An Iff instance.</param>
|
||||
/// <param name="stream">A Stream object holding a BMP chunk.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
98
server/tso.files/Formats/IFF/Chunks/CARR.cs
Executable file
98
server/tso.files/Formats/IFF/Chunks/CARR.cs
Executable file
|
@ -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<numLevels; i++)
|
||||
{
|
||||
JobLevels[i] = new JobLevel(iop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int GetJobData(int level, int data)
|
||||
{
|
||||
var entry = JobLevels[level];
|
||||
switch (data)
|
||||
{
|
||||
case 0: //number of levels
|
||||
return JobLevels.Length;
|
||||
case 1: //salary
|
||||
return entry.Salary;
|
||||
case 12: //start hour
|
||||
return entry.StartTime;
|
||||
case 13:
|
||||
return entry.EndTime;
|
||||
case 21:
|
||||
return entry.CarType;
|
||||
case 22:
|
||||
return 0;
|
||||
default:
|
||||
if (data < 12)
|
||||
return entry.MinRequired[data-2];
|
||||
else
|
||||
return entry.MotiveDelta[data-14];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class JobLevel
|
||||
{
|
||||
public int[] MinRequired = new int[10]; //friends, then skills.
|
||||
public int[] MotiveDelta = new int[7];
|
||||
public int Salary;
|
||||
public int StartTime;
|
||||
public int EndTime;
|
||||
public int CarType;
|
||||
|
||||
public string JobName;
|
||||
public string MaleUniformMesh;
|
||||
public string FemaleUniformMesh;
|
||||
public string UniformSkin;
|
||||
public string unknown;
|
||||
|
||||
public JobLevel(IffFieldEncode iop)
|
||||
{
|
||||
for (int i=0; i<MinRequired.Length; i++)
|
||||
MinRequired[i] = iop.ReadInt32();
|
||||
for (int i = 0; i < MotiveDelta.Length; i++)
|
||||
MotiveDelta[i] = iop.ReadInt32();
|
||||
Salary = iop.ReadInt32();
|
||||
StartTime = iop.ReadInt32();
|
||||
EndTime = iop.ReadInt32();
|
||||
CarType = iop.ReadInt32();
|
||||
|
||||
JobName = iop.ReadString(false);
|
||||
MaleUniformMesh = iop.ReadString(false);
|
||||
FemaleUniformMesh = iop.ReadString(false);
|
||||
UniformSkin = iop.ReadString(false);
|
||||
unknown = iop.ReadString(true);
|
||||
}
|
||||
}
|
||||
}
|
9
server/tso.files/Formats/IFF/Chunks/CTSS.cs
Executable file
9
server/tso.files/Formats/IFF/Chunks/CTSS.cs
Executable file
|
@ -0,0 +1,9 @@
|
|||
namespace FSO.Files.Formats.IFF.Chunks
|
||||
{
|
||||
/// <summary>
|
||||
/// Catalog text strings; equivalent in format to STR#.
|
||||
/// </summary>
|
||||
public class CTSS : STR
|
||||
{
|
||||
}
|
||||
}
|
324
server/tso.files/Formats/IFF/Chunks/DGRP.cs
Executable file
324
server/tso.files/Formats/IFF/Chunks/DGRP.cs
Executable file
|
@ -0,0 +1,324 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using FSO.Files.Utils;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace FSO.Files.Formats.IFF.Chunks
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class DGRP : IffChunk
|
||||
{
|
||||
public DGRPImage[] Images { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a DGRPImage instance from this DGRP instance.
|
||||
/// </summary>
|
||||
/// <param name="direction">The direction the DGRP is facing.</param>
|
||||
/// <param name="zoom">Zoom level DGRP is drawn at.</param>
|
||||
/// <param name="worldRotation">Current rotation of world.</param>
|
||||
/// <returns>A DGRPImage instance.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a DGRP from a stream instance.
|
||||
/// </summary>
|
||||
/// <param name="iff">An Iff instance.</param>
|
||||
/// <param name="stream">A Stream instance holding a DGRP chunk.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A DGRP is made up of multiple DGRPImages,
|
||||
/// which are made up of multiple DGRPSprites.
|
||||
/// </summary>
|
||||
public class DGRPImage
|
||||
{
|
||||
private DGRP Parent;
|
||||
public uint Direction;
|
||||
public uint Zoom;
|
||||
public DGRPSprite[] Sprites;
|
||||
|
||||
public DGRPImage(DGRP parent)
|
||||
{
|
||||
this.Parent = parent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a DGRPImage from a stream.
|
||||
/// </summary>
|
||||
/// <param name="iff">An Iff instance.</param>
|
||||
/// <param name="stream">A Stream object holding a DGRPImage.</param>
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes up a DGRPImage.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a DGRPSprite from a stream.
|
||||
/// </summary>
|
||||
/// <param name="iff">An Iff instance.</param>
|
||||
/// <param name="stream">A Stream object holding a DGRPSprite.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets position of this sprite.
|
||||
/// </summary>
|
||||
/// <returns>A Vector2 instance holding position of this sprite.</returns>
|
||||
public Vector2 GetPosition()
|
||||
{
|
||||
var iff = Parent.ChunkParent;
|
||||
var spr2 = iff.Get<SPR2>((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<SPR2>((ushort)this.SpriteID);
|
||||
if (spr2 != null){
|
||||
return spr2.Frames[this.SpriteFrameIndex].GetTexture(device);
|
||||
}
|
||||
var spr1 = iff.Get<SPR>((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<SPR2>((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<SPR2>((ushort)this.SpriteID);
|
||||
if (spr2 != null)
|
||||
{
|
||||
return spr2.Frames[this.SpriteFrameIndex].GetWorldTexture(device);
|
||||
}
|
||||
var spr1 = iff.Get<SPR>((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<SPR2>((ushort)this.SpriteID);
|
||||
if (spr2 != null)
|
||||
{
|
||||
var frame = spr2.Frames[this.SpriteFrameIndex];
|
||||
return new Point(frame.Width, frame.Height);
|
||||
}
|
||||
var spr1 = iff.Get<SPR>((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
|
||||
}
|
||||
}
|
102
server/tso.files/Formats/IFF/Chunks/FAMI.cs
Executable file
102
server/tso.files/Formats/IFF/Chunks/FAMI.cs
Executable file
|
@ -0,0 +1,102 @@
|
|||
using FSO.Files.Utils;
|
||||
using System.IO;
|
||||
|
||||
namespace FSO.Files.Formats.IFF.Chunks
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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 };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a FAMI chunk from a stream.
|
||||
/// </summary>
|
||||
/// <param name="iff">An Iff instance.</param>
|
||||
/// <param name="stream">A Stream object holding a OBJf chunk.</param>
|
||||
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<FamilyGUIDs.Length; i++)
|
||||
{
|
||||
FamilyGUIDs[i] = io.ReadUInt32();
|
||||
}
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < 4; i++)
|
||||
io.ReadInt32();
|
||||
} catch
|
||||
{
|
||||
//for some reason FAMI "Default" only has 3 zeroes after it, but only if saved by base game.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Write(IffFile iff, Stream stream)
|
||||
{
|
||||
using (var io = IoWriter.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
|
||||
{
|
||||
io.WriteInt32(0);
|
||||
io.WriteUInt32(9);
|
||||
io.WriteCString("IMAF", 4);
|
||||
io.WriteInt32(HouseNumber);
|
||||
io.WriteInt32(FamilyNumber);
|
||||
io.WriteInt32(Budget);
|
||||
io.WriteInt32(ValueInArch);
|
||||
io.WriteInt32(FamilyFriends);
|
||||
io.WriteInt32(Unknown);
|
||||
io.WriteInt32(FamilyGUIDs.Length);
|
||||
foreach (var guid in FamilyGUIDs)
|
||||
io.WriteUInt32(guid);
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
io.WriteInt32(0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
6
server/tso.files/Formats/IFF/Chunks/FAMs.cs
Executable file
6
server/tso.files/Formats/IFF/Chunks/FAMs.cs
Executable file
|
@ -0,0 +1,6 @@
|
|||
namespace FSO.Files.Formats.IFF.Chunks
|
||||
{
|
||||
public class FAMs : STR
|
||||
{
|
||||
}
|
||||
}
|
56
server/tso.files/Formats/IFF/Chunks/FCNS.cs
Executable file
56
server/tso.files/Formats/IFF/Chunks/FCNS.cs
Executable file
|
@ -0,0 +1,56 @@
|
|||
using FSO.Files.Utils;
|
||||
using System.IO;
|
||||
|
||||
namespace FSO.Files.Formats.IFF.Chunks
|
||||
{
|
||||
/// <summary>
|
||||
/// Duplicate of STR chunk, instead used for simulator constants.
|
||||
/// </summary>
|
||||
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<count; i++)
|
||||
{
|
||||
string name, desc;
|
||||
float value;
|
||||
if (version == 2)
|
||||
{
|
||||
name = io.ReadVariableLengthPascalString();
|
||||
value = io.ReadFloat();
|
||||
desc = io.ReadVariableLengthPascalString();
|
||||
}
|
||||
else
|
||||
{
|
||||
name = io.ReadNullTerminatedString();
|
||||
if (name.Length % 2 == 0) io.ReadByte(); //padding to 2 byte align
|
||||
value = io.ReadFloat();
|
||||
desc = io.ReadNullTerminatedString();
|
||||
if (desc.Length % 2 == 0) io.ReadByte(); //padding to 2 byte align
|
||||
}
|
||||
|
||||
LanguageSets[0].Strings[i] = new STRItem()
|
||||
{
|
||||
Value = name + ": " + value,
|
||||
Comment = desc
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Write(IffFile iff, Stream stream)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
57
server/tso.files/Formats/IFF/Chunks/FSOM.cs
Executable file
57
server/tso.files/Formats/IFF/Chunks/FSOM.cs
Executable file
|
@ -0,0 +1,57 @@
|
|||
using FSO.Files.RC;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
|
||||
namespace FSO.Files.Formats.IFF.Chunks
|
||||
{
|
||||
/// <summary>
|
||||
/// Iff chunk wrapper for an FSOM file.
|
||||
/// </summary>
|
||||
public class FSOM : IffChunk
|
||||
{
|
||||
private byte[] data;
|
||||
private DGRP3DMesh Cached;
|
||||
|
||||
/// <summary>
|
||||
/// Reads a BMP chunk from a stream.
|
||||
/// </summary>
|
||||
/// <param name="iff">An Iff instance.</param>
|
||||
/// <param name="stream">A Stream object holding a BMP chunk.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
35
server/tso.files/Formats/IFF/Chunks/FSOR.cs
Executable file
35
server/tso.files/Formats/IFF/Chunks/FSOR.cs
Executable file
|
@ -0,0 +1,35 @@
|
|||
using FSO.Files.RC;
|
||||
using FSO.Files.Utils;
|
||||
using System.IO;
|
||||
|
||||
namespace FSO.Files.Formats.IFF.Chunks
|
||||
{
|
||||
/// <summary>
|
||||
/// Metadata for an object's mesh reconstruction. Currently only supports file-wise parameters.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
36
server/tso.files/Formats/IFF/Chunks/FSOV.cs
Executable file
36
server/tso.files/Formats/IFF/Chunks/FSOV.cs
Executable file
|
@ -0,0 +1,36 @@
|
|||
using FSO.Files.Utils;
|
||||
using System.IO;
|
||||
|
||||
namespace FSO.Files.Formats.IFF.Chunks
|
||||
{
|
||||
/// <summary>
|
||||
/// A simple container for FSOV data within an iff. If this exists, normal TS1 iff loading is subverted.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
35
server/tso.files/Formats/IFF/Chunks/FWAV.cs
Executable file
35
server/tso.files/Formats/IFF/Chunks/FWAV.cs
Executable file
|
@ -0,0 +1,35 @@
|
|||
using System.IO;
|
||||
using FSO.Files.Utils;
|
||||
|
||||
namespace FSO.Files.Formats.IFF.Chunks
|
||||
{
|
||||
/// <summary>
|
||||
/// This chunk type holds the name of a sound event that this object uses.
|
||||
/// </summary>
|
||||
public class FWAV : IffChunk
|
||||
{
|
||||
public string Name;
|
||||
|
||||
/// <summary>
|
||||
/// Reads a FWAV chunk from a stream.
|
||||
/// </summary>
|
||||
/// <param name="iff">An Iff instance.</param>
|
||||
/// <param name="stream">A Stream object holding a FWAV chunk.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
52
server/tso.files/Formats/IFF/Chunks/GLOB.cs
Executable file
52
server/tso.files/Formats/IFF/Chunks/GLOB.cs
Executable file
|
@ -0,0 +1,52 @@
|
|||
using System.Text;
|
||||
using System.IO;
|
||||
using FSO.Files.Utils;
|
||||
|
||||
namespace FSO.Files.Formats.IFF.Chunks
|
||||
{
|
||||
/// <summary>
|
||||
/// This chunk type holds the filename of a semi-global iff file used by this object.
|
||||
/// </summary>
|
||||
public class GLOB : IffChunk
|
||||
{
|
||||
public string Name;
|
||||
|
||||
/// <summary>
|
||||
/// Reads a GLOB chunk from a stream.
|
||||
/// </summary>
|
||||
/// <param name="iff">An Iff instance.</param>
|
||||
/// <param name="stream">A Stream object holding a GLOB chunk.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
38
server/tso.files/Formats/IFF/Chunks/HOUS.cs
Executable file
38
server/tso.files/Formats/IFF/Chunks/HOUS.cs
Executable file
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
server/tso.files/Formats/IFF/Chunks/ISPR.cs
Executable file
18
server/tso.files/Formats/IFF/Chunks/ISPR.cs
Executable file
|
@ -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
|
||||
{
|
||||
|
||||
}
|
||||
}
|
152
server/tso.files/Formats/IFF/Chunks/IffFieldEncode.cs
Executable file
152
server/tso.files/Formats/IFF/Chunks/IffFieldEncode.cs
Executable file
|
@ -0,0 +1,152 @@
|
|||
using FSO.Files.Utils;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace FSO.Files.Formats.IFF.Chunks
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to read values from field encoded stream.
|
||||
/// </summary>
|
||||
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<long, int> DebugReadField(bool big)
|
||||
{
|
||||
if (ReadBit() == 0) return new Tuple<long, int>(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<long, int>(value, width);
|
||||
}
|
||||
|
||||
public Tuple<byte, byte, bool, long> MarkStream()
|
||||
{
|
||||
return new Tuple<byte, byte, bool, long>(bitPos, curByte, odd, io.Position);
|
||||
}
|
||||
|
||||
public void RevertToMark(Tuple<byte, byte, bool, long> 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;
|
||||
}
|
||||
}
|
||||
}
|
59
server/tso.files/Formats/IFF/Chunks/MTEX.cs
Executable file
59
server/tso.files/Formats/IFF/Chunks/MTEX.cs
Executable file
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Texture for a 3D Mesh. Can be jpg, png or bmp.
|
||||
/// </summary>
|
||||
public class MTEX : IffChunk
|
||||
{
|
||||
private byte[] data;
|
||||
private Texture2D Cached;
|
||||
|
||||
/// <summary>
|
||||
/// Reads a BMP chunk from a stream.
|
||||
/// </summary>
|
||||
/// <param name="iff">An Iff instance.</param>
|
||||
/// <param name="stream">A Stream object holding a BMP chunk.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
224
server/tso.files/Formats/IFF/Chunks/NBRS.cs
Executable file
224
server/tso.files/Formats/IFF/Chunks/NBRS.cs
Executable file
|
@ -0,0 +1,224 @@
|
|||
using FSO.Files.Utils;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace FSO.Files.Formats.IFF.Chunks
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class NBRS : IffChunk
|
||||
{
|
||||
public List<Neighbour> Entries = new List<Neighbour>();
|
||||
public Dictionary<short, Neighbour> NeighbourByID = new Dictionary<short, Neighbour>();
|
||||
public Dictionary<uint, short> DefaultNeighbourByGUID = new Dictionary<uint, short>();
|
||||
|
||||
public uint Version;
|
||||
|
||||
/// <summary>
|
||||
/// Reads a NBRS chunk from a stream.
|
||||
/// </summary>
|
||||
/// <param name="iff">An Iff instance.</param>
|
||||
/// <param name="stream">A Stream object holding a NBRS chunk.</param>
|
||||
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<count; i++)
|
||||
{
|
||||
if (!io.HasMore) break;
|
||||
var neigh = new Neighbour(io);
|
||||
Entries.Add(neigh);
|
||||
if (neigh.Unknown1 > 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a NBRS chunk to a stream.
|
||||
/// </summary>
|
||||
/// <param name="iff">An Iff instance.</param>
|
||||
/// <param name="stream">A destination stream.</param>
|
||||
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<int, List<short>> 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<size; i+=2)
|
||||
{
|
||||
if (pdi >= 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<int, List<short>>();
|
||||
for (int i=0; i<entries; i++)
|
||||
{
|
||||
var keyCount = io.ReadInt32();
|
||||
if (keyCount != 1) { }
|
||||
var key = io.ReadInt32();
|
||||
var values = new List<short>();
|
||||
var valueCount = io.ReadInt32();
|
||||
for (int j=0; j<valueCount; j++)
|
||||
{
|
||||
values.Add((short)io.ReadInt32());
|
||||
}
|
||||
Relationships.Add(key, values);
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
|
||||
public void Save(IoWriter io)
|
||||
{
|
||||
io.WriteInt32(Unknown1);
|
||||
io.WriteInt32(Version);
|
||||
if (Version == 0xA) io.WriteInt32(Unknown3);
|
||||
io.WriteNullTerminatedString(Name);
|
||||
if (Name.Length % 2 == 0) io.WriteByte(0);
|
||||
io.WriteInt32(MysteryZero);
|
||||
io.WriteInt32(PersonMode);
|
||||
if (PersonMode > 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
117
server/tso.files/Formats/IFF/Chunks/NGBH.cs
Executable file
117
server/tso.files/Formats/IFF/Chunks/NGBH.cs
Executable file
|
@ -0,0 +1,117 @@
|
|||
using FSO.Files.Utils;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace FSO.Files.Formats.IFF.Chunks
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class NGBH : IffChunk
|
||||
{
|
||||
public short[] NeighborhoodData = new short[16];
|
||||
public Dictionary<short, List<InventoryItem>> InventoryByID = new Dictionary<short, List<InventoryItem>>();
|
||||
|
||||
public uint Version;
|
||||
|
||||
/// <summary>
|
||||
/// Reads a NGBH chunk from a stream.
|
||||
/// </summary>
|
||||
/// <param name="iff">An Iff instance.</param>
|
||||
/// <param name="stream">A Stream object holding a OBJf chunk.</param>
|
||||
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<InventoryItem>();
|
||||
|
||||
for (int j=0; j<inventoryCount; j++)
|
||||
{
|
||||
inventory.Add(new InventoryItem(io));
|
||||
}
|
||||
InventoryByID[neighID] = inventory;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Write(IffFile iff, Stream stream)
|
||||
{
|
||||
using (var io = IoWriter.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
|
||||
{
|
||||
io.WriteInt32(0);
|
||||
io.WriteInt32(0x49);
|
||||
io.WriteCString("HBGN", 4);
|
||||
|
||||
for (int i=0; i<NeighborhoodData.Length; i++)
|
||||
{
|
||||
io.WriteInt16(NeighborhoodData[i]);
|
||||
}
|
||||
|
||||
io.WriteInt32(InventoryByID.Count);
|
||||
foreach (var item in InventoryByID)
|
||||
{
|
||||
io.WriteInt32(1);
|
||||
io.WriteInt16(item.Key);
|
||||
io.WriteInt32(item.Value.Count);
|
||||
foreach (var invent in item.Value)
|
||||
{
|
||||
invent.SerializeInto(io);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class InventoryItem
|
||||
{
|
||||
public int Type;
|
||||
public uint GUID;
|
||||
public ushort Count;
|
||||
|
||||
public InventoryItem() { }
|
||||
|
||||
public InventoryItem(IoBuffer io)
|
||||
{
|
||||
Type = io.ReadInt32();
|
||||
GUID = io.ReadUInt32();
|
||||
Count = io.ReadUInt16();
|
||||
}
|
||||
|
||||
public void SerializeInto(IoWriter io)
|
||||
{
|
||||
io.WriteInt32(Type);
|
||||
io.WriteUInt32(GUID);
|
||||
io.WriteUInt16(Count);
|
||||
}
|
||||
|
||||
public InventoryItem Clone()
|
||||
{
|
||||
return new InventoryItem() { Type = Type, GUID = GUID, Count = Count };
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return "Type: "+Type+", GUID: "+GUID+", Count: "+Count;
|
||||
}
|
||||
}
|
||||
}
|
585
server/tso.files/Formats/IFF/Chunks/OBJD.cs
Executable file
585
server/tso.files/Formats/IFF/Chunks/OBJD.cs
Executable file
|
@ -0,0 +1,585 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
using FSO.Files.Utils;
|
||||
|
||||
namespace FSO.Files.Formats.IFF.Chunks
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of OBJD.
|
||||
/// </summary>
|
||||
[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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<T>(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<int>(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<int>(prop));
|
||||
}
|
||||
for (int i = VERSION_142_Fields.Length; i < 105; i++)
|
||||
{
|
||||
io.WriteUInt16(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
138
server/tso.files/Formats/IFF/Chunks/OBJM.cs
Executable file
138
server/tso.files/Formats/IFF/Chunks/OBJM.cs
Executable file
|
@ -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<int, MappedObject> 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<test1*2; i++)
|
||||
{
|
||||
testas[i] = iop.ReadUInt16();
|
||||
}*/
|
||||
|
||||
var table = new List<ushort>();
|
||||
while (io.HasMore)
|
||||
{
|
||||
var value = iop.ReadUInt16();
|
||||
if (value == 0) break;
|
||||
table.Add(value);
|
||||
}
|
||||
IDToOBJT = table.ToArray();
|
||||
|
||||
var list = new List<short>();
|
||||
while (io.HasMore)
|
||||
{
|
||||
list.Add(iop.ReadInt16());
|
||||
}
|
||||
|
||||
var offsets = SearchForObjectData(list);
|
||||
for (int i=1; i<offsets.Count; i++)
|
||||
{
|
||||
Console.WriteLine(offsets[i] - offsets[i-1]);
|
||||
}
|
||||
|
||||
ObjectData = new Dictionary<int, MappedObject>();
|
||||
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<int> SearchForObjectData(List<short> 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<int>();
|
||||
for (int i=0; i<data.Count-3; i++)
|
||||
{
|
||||
if ((data[i] == 1997 || data[i] == 1998 || data[i] == (1997-36)) && (data[i + 1] > 0 && data[i+1] < 13) && (data[i + 2] > 0 && data[i + 2] < 32)) {
|
||||
offsets.Add(i - 45);
|
||||
}
|
||||
}
|
||||
|
||||
return offsets;
|
||||
}
|
||||
}
|
||||
}
|
64
server/tso.files/Formats/IFF/Chunks/OBJT.cs
Executable file
64
server/tso.files/Formats/IFF/Chunks/OBJT.cs
Executable file
|
@ -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<OBJTEntry> 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<OBJTEntry>();
|
||||
//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}]";
|
||||
}
|
||||
}
|
||||
}
|
59
server/tso.files/Formats/IFF/Chunks/OBJf.cs
Executable file
59
server/tso.files/Formats/IFF/Chunks/OBJf.cs
Executable file
|
@ -0,0 +1,59 @@
|
|||
using System.IO;
|
||||
using FSO.Files.Utils;
|
||||
|
||||
namespace FSO.Files.Formats.IFF.Chunks
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class OBJf : IffChunk
|
||||
{
|
||||
public OBJfFunctionEntry[] functions;
|
||||
public uint Version;
|
||||
|
||||
/// <summary>
|
||||
/// Reads a OBJf chunk from a stream.
|
||||
/// </summary>
|
||||
/// <param name="iff">An Iff instance.</param>
|
||||
/// <param name="stream">A Stream object holding a OBJf chunk.</param>
|
||||
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<functions.Length; i++) {
|
||||
var result = new OBJfFunctionEntry();
|
||||
result.ConditionFunction = io.ReadUInt16();
|
||||
result.ActionFunction = io.ReadUInt16();
|
||||
functions[i] = result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Write(IffFile iff, Stream stream)
|
||||
{
|
||||
using (var io = IoWriter.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
|
||||
{
|
||||
io.WriteUInt32(0);
|
||||
io.WriteUInt32(Version);
|
||||
io.WriteCString("fJBO", 4);
|
||||
io.WriteInt32(functions.Length);
|
||||
foreach(var func in functions)
|
||||
{
|
||||
io.WriteUInt16(func.ConditionFunction);
|
||||
io.WriteUInt16(func.ActionFunction);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct OBJfFunctionEntry {
|
||||
public ushort ConditionFunction;
|
||||
public ushort ActionFunction;
|
||||
}
|
||||
}
|
79
server/tso.files/Formats/IFF/Chunks/PALT.cs
Executable file
79
server/tso.files/Formats/IFF/Chunks/PALT.cs
Executable file
|
@ -0,0 +1,79 @@
|
|||
using System.IO;
|
||||
using FSO.Files.Utils;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace FSO.Files.Formats.IFF.Chunks
|
||||
{
|
||||
/// <summary>
|
||||
/// This chunk type holds a color palette.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Reads a PALT chunk from a stream.
|
||||
/// </summary>
|
||||
/// <param name="iff">An Iff instance.</param>
|
||||
/// <param name="stream">A Stream object holding a PALT chunk.</param>
|
||||
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<Colors.Length; i++)
|
||||
{
|
||||
if (i >= data.Length) return true;
|
||||
if (data[i].A != 0 && data[i] != Colors[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
140
server/tso.files/Formats/IFF/Chunks/PART.cs
Executable file
140
server/tso.files/Formats/IFF/Chunks/PART.cs
Executable file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
187
server/tso.files/Formats/IFF/Chunks/PIFF.cs
Executable file
187
server/tso.files/Formats/IFF/Chunks/PIFF.cs
Executable file
|
@ -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<Entries.Length; i++)
|
||||
{
|
||||
var e = new PIFFEntry();
|
||||
e.Type = io.ReadCString(4);
|
||||
e.ChunkID = io.ReadUInt16();
|
||||
if (Version > 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<e.Patches.Length; j++)
|
||||
{
|
||||
var p = new PIFFPatch();
|
||||
|
||||
p.Offset = lastOff + io.ReadVarLen();
|
||||
lastOff = p.Offset;
|
||||
p.Size = io.ReadVarLen();
|
||||
p.Mode = (PIFFPatchMode)io.ReadByte();
|
||||
|
||||
if (p.Mode == PIFFPatchMode.Add) p.Data = io.ReadBytes(p.Size);
|
||||
e.Patches[j] = p;
|
||||
}
|
||||
}
|
||||
Entries[i] = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Write(IffFile iff, Stream stream)
|
||||
{
|
||||
using (var io = IoWriter.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
|
||||
{
|
||||
io.WriteUInt16(CURRENT_VERSION);
|
||||
io.WriteVariableLengthPascalString(SourceIff);
|
||||
io.WriteVariableLengthPascalString(Comment);
|
||||
io.WriteUInt16((ushort)Entries.Length);
|
||||
foreach (var ent in Entries)
|
||||
{
|
||||
io.WriteCString(ent.Type, 4);
|
||||
io.WriteUInt16(ent.ChunkID);
|
||||
io.WriteVariableLengthPascalString(ent.Comment);
|
||||
io.WriteByte((byte)(ent.EntryType));
|
||||
|
||||
if (ent.EntryType == PIFFEntryType.Patch)
|
||||
{
|
||||
io.WriteVariableLengthPascalString(ent.ChunkLabel); //0 length means no replacement
|
||||
io.WriteUInt16(ent.ChunkFlags);
|
||||
io.WriteUInt16(ent.NewChunkID);
|
||||
io.WriteUInt32(ent.NewDataSize);
|
||||
io.WriteUInt32((uint)ent.Patches.Length);
|
||||
|
||||
uint lastOff = 0;
|
||||
foreach (var p in ent.Patches)
|
||||
{
|
||||
io.WriteVarLen(p.Offset-lastOff);
|
||||
lastOff = p.Offset;
|
||||
io.WriteVarLen(p.Size);
|
||||
io.WriteByte((byte)p.Mode);
|
||||
if (p.Mode == PIFFPatchMode.Add) io.WriteBytes(p.Data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class PIFFEntry
|
||||
{
|
||||
public string Type;
|
||||
public ushort ChunkID;
|
||||
public ushort NewChunkID;
|
||||
public PIFFEntryType EntryType;
|
||||
public string Comment = "";
|
||||
|
||||
public string ChunkLabel;
|
||||
public ushort ChunkFlags;
|
||||
public uint NewDataSize;
|
||||
|
||||
public PIFFPatch[] Patches;
|
||||
|
||||
public byte[] Apply(byte[] src)
|
||||
{
|
||||
var result = new byte[NewDataSize];
|
||||
uint srcPtr = 0;
|
||||
uint destPtr = 0;
|
||||
int i = 0;
|
||||
foreach (var p in Patches)
|
||||
{
|
||||
var copyCount = p.Offset - destPtr;
|
||||
Array.Copy(src, srcPtr, result, destPtr, copyCount);
|
||||
srcPtr += copyCount; destPtr += copyCount;
|
||||
if (p.Mode == PIFFPatchMode.Add)
|
||||
{
|
||||
Array.Copy(p.Data, 0, result, destPtr, p.Size);
|
||||
destPtr += p.Size;
|
||||
} else
|
||||
{
|
||||
srcPtr += p.Size;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
var remainder = NewDataSize - destPtr;
|
||||
if (remainder != 0) Array.Copy(src, srcPtr, result, destPtr, remainder);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public enum PIFFEntryType : byte
|
||||
{
|
||||
Patch,
|
||||
Remove,
|
||||
Add
|
||||
}
|
||||
|
||||
public struct PIFFPatch
|
||||
{
|
||||
public uint Offset;
|
||||
public uint Size;
|
||||
public PIFFPatchMode Mode;
|
||||
public byte[] Data;
|
||||
}
|
||||
|
||||
public enum PIFFPatchMode : byte
|
||||
{
|
||||
Remove = 0,
|
||||
Add = 1
|
||||
}
|
||||
}
|
10
server/tso.files/Formats/IFF/Chunks/PNG.cs
Executable file
10
server/tso.files/Formats/IFF/Chunks/PNG.cs
Executable file
|
@ -0,0 +1,10 @@
|
|||
namespace FSO.Files.Formats.IFF.Chunks
|
||||
{
|
||||
/// <summary>
|
||||
/// This chunk type holds an image in PNG format.
|
||||
/// </summary>
|
||||
public class PNG : BMP
|
||||
{
|
||||
}
|
||||
|
||||
}
|
165
server/tso.files/Formats/IFF/Chunks/SIMI.cs
Executable file
165
server/tso.files/Formats/IFF/Chunks/SIMI.cs
Executable file
|
@ -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<items; i++)
|
||||
{
|
||||
var dat = io.ReadInt16();
|
||||
if (i < GlobalData.Length)
|
||||
GlobalData[i] = dat;
|
||||
}
|
||||
|
||||
Unknown1 = io.ReadInt16();
|
||||
Unknown2 = io.ReadInt32();
|
||||
Unknown3 = io.ReadInt32();
|
||||
GUID1 = io.ReadInt32();
|
||||
GUID2 = io.ReadInt32();
|
||||
Unknown4 = io.ReadInt32();
|
||||
LotValue = io.ReadInt32();
|
||||
ObjectsValue = io.ReadInt32();
|
||||
ArchitectureValue = io.ReadInt32();
|
||||
|
||||
//short Unknown1 (0x7E1E, 0x702B)
|
||||
//int Unknown2 (2 on house 1, 1 on house 66)
|
||||
//int Unknown3 (0)
|
||||
//int GUID1
|
||||
//int GUID2 (changes on bulldoze)
|
||||
//int Unknown4 (0)
|
||||
//int LotValue
|
||||
//int ObjectsValue
|
||||
//int ArchitectureValue
|
||||
|
||||
//the sims tracked a sim's budget over the past few days of gameplay.
|
||||
//this drove the budget window, which never actually came of much use to anyone ever.
|
||||
|
||||
BudgetDays = new SIMIBudgetDay[6];
|
||||
for (int i=0; i<6; i++)
|
||||
{
|
||||
BudgetDays[i] = new SIMIBudgetDay(io);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Write(IffFile iff, Stream stream)
|
||||
{
|
||||
using (var io = IoWriter.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
|
||||
{
|
||||
io.WriteInt32(0);
|
||||
io.WriteInt32(0x3E);
|
||||
io.WriteCString("IMIS", 4);
|
||||
var items = (Version > 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
218
server/tso.files/Formats/IFF/Chunks/SLOT.cs
Executable file
218
server/tso.files/Formats/IFF/Chunks/SLOT.cs
Executable file
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// This format isn't documented on the wiki! Thanks, Darren!
|
||||
/// </summary>
|
||||
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<ushort, List<SLOTItem>> Slots = new Dictionary<ushort, List<SLOTItem>>();
|
||||
public List<SLOTItem> Chronological = new List<SLOTItem>();
|
||||
|
||||
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<SLOTItem>());
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
299
server/tso.files/Formats/IFF/Chunks/SPR.cs
Executable file
299
server/tso.files/Formats/IFF/Chunks/SPR.cs
Executable file
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public class SPR : IffChunk
|
||||
{
|
||||
public List<SPRFrame> Frames { get; internal set; }
|
||||
public ushort PaletteID;
|
||||
private List<uint> Offsets;
|
||||
public ByteOrder ByteOrd;
|
||||
public bool WallStyle;
|
||||
|
||||
/// <summary>
|
||||
/// Reads a SPR chunk from a stream.
|
||||
/// </summary>
|
||||
/// <param name="iff">An Iff instance.</param>
|
||||
/// <param name="stream">A Stream object holding a SPR chunk.</param>
|
||||
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<SPRFrame>();
|
||||
if (version != 1001)
|
||||
{
|
||||
var offsetTable = new List<uint>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The frame (I.E sprite) of a SPR chunk.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new SPRFrame instance.
|
||||
/// </summary>
|
||||
/// <param name="parent">A SPR parent.</param>
|
||||
public SPRFrame(SPR parent)
|
||||
{
|
||||
this.Parent = parent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a SPRFrame from a stream.
|
||||
/// </summary>
|
||||
/// <param name="iff">An Iff instance.</param>
|
||||
/// <param name="stream">A Stream object holding a SPRFrame.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decodes this SPRFrame.
|
||||
/// </summary>
|
||||
/// <param name="io">IOBuffer used to read a SPRFrame.</param>
|
||||
private void Decode(IoBuffer io)
|
||||
{
|
||||
var palette = Parent.ChunkParent.Get<PALT>(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<byte>(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<Color>(this.Data);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
PixelCache = new Texture2D(device, Math.Max(1, Width), Math.Max(1, Height), mip, SurfaceFormat.Color);
|
||||
PixelCache.SetData<Color>(new Color[] { Color.Transparent });
|
||||
}
|
||||
|
||||
PixelCache.Tag = new TextureInfo(PixelCache, Width, Height);
|
||||
if (!IffFile.RETAIN_CHUNK_DATA) Data = null;
|
||||
}
|
||||
return PixelCache;
|
||||
}
|
||||
}
|
||||
}
|
787
server/tso.files/Formats/IFF/Chunks/SPR2.cs
Executable file
787
server/tso.files/Formats/IFF/Chunks/SPR2.cs
Executable file
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// This chunk type holds a number of paletted sprites that may have z-buffer and/or alpha channels.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a SPR2 chunk from a stream.
|
||||
/// </summary>
|
||||
/// <param name="iff">An Iff instance.</param>
|
||||
/// <param name="stream">A Stream object holding a SPR2 chunk.</param>
|
||||
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<PALT>(frame.PaletteID);
|
||||
if (palette != null) palette.References--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The frame (I.E sprite) of a SPR2 chunk.
|
||||
/// </summary>
|
||||
public class SPR2Frame : ITextureProvider, IWorldTextureProvider
|
||||
{
|
||||
public Color[] PixelData;
|
||||
public byte[] ZBufferData;
|
||||
public byte[] PalData;
|
||||
|
||||
private WeakReference<Texture2D> ZCache = new WeakReference<Texture2D>(null);
|
||||
private WeakReference<Texture2D> PixelCache = new WeakReference<Texture2D>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a SPR2 chunk from a stream.
|
||||
/// </summary>
|
||||
/// <param name="version">Version of the SPR2 that this frame belongs to.</param>
|
||||
/// <param name="stream">A IOBuffer object used to read a SPR2 chunk.</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decodes this SPR2Frame.
|
||||
/// </summary>
|
||||
/// <param name="io">An IOBuffer instance used to read a SPR2Frame.</param>
|
||||
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<PALT>(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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a pixel from this SPR2Frame.
|
||||
/// </summary>
|
||||
/// <param name="x">X position of pixel.</param>
|
||||
/// <param name="y">Y position of pixel.</param>
|
||||
/// <returns>A Color instance with color of pixel.</returns>
|
||||
public Color GetPixel(int x, int y)
|
||||
{
|
||||
return PixelData[(y * Width) + x];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a pixel from this SPR2Frame.
|
||||
/// </summary>
|
||||
/// <param name="x">X position of pixel.</param>
|
||||
/// <param name="y">Y position of pixel.</param>
|
||||
public void SetPixel(int x, int y, Color color)
|
||||
{
|
||||
PixelData[(y * Width) + x] = color;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the Z buffer into the current sprite's alpha channel. Used by water tile.
|
||||
/// </summary>
|
||||
public void CopyZToAlpha()
|
||||
{
|
||||
for (int i=0; i<PixelData.Length; i++)
|
||||
{
|
||||
PixelData[i].A = (ZBufferData[i] < 32)?(byte)0:ZBufferData[i];
|
||||
}
|
||||
}
|
||||
|
||||
public void FloorCopy()
|
||||
{
|
||||
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;
|
||||
for (int y = 0; y < Height; y++)
|
||||
{
|
||||
for (int x = 0; x < Width; x++)
|
||||
{
|
||||
var xp = (x + hw) % Width;
|
||||
var yp = (y + hh) % Height;
|
||||
var rep = PixelData[xp + yp * Width];
|
||||
if (rep.A >= 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<PALT>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a texture representing this SPR2Frame.
|
||||
/// </summary>
|
||||
/// <param name="device">GraphicsDevice instance used for drawing.</param>
|
||||
/// <returns>A Texture2D instance holding the texture data.</returns>
|
||||
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<byte>(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<Color>(this.PixelData);
|
||||
}
|
||||
result.Tag = new TextureInfo(result, Width, Height);
|
||||
PixelCache = new WeakReference<Texture2D>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a z-texture representing this SPR2Frame.
|
||||
/// </summary>
|
||||
/// <param name="device">GraphicsDevice instance used for drawing.</param>
|
||||
/// <returns>A Texture2D instance holding the texture data.</returns>
|
||||
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<Height; i++)
|
||||
{
|
||||
Array.Copy(ZBufferData, sind, tempZ, dind, Width);
|
||||
sind += Width;
|
||||
dind += result.Width;
|
||||
}
|
||||
|
||||
result.SetData<byte>(tempZ);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = new CachableTexture2D(device, this.Width, this.Height, false, SurfaceFormat.Alpha8);
|
||||
result.SetData<byte>(this.ZBufferData);
|
||||
}
|
||||
ZCache = new WeakReference<Texture2D>(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<PALT>(this.PaletteID);
|
||||
if (old != null) old.References--;
|
||||
}
|
||||
PaletteID = p.ChunkID;
|
||||
p.References++;
|
||||
}
|
||||
}
|
||||
}
|
124
server/tso.files/Formats/IFF/Chunks/SPR2FrameEncoder.cs
Executable file
124
server/tso.files/Formats/IFF/Chunks/SPR2FrameEncoder.cs
Executable file
|
@ -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<frame.Height; y++)
|
||||
{
|
||||
byte lastCmd = 0;
|
||||
List<byte> dataBuf = new List<byte>();
|
||||
int rlecount = 0;
|
||||
bool anySolid = false;
|
||||
|
||||
var scanStream = new MemoryStream();
|
||||
var scanOut = IoWriter.FromStream(scanStream, ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
for (int x=0; x<frame.Width; x++)
|
||||
{
|
||||
byte plt = bytes[index];
|
||||
byte a = col[index].A;
|
||||
byte z = zs[index];
|
||||
|
||||
var cmd = getCmd(plt, a, z);
|
||||
|
||||
if (x == 0 || cmd != lastCmd)
|
||||
{
|
||||
if (x != 0)
|
||||
{
|
||||
//write a command to write the last sequence of pixels
|
||||
scanOut.WriteUInt16((ushort)(((int)lastCmd<<13)|rlecount));
|
||||
if ((dataBuf.Count % 2) != 0) dataBuf.Add(0);
|
||||
scanOut.WriteBytes(dataBuf.ToArray());
|
||||
dataBuf.Clear();
|
||||
}
|
||||
|
||||
lastCmd = cmd;
|
||||
rlecount = 0;
|
||||
}
|
||||
|
||||
switch (cmd)
|
||||
{
|
||||
case 0x1:
|
||||
case 0x2:
|
||||
dataBuf.Add(z);
|
||||
dataBuf.Add(plt);
|
||||
if (cmd == 0x2) dataBuf.Add((byte)Math.Ceiling(a/ 8.2258064516129032258064516129032));
|
||||
anySolid = true;
|
||||
break;
|
||||
case 0x6:
|
||||
dataBuf.Add(plt);
|
||||
anySolid = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
rlecount++;
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
if (anySolid)
|
||||
{
|
||||
//write a command to write the last sequence of pixels
|
||||
scanOut.WriteUInt16((ushort)(((int)lastCmd << 13) | rlecount));
|
||||
if ((dataBuf.Count % 2) != 0) dataBuf.Add(0);
|
||||
scanOut.WriteBytes(dataBuf.ToArray());
|
||||
dataBuf.Clear();
|
||||
}
|
||||
|
||||
var scanData = scanStream.ToArray();
|
||||
|
||||
if (scanData.Length == 0)
|
||||
blankLines++; //line is transparent
|
||||
else
|
||||
{
|
||||
if (blankLines > 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
|
||||
}
|
||||
}
|
||||
}
|
438
server/tso.files/Formats/IFF/Chunks/STR.cs
Executable file
438
server/tso.files/Formats/IFF/Chunks/STR.cs
Executable file
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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] };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// How many strings are in this chunk?
|
||||
/// </summary>
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a string from this chunk.
|
||||
/// </summary>
|
||||
/// <param name="index">Index of string.</param>
|
||||
/// <returns>A string at specific index, null if not found.</returns>
|
||||
///
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a STRItem instance from this STR chunk.
|
||||
/// </summary>
|
||||
/// <param name="index">Index of STRItem.</param>
|
||||
/// <returns>STRItem at index, null if not found.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a STR chunk from a stream.
|
||||
/// </summary>
|
||||
/// <param name="iff">An Iff instance.</param>
|
||||
/// <param name="stream">A Stream object holding a STR chunk.</param>
|
||||
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<STRItem>[] LangSort = new List<STRItem>[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<STRItem>();
|
||||
}
|
||||
|
||||
LangSort[lang - 1].Add(item);
|
||||
}
|
||||
for (int i=0; i<LanguageSets.Length; i++)
|
||||
{
|
||||
if (LangSort[i] != null) LanguageSets[i].Strings = LangSort[i].ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
//This format is only used in The Sims Online. The format is essentially a performance improvement:
|
||||
//it counteracts both the short string limit of 255 characters found in 00 00 and the inherent slowness
|
||||
//of null-terminated strings in the other formats (which requires two passes over each string), and it
|
||||
//also provides a string pair count for each language set which eliminates the need for two passes over
|
||||
//each language set.
|
||||
else if (formatCode == -4)
|
||||
{
|
||||
var numLanguageSets = io.ReadByte();
|
||||
this.LanguageSets = new STRLanguageSet[numLanguageSets];
|
||||
|
||||
for(var i=0; i < numLanguageSets; i++)
|
||||
{
|
||||
var item = new STRLanguageSet();
|
||||
var numStringPairs = io.ReadUInt16();
|
||||
item.Strings = new STRItem[numStringPairs];
|
||||
for (var x = 0; x < numStringPairs; x++)
|
||||
{
|
||||
item.Strings[x] = new STRItem
|
||||
{
|
||||
LanguageCode = (byte)(io.ReadByte() + 1),
|
||||
Value = io.ReadVariableLengthPascalString(),
|
||||
Comment = io.ReadVariableLengthPascalString()
|
||||
};
|
||||
}
|
||||
this.LanguageSets[i] = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public override bool Write(IffFile iff, Stream stream)
|
||||
{
|
||||
using (var io = IoWriter.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
|
||||
{
|
||||
if (IffFile.TargetTS1)
|
||||
{
|
||||
// TS1 format - null terminated string
|
||||
io.WriteInt16(-3);
|
||||
var total = (short)LanguageSets.Sum(x => 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<total; i++)
|
||||
{
|
||||
io.WriteByte(0xA3);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// TSO format - variable length pascal
|
||||
io.WriteInt16(-4);
|
||||
io.WriteByte(20);
|
||||
|
||||
foreach (var set in LanguageSets)
|
||||
{
|
||||
if (set?.Strings == null)
|
||||
{
|
||||
io.WriteInt16(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
io.WriteUInt16((ushort)set.Strings.Length);
|
||||
|
||||
foreach (var str in set.Strings)
|
||||
{
|
||||
io.WriteByte((byte)(str.LanguageCode - 1));
|
||||
io.WriteVariableLengthPascalString(str.Value);
|
||||
io.WriteVariableLengthPascalString(str.Comment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Item in a STR chunk.
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set of STRItems for a language.
|
||||
/// </summary>
|
||||
public class STRLanguageSet
|
||||
{
|
||||
public STRItem[] Strings = new STRItem[0];
|
||||
}
|
||||
}
|
26
server/tso.files/Formats/IFF/Chunks/THMB.cs
Executable file
26
server/tso.files/Formats/IFF/Chunks/THMB.cs
Executable file
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
79
server/tso.files/Formats/IFF/Chunks/TPRP.cs
Executable file
79
server/tso.files/Formats/IFF/Chunks/TPRP.cs
Executable file
|
@ -0,0 +1,79 @@
|
|||
using FSO.Files.Utils;
|
||||
using System.IO;
|
||||
|
||||
namespace FSO.Files.Formats.IFF.Chunks
|
||||
{
|
||||
/// <summary>
|
||||
/// Labels for BHAV local variables and parameters.
|
||||
/// </summary>
|
||||
public class TPRP : IffChunk
|
||||
{
|
||||
public string[] ParamNames;
|
||||
public string[] LocalNames;
|
||||
|
||||
/// <summary>
|
||||
/// Reads a TPRP from a stream.
|
||||
/// </summary>
|
||||
/// <param name="iff">Iff instance.</param>
|
||||
/// <param name="stream">A Stream instance holding a TPRP chunk.</param>
|
||||
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<ParamNames.Length; i++)
|
||||
{
|
||||
io.WriteByte(0);
|
||||
}
|
||||
|
||||
io.WriteInt32(0);
|
||||
io.WriteInt32(0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
96
server/tso.files/Formats/IFF/Chunks/TRCN.cs
Executable file
96
server/tso.files/Formats/IFF/Chunks/TRCN.cs
Executable file
|
@ -0,0 +1,96 @@
|
|||
using System.IO;
|
||||
using FSO.Files.Utils;
|
||||
|
||||
namespace FSO.Files.Formats.IFF.Chunks
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides labels for BCON constants with the same resource ID.
|
||||
/// </summary>
|
||||
public class TRCN : IffChunk
|
||||
{
|
||||
public int Version;
|
||||
public TRCNEntry[] Entries;
|
||||
|
||||
/// <summary>
|
||||
/// Reads a BCON chunk from a stream.
|
||||
/// </summary>
|
||||
/// <param name="iff">An Iff instance.</param>
|
||||
/// <param name="stream">A Stream instance holding a BCON.</param>
|
||||
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<count; i++)
|
||||
{
|
||||
var entry = new TRCNEntry();
|
||||
entry.Read(io, Version, 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);
|
||||
}
|
||||
}
|
||||
}
|
345
server/tso.files/Formats/IFF/Chunks/TREE.cs
Executable file
345
server/tso.files/Formats/IFF/Chunks/TREE.cs
Executable file
|
@ -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<byte, short> 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<bhav.Instructions.Length; i++)
|
||||
{
|
||||
var inst = bhav.Instructions[i];
|
||||
var box = new TREEBox(result);
|
||||
box.InternalID = i;
|
||||
box.PosisionInvalid = true;
|
||||
box.Type = TREEBoxType.Primitive;
|
||||
box.TruePointer =
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
public List<TREEBox> Entries = new List<TREEBox>();
|
||||
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<entryCount; i++)
|
||||
{
|
||||
var box = new TREEBox(this);
|
||||
box.Read(io, version);
|
||||
box.InternalID = (short)i;
|
||||
Entries.Add(box);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Write(IffFile iff, Stream stream)
|
||||
{
|
||||
using (var io = IoWriter.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
|
||||
{
|
||||
io.WriteInt32(0);
|
||||
io.WriteInt32(1);
|
||||
io.WriteCString("EERT", 4);
|
||||
io.WriteInt32(Entries.Count);
|
||||
foreach (var entry in Entries)
|
||||
{
|
||||
entry.Write(io);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ApplyPointerDelta(int delta, int after)
|
||||
{
|
||||
foreach (var box in Entries)
|
||||
{
|
||||
if (box.InternalID >= 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; i++)
|
||||
{
|
||||
var box = new TREEBox(this);
|
||||
box.InternalID = (short)i;
|
||||
box.PosisionInvalid = true;
|
||||
box.Type = TREEBoxType.Primitive;
|
||||
Entries.Insert(i, box);
|
||||
}
|
||||
}
|
||||
else if (treePrimCount > 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<realPrimCount; i++)
|
||||
{
|
||||
var prim = bhav.Instructions[i];
|
||||
var box = Entries[i];
|
||||
|
||||
if (prim.TruePointer != GetTrueID(box.TruePointer))
|
||||
{
|
||||
box.TruePointer = GetCorrectBox(prim.TruePointer);
|
||||
}
|
||||
if (prim.FalsePointer != GetTrueID((short)box.FalsePointer))
|
||||
{
|
||||
box.FalsePointer = GetCorrectBox(prim.FalsePointer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void DeleteBox(TREEBox box)
|
||||
{
|
||||
//remove box. apply delta
|
||||
var id = box.InternalID;
|
||||
foreach (var box2 in Entries)
|
||||
{
|
||||
if (box2.TruePointer == id) box2.TruePointer = -1;
|
||||
if (box2.FalsePointer == id) box2.FalsePointer = -1;
|
||||
}
|
||||
Entries.RemoveAt(id);
|
||||
ApplyPointerDelta(-1, id);
|
||||
}
|
||||
|
||||
public void InsertPrimitiveBox(TREEBox box)
|
||||
{
|
||||
var primEnd = PrimitiveCount;
|
||||
ApplyPointerDelta(1, primEnd);
|
||||
|
||||
box.InternalID = (short)primEnd;
|
||||
Entries.Insert(primEnd, box);
|
||||
}
|
||||
|
||||
public void InsertRemovedBox(TREEBox box)
|
||||
{
|
||||
var oldIndex = box.InternalID;
|
||||
ApplyPointerDelta(1, oldIndex);
|
||||
Entries.Insert(oldIndex, box);
|
||||
}
|
||||
|
||||
public TREEBox MakeNewPrimitiveBox(TREEBoxType type)
|
||||
{
|
||||
var primEnd = PrimitiveCount;
|
||||
ApplyPointerDelta(1, primEnd);
|
||||
//find end of primitives and add box there. apply delta
|
||||
var box = new TREEBox(this);
|
||||
box.InternalID = (short)primEnd;
|
||||
box.PosisionInvalid = true;
|
||||
box.Type = type;
|
||||
Entries.Insert(primEnd, box);
|
||||
return box;
|
||||
}
|
||||
|
||||
public TREEBox MakeNewSpecialBox(TREEBoxType type)
|
||||
{
|
||||
//add box at end. no delta needs to be applied.
|
||||
var box = new TREEBox(this);
|
||||
box.InternalID = (short)Entries.Count;
|
||||
box.PosisionInvalid = true;
|
||||
box.Type = type;
|
||||
Entries.Add(box);
|
||||
return box;
|
||||
}
|
||||
|
||||
private short GetCorrectBox(byte realID)
|
||||
{
|
||||
switch (realID)
|
||||
{
|
||||
case 255:
|
||||
//create false box
|
||||
var f = MakeNewSpecialBox(TREEBoxType.False);
|
||||
return f.InternalID;
|
||||
case 254:
|
||||
//create true box
|
||||
var t = MakeNewSpecialBox(TREEBoxType.True);
|
||||
return t.InternalID;
|
||||
case 253:
|
||||
return -1;
|
||||
default:
|
||||
return realID;
|
||||
}
|
||||
}
|
||||
|
||||
public byte GetTrueID(short boxID)
|
||||
{
|
||||
return GetBox(boxID)?.TrueID ?? 253;
|
||||
}
|
||||
|
||||
public TREEBox GetBox(short pointer)
|
||||
{
|
||||
if (pointer < 0 || pointer >= 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<short>());
|
||||
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<short> 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
|
||||
}
|
||||
}
|
433
server/tso.files/Formats/IFF/Chunks/TTAB.cs
Executable file
433
server/tso.files/Formats/IFF/Chunks/TTAB.cs
Executable file
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class TTAB : IffChunk
|
||||
{
|
||||
public TTABInteraction[] Interactions = new TTABInteraction[0];
|
||||
public Dictionary<uint, TTABInteraction> InteractionByIndex = new Dictionary<uint, TTABInteraction>();
|
||||
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
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Reads a TTAB chunk from a stream.
|
||||
/// </summary>
|
||||
/// <param name="iff">An Iff instance.</param>
|
||||
/// <param name="stream">A Stream object holding a TTAB chunk.</param>
|
||||
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) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an interaction in a TTAB chunk.
|
||||
/// </summary>
|
||||
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<MotiveEntries.Length; i++)
|
||||
{
|
||||
MotiveEntries[i].MotiveIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a motive entry in a TTAB chunk.
|
||||
/// </summary>
|
||||
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,
|
||||
}
|
||||
}
|
65
server/tso.files/Formats/IFF/Chunks/TTAT.cs
Executable file
65
server/tso.files/Formats/IFF/Chunks/TTAT.cs
Executable file
|
@ -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<uint, short[]> TypeAttributesByGUID = new Dictionary<uint, short[]>();
|
||||
|
||||
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<total; i++)
|
||||
{
|
||||
var guid = (uint)iop.ReadInt32();
|
||||
var count = iop.ReadInt32();
|
||||
var tatts = new short[count];
|
||||
for (int j=0; j<count; j++)
|
||||
{
|
||||
tatts[j] = iop.ReadInt16();
|
||||
}
|
||||
TypeAttributesByGUID[guid] = tatts;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Write(IffFile iff, Stream stream)
|
||||
{
|
||||
using (var io = IoWriter.FromStream(stream, ByteOrder.LITTLE_ENDIAN))
|
||||
{
|
||||
io.WriteInt32(0);
|
||||
io.WriteInt32(0); //version
|
||||
|
||||
io.WriteCString("TTAT", 4);
|
||||
|
||||
io.WriteByte(0); //compression code
|
||||
io.WriteInt32(TypeAttributesByGUID.Count);
|
||||
foreach (var tatt in TypeAttributesByGUID)
|
||||
{
|
||||
io.WriteUInt32(tatt.Key);
|
||||
io.WriteInt32(tatt.Value.Length);
|
||||
foreach (var value in tatt.Value)
|
||||
io.WriteInt16(value);
|
||||
}
|
||||
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
10
server/tso.files/Formats/IFF/Chunks/TTAs.cs
Executable file
10
server/tso.files/Formats/IFF/Chunks/TTAs.cs
Executable file
|
@ -0,0 +1,10 @@
|
|||
namespace FSO.Files.Formats.IFF.Chunks
|
||||
{
|
||||
/// <summary>
|
||||
/// Duplicate of STR chunk, instead used for pie menu strings.
|
||||
/// </summary>
|
||||
public class TTAs : STR
|
||||
{
|
||||
//no difference!
|
||||
}
|
||||
}
|
61
server/tso.files/Formats/IFF/Chunks/WALm.cs
Executable file
61
server/tso.files/Formats/IFF/Chunks/WALm.cs
Executable file
|
@ -0,0 +1,61 @@
|
|||
using FSO.Files.Utils;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace FSO.Files.Formats.IFF.Chunks
|
||||
{
|
||||
/// <summary>
|
||||
/// WALm and FLRm chunks, used for mapping walls and floors in ARRY chunks to walls and floors in resource files (outwith floors.iff)
|
||||
/// </summary>
|
||||
public class WALm : IffChunk
|
||||
{
|
||||
public List<WALmEntry> 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<WALmEntry>();
|
||||
|
||||
for (int i=0; i<count; i++)
|
||||
{
|
||||
//size of fields depends on chunk id.
|
||||
Entries.Add(new WALmEntry(io, ChunkID));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class FLRm : WALm
|
||||
{
|
||||
//literally no difference
|
||||
}
|
||||
|
||||
public class WALmEntry
|
||||
{
|
||||
public string Name;
|
||||
public int Unknown; //usually 1
|
||||
public byte ID;
|
||||
public byte[] Unknown2;
|
||||
public WALmEntry(IoBuffer io, int id)
|
||||
{
|
||||
Name = io.ReadNullTerminatedString();
|
||||
if (Name.Length % 2 == 0) io.ReadByte(); //pad to short width
|
||||
Unknown = io.ReadInt32(); //index in iff?
|
||||
ID = io.ReadByte();
|
||||
Unknown2 = io.ReadBytes(5 + id * 2);
|
||||
|
||||
//id 0 seems to be an older format
|
||||
//unknown2 is 01 00 00 00 00 00
|
||||
//id 1 adds more fields
|
||||
//unknown2 is 01 01 00 00 00 00 00 00
|
||||
|
||||
//related to number of walls or floors in the file?
|
||||
}
|
||||
}
|
||||
}
|
587
server/tso.files/Formats/IFF/IffFile.cs
Executable file
587
server/tso.files/Formats/IFF/IffFile.cs
Executable file
|
@ -0,0 +1,587 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
using FSO.Files.Formats.IFF.Chunks;
|
||||
using FSO.Files.Utils;
|
||||
using FSO.Common.Utils;
|
||||
|
||||
namespace FSO.Files.Formats.IFF
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class IffFile : IFileInfoUtilizer, ITimedCachable
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<string, Type> CHUNK_TYPES = new Dictionary<string, Type>()
|
||||
{
|
||||
{"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<Type, Dictionary<ushort, object>> ByChunkId;
|
||||
private Dictionary<Type, List<object>> ByChunkType;
|
||||
public List<IffChunk> RemovedOriginal = new List<IffChunk>();
|
||||
public PIFF CurrentPIFF;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new IFF instance.
|
||||
/// </summary>
|
||||
public IffFile()
|
||||
{
|
||||
ByChunkId = new Dictionary<Type, Dictionary<ushort, object>>();
|
||||
ByChunkType = new Dictionary<Type, List<object>>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs an IFF instance from a filepath.
|
||||
/// </summary>
|
||||
/// <param name="filepath">Path to the IFF.</param>
|
||||
public IffFile(string filepath) : this()
|
||||
{
|
||||
using (var stream = File.Open(filepath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||||
{
|
||||
this.Read(stream);
|
||||
SetFilename(Path.GetFileName(filepath));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs an IFF instance from a filepath.
|
||||
/// </summary>
|
||||
/// <param name="filepath">Path to the IFF.</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads an IFF from a stream.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to read from.</param>
|
||||
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<object> executableTypes = ByChunkType[typeof(BHAV)];
|
||||
if (ByChunkType.ContainsKey(typeof(BCON))) executableTypes = executableTypes.Concat(ByChunkType[typeof(BCON)]);
|
||||
var hash = new xxHashSharp.xxHash();
|
||||
hash.Init();
|
||||
foreach (IffChunk chunk in executableTypes)
|
||||
{
|
||||
hash.Update(chunk.ChunkData ?? chunk.OriginalData, chunk.ChunkData.Length);
|
||||
}
|
||||
ExecutableHash = hash.Digest();
|
||||
}
|
||||
}
|
||||
|
||||
public IffChunk AddChunk(Stream stream, IoBuffer io, bool add)
|
||||
{
|
||||
var chunkType = io.ReadCString(4);
|
||||
var chunkSize = io.ReadUInt32();
|
||||
var chunkID = io.ReadUInt16();
|
||||
var chunkFlags = io.ReadUInt16();
|
||||
var chunkLabel = io.ReadCString(64).TrimEnd('\0');
|
||||
var chunkDataSize = chunkSize - 76;
|
||||
|
||||
/** Do we understand this chunk type? **/
|
||||
if (!CHUNK_TYPES.ContainsKey(chunkType))
|
||||
{
|
||||
/** Skip it! **/
|
||||
io.Skip(Math.Min(chunkDataSize, stream.Length - stream.Position - 1)); //if the chunk is invalid, it will likely provide a chunk size beyond the limits of the file. (walls2.iff)
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
Type chunkClass = CHUNK_TYPES[chunkType];
|
||||
IffChunk newChunk = (IffChunk)Activator.CreateInstance(chunkClass);
|
||||
newChunk.ChunkID = chunkID;
|
||||
newChunk.OriginalID = chunkID;
|
||||
newChunk.ChunkFlags = chunkFlags;
|
||||
newChunk.ChunkLabel = chunkLabel;
|
||||
newChunk.ChunkType = chunkType;
|
||||
newChunk.ChunkData = io.ReadBytes(chunkDataSize);
|
||||
|
||||
if (RetainChunkData)
|
||||
{
|
||||
newChunk.OriginalLabel = chunkLabel;
|
||||
newChunk.OriginalData = newChunk.ChunkData;
|
||||
}
|
||||
|
||||
if (add)
|
||||
{
|
||||
newChunk.ChunkParent = this;
|
||||
|
||||
if (!ByChunkType.ContainsKey(chunkClass))
|
||||
{
|
||||
ByChunkType.Add(chunkClass, new List<object>());
|
||||
}
|
||||
if (!ByChunkId.ContainsKey(chunkClass))
|
||||
{
|
||||
ByChunkId.Add(chunkClass, new Dictionary<ushort, object>());
|
||||
}
|
||||
|
||||
ByChunkType[chunkClass].Add(newChunk);
|
||||
if (!ByChunkId[chunkClass].ContainsKey(chunkID)) ByChunkId[chunkClass].Add(chunkID, newChunk);
|
||||
}
|
||||
return newChunk;
|
||||
}
|
||||
}
|
||||
|
||||
public void Write(Stream stream)
|
||||
{
|
||||
using (var io = IoWriter.FromStream(stream, ByteOrder.BIG_ENDIAN))
|
||||
{
|
||||
io.WriteCString("IFF FILE 2.5:TYPE FOLLOWED BY SIZE\0 JAMIE DOORNBOS & MAXIS 1", 60);
|
||||
io.WriteUInt32(0); //todo: resource map offset
|
||||
|
||||
var chunks = ListAll();
|
||||
foreach (var c in chunks)
|
||||
{
|
||||
WriteChunk(io, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteChunk(IoWriter io, IffChunk c)
|
||||
{
|
||||
var typeString = CHUNK_TYPES.FirstOrDefault(x => x.Value == c.GetType()).Key;
|
||||
|
||||
io.WriteCString((typeString == null) ? c.ChunkType : typeString, 4);
|
||||
|
||||
byte[] data;
|
||||
using (var cstr = new MemoryStream())
|
||||
{
|
||||
if (c.Write(this, cstr)) data = cstr.ToArray();
|
||||
else data = c.OriginalData;
|
||||
}
|
||||
|
||||
//todo: exporting PIFF as IFF SHOULD NOT DO THIS
|
||||
c.OriginalData = data; //if we revert, it is to the last save.
|
||||
c.RuntimeInfo = ChunkRuntimeState.Normal;
|
||||
|
||||
io.WriteUInt32((uint)data.Length + 76);
|
||||
io.WriteUInt16(c.ChunkID);
|
||||
if (c.ChunkFlags == 0) c.ChunkFlags = 0x10;
|
||||
io.WriteUInt16(c.ChunkFlags);
|
||||
io.WriteCString(c.ChunkLabel, 64);
|
||||
io.WriteBytes(data);
|
||||
}
|
||||
|
||||
private T prepare<T>(object input)
|
||||
{
|
||||
IffChunk chunk = (IffChunk)input;
|
||||
if (chunk.ChunkProcessed != true)
|
||||
{
|
||||
lock (chunk)
|
||||
{
|
||||
if (chunk.ChunkProcessed != true)
|
||||
{
|
||||
using (var stream = new MemoryStream(chunk.ChunkData))
|
||||
{
|
||||
chunk.Read(this, stream);
|
||||
chunk.ChunkData = null;
|
||||
chunk.ChunkProcessed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return (T)input;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a chunk by its type and ID
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of the chunk.</typeparam>
|
||||
/// <param name="id">ID of the chunk.</param>
|
||||
/// <returns>A chunk.</returns>
|
||||
public T Get<T>(ushort id){
|
||||
Type typeofT = typeof(T);
|
||||
if (ByChunkId.ContainsKey(typeofT))
|
||||
{
|
||||
var lookup = ByChunkId[typeofT];
|
||||
if (lookup.ContainsKey(id))
|
||||
{
|
||||
return prepare<T>(lookup[id]);
|
||||
}
|
||||
}
|
||||
return default(T);
|
||||
}
|
||||
|
||||
public List<IffChunk> ListAll()
|
||||
{
|
||||
var result = new List<IffChunk>();
|
||||
foreach (var type in ByChunkType.Values)
|
||||
{
|
||||
foreach (var chunk in type)
|
||||
{
|
||||
result.Add(this.prepare<IffChunk>(chunk));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public List<IffChunk> SilentListAll()
|
||||
{
|
||||
var result = new List<IffChunk>();
|
||||
foreach (var type in ByChunkType.Values)
|
||||
{
|
||||
foreach (var chunk in type)
|
||||
{
|
||||
result.Add((IffChunk)chunk);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// List all chunks of a certain type
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the chunks to list.</typeparam>
|
||||
/// <returns>A list of chunks of the type.</returns>
|
||||
public List<T> List<T>()
|
||||
{
|
||||
Type typeofT = typeof(T);
|
||||
|
||||
if (ByChunkType.ContainsKey(typeofT))
|
||||
{
|
||||
var result = new List<T>();
|
||||
foreach (var item in ByChunkType[typeofT])
|
||||
{
|
||||
result.Add(this.prepare<T>(item));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void RemoveChunk(IffChunk chunk)
|
||||
{
|
||||
var type = chunk.GetType();
|
||||
ByChunkId[type].Remove(chunk.ChunkID);
|
||||
ByChunkType[type].Remove(chunk);
|
||||
}
|
||||
|
||||
public void FullRemoveChunk(IffChunk chunk)
|
||||
{
|
||||
//register this chunk as one that has been hard removed
|
||||
if (!chunk.AddedByPatch) RemovedOriginal.Add(chunk);
|
||||
RemoveChunk(chunk);
|
||||
}
|
||||
|
||||
public void AddChunk(IffChunk chunk)
|
||||
{
|
||||
var type = chunk.GetType();
|
||||
chunk.ChunkParent = this;
|
||||
|
||||
if (!ByChunkType.ContainsKey(type))
|
||||
{
|
||||
ByChunkType.Add(type, new List<object>());
|
||||
}
|
||||
if (!ByChunkId.ContainsKey(type))
|
||||
{
|
||||
ByChunkId.Add(type, new Dictionary<ushort, object>());
|
||||
}
|
||||
|
||||
ByChunkId[type].Add(chunk.ChunkID, chunk);
|
||||
ByChunkType[type].Add(chunk);
|
||||
}
|
||||
|
||||
public void MoveAndSwap(IffChunk chunk, ushort targID)
|
||||
{
|
||||
if (chunk.ChunkID == targID) return;
|
||||
var type = chunk.GetType();
|
||||
object targ = null;
|
||||
if (ByChunkId.ContainsKey(type))
|
||||
{
|
||||
ByChunkId[type].TryGetValue(targID, out targ);
|
||||
}
|
||||
|
||||
IffChunk tChunk = (IffChunk)targ;
|
||||
|
||||
if (tChunk != null) RemoveChunk(tChunk);
|
||||
var oldID = chunk.ChunkID;
|
||||
RemoveChunk(chunk);
|
||||
chunk.ChunkID = targID;
|
||||
AddChunk(chunk);
|
||||
if (tChunk != null)
|
||||
{
|
||||
tChunk.ChunkID = oldID;
|
||||
AddChunk(tChunk);
|
||||
}
|
||||
}
|
||||
|
||||
public void Revert()
|
||||
{
|
||||
//revert all iffs and rerun patches
|
||||
|
||||
var toRemove = new List<IffChunk>();
|
||||
foreach (var chunk in SilentListAll())
|
||||
{
|
||||
if (chunk.AddedByPatch) chunk.ChunkParent.RemoveChunk(chunk);
|
||||
else
|
||||
{
|
||||
if (chunk.OriginalData != null)
|
||||
{
|
||||
//revert if we have a state to revert to. for new chunks this is not really possible.
|
||||
chunk.ChunkData = chunk.OriginalData;
|
||||
chunk.ChunkParent.MoveAndSwap(chunk, chunk.OriginalID);
|
||||
chunk.Dispose();
|
||||
chunk.ChunkProcessed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//add back removed chunks
|
||||
foreach (var chunk in RemovedOriginal)
|
||||
{
|
||||
chunk.ChunkParent.AddChunk(chunk);
|
||||
}
|
||||
RemovedOriginal.Clear();
|
||||
|
||||
//rerun patches
|
||||
foreach (var piff in RuntimeInfo.Patches)
|
||||
{
|
||||
Patch(piff);
|
||||
}
|
||||
|
||||
foreach (var type in ByChunkType.Values)
|
||||
{
|
||||
foreach (IffChunk chunk in type)
|
||||
{
|
||||
prepare<IffChunk>(chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Revert<T>(T chunk) where T : IffChunk
|
||||
{
|
||||
chunk.RuntimeInfo = ChunkRuntimeState.Normal;
|
||||
if (RuntimeInfo.State != IffRuntimeState.Standalone && chunk.AddedByPatch)
|
||||
{
|
||||
//added by piff.
|
||||
foreach (var piff in RuntimeInfo.Patches)
|
||||
{
|
||||
var oldC = piff.Get<T>(chunk.ChunkID);
|
||||
if (oldC != null)
|
||||
{
|
||||
chunk.ChunkData = oldC.OriginalData;
|
||||
chunk.Dispose();
|
||||
chunk.ChunkProcessed = false;
|
||||
chunk.RuntimeInfo = ChunkRuntimeState.Patched;
|
||||
prepare<T>(chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
chunk.ChunkData = chunk.OriginalData;
|
||||
foreach (var piffFile in RuntimeInfo.Patches)
|
||||
{
|
||||
var piff = piffFile.List<PIFF>()[0];
|
||||
foreach (var e in piff.Entries)
|
||||
{
|
||||
var type = CHUNK_TYPES[e.Type];
|
||||
if (!(type.IsAssignableFrom(chunk.GetType())) || e.ChunkID != chunk.ChunkID) continue;
|
||||
if (chunk.RuntimeInfo == ChunkRuntimeState.Patched) continue;
|
||||
|
||||
chunk.ChunkData = e.Apply(chunk.ChunkData);
|
||||
chunk.RuntimeInfo = ChunkRuntimeState.Patched;
|
||||
}
|
||||
}
|
||||
chunk.ChunkProcessed = false;
|
||||
chunk.Dispose();
|
||||
prepare<T>(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
public void Patch(IffFile piffFile)
|
||||
{
|
||||
if (RuntimeInfo.State == IffRuntimeState.ReadOnly) RuntimeInfo.State = IffRuntimeState.PIFFPatch;
|
||||
var piff = piffFile.List<PIFF>()[0];
|
||||
CurrentPIFF = piff;
|
||||
|
||||
//patch existing chunks using the PIFF chunk
|
||||
//also delete chunks marked for deletion
|
||||
|
||||
var moveChunks = new List<IffChunk>();
|
||||
var newIDs = new List<ushort>();
|
||||
|
||||
foreach (var e in piff.Entries)
|
||||
{
|
||||
var type = CHUNK_TYPES[e.Type];
|
||||
|
||||
Dictionary<ushort, object> chunks = null;
|
||||
ByChunkId.TryGetValue(type, out chunks);
|
||||
if (chunks == null) continue;
|
||||
object objC = null;
|
||||
chunks.TryGetValue(e.ChunkID, out objC);
|
||||
if (objC == null) continue;
|
||||
|
||||
var chunk = (IffChunk)objC;
|
||||
if (e.EntryType == PIFFEntryType.Remove)
|
||||
{
|
||||
FullRemoveChunk(chunk); //removed by PIFF
|
||||
}
|
||||
else if(e.EntryType == PIFFEntryType.Patch)
|
||||
{
|
||||
chunk.ChunkData = e.Apply(chunk.ChunkData ?? chunk.OriginalData);
|
||||
chunk.ChunkProcessed = false;
|
||||
if (e.ChunkLabel != "") chunk.ChunkLabel = e.ChunkLabel;
|
||||
chunk.RuntimeInfo = ChunkRuntimeState.Patched;
|
||||
|
||||
if (e.ChunkID != e.NewChunkID)
|
||||
{
|
||||
moveChunks.Add(chunk);
|
||||
newIDs.Add(e.NewChunkID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i=0; i<moveChunks.Count; i++)
|
||||
MoveAndSwap(moveChunks[i], newIDs[i]);
|
||||
|
||||
//add chunks present in the piff to the original file
|
||||
foreach (var typeG in piffFile.ByChunkType)
|
||||
{
|
||||
if (typeG.Key == typeof(PIFF)) continue;
|
||||
foreach (var res in typeG.Value)
|
||||
{
|
||||
var chunk = (IffChunk)res;
|
||||
chunk.AddedByPatch = true;
|
||||
if (!ByChunkId.ContainsKey(chunk.GetType()) || !ByChunkId[chunk.GetType()].ContainsKey(chunk.ChunkID)) this.AddChunk(chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetFilename(string filename)
|
||||
{
|
||||
Filename = filename;
|
||||
var piffs = PIFFRegistry.GetPIFFs(filename);
|
||||
RuntimeInfo.Patches.Clear();
|
||||
if (piffs != null)
|
||||
{
|
||||
//apply patches
|
||||
foreach (var piff in piffs)
|
||||
{
|
||||
Patch(piff);
|
||||
if (RetainChunkData) RuntimeInfo.Patches.Add(piff);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
29
server/tso.files/Formats/IFF/IffRuntimeInfo.cs
Executable file
29
server/tso.files/Formats/IFF/IffRuntimeInfo.cs
Executable file
|
@ -0,0 +1,29 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace FSO.Files.Formats.IFF
|
||||
{
|
||||
public class IffRuntimeInfo
|
||||
{
|
||||
public IffRuntimeState State;
|
||||
public IffUseCase UseCase;
|
||||
public string Filename;
|
||||
public string Path;
|
||||
public bool Dirty;
|
||||
public List<IffFile> Patches = new List<IffFile>();
|
||||
}
|
||||
|
||||
public enum IffRuntimeState
|
||||
{
|
||||
ReadOnly, //orignal game iff
|
||||
PIFFPatch, //replacement patch
|
||||
PIFFClone, //clone of original object
|
||||
Standalone //standalone, mutable iff
|
||||
}
|
||||
|
||||
public enum IffUseCase
|
||||
{
|
||||
Global,
|
||||
Object,
|
||||
ObjectSprites,
|
||||
}
|
||||
}
|
111
server/tso.files/Formats/IFF/PIFFRegistry.cs
Executable file
111
server/tso.files/Formats/IFF/PIFFRegistry.cs
Executable file
|
@ -0,0 +1,111 @@
|
|||
using FSO.Files.Formats.IFF.Chunks;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace FSO.Files.Formats.IFF
|
||||
{
|
||||
public static class PIFFRegistry
|
||||
{
|
||||
private static Dictionary<string, List<IffFile>> PIFFsByName = new Dictionary<string, List<IffFile>>();
|
||||
private static Dictionary<string, string> OtfRewrite = new Dictionary<string, string>();
|
||||
private static Dictionary<string, bool> IsPIFFUser = new Dictionary<string, bool>(); //if a piff is User, all other piffs for that file are ignored.
|
||||
private static HashSet<string> OBJDAdded = new HashSet<string>();
|
||||
|
||||
public static void Init(string basePath)
|
||||
{
|
||||
if (!Directory.Exists(basePath)) return;
|
||||
string[] paths = Directory.GetFiles(basePath, "*.piff", SearchOption.AllDirectories);
|
||||
for (int i = 0; i < paths.Length; i++)
|
||||
{
|
||||
string entry = paths[i].Replace('\\', '/');
|
||||
bool user = entry.Contains("User/");
|
||||
string filename = Path.GetFileName(entry);
|
||||
|
||||
PIFF piff;
|
||||
IffFile piffFile;
|
||||
try
|
||||
{
|
||||
piffFile = new IffFile(entry);
|
||||
piff = piffFile.List<PIFF>()[0];
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (piff.Version < 2)
|
||||
{
|
||||
piff.AppendAddedChunks(piffFile);
|
||||
}
|
||||
|
||||
if (IsPIFFUser.ContainsKey(piff.SourceIff))
|
||||
{
|
||||
var old = IsPIFFUser[piff.SourceIff];
|
||||
if (old != user)
|
||||
{
|
||||
if (user)
|
||||
{
|
||||
//remove old piffs, as they have been overwritten by this user piff.
|
||||
PIFFsByName[piff.SourceIff].Clear();
|
||||
IsPIFFUser[piff.SourceIff] = true;
|
||||
}
|
||||
else continue; //a user piff exists. ignore these ones.
|
||||
}
|
||||
}
|
||||
else IsPIFFUser.Add(piff.SourceIff, user);
|
||||
|
||||
if (!PIFFsByName.ContainsKey(piff.SourceIff)) PIFFsByName.Add(piff.SourceIff, new List<IffFile>());
|
||||
PIFFsByName[piff.SourceIff].Add(piffFile);
|
||||
}
|
||||
|
||||
string[] otfs = Directory.GetFiles(basePath, "*.otf", SearchOption.AllDirectories);
|
||||
|
||||
foreach (var otf in otfs)
|
||||
{
|
||||
string entry = otf.Replace('\\', '/');
|
||||
OtfRewrite[Path.GetFileName(entry)] = entry;
|
||||
}
|
||||
|
||||
foreach (var piffs in PIFFsByName)
|
||||
{
|
||||
foreach (var piff in piffs.Value)
|
||||
{
|
||||
var addedOBJD = piff.List<OBJD>();
|
||||
if (addedOBJD != null)
|
||||
{
|
||||
OBJDAdded.Add(piffs.Key);
|
||||
continue;
|
||||
}
|
||||
|
||||
var pChunk = piff.List<PIFF>()?.FirstOrDefault();
|
||||
if (pChunk != null && pChunk.Entries.Any(x => x.Type == "OBJD"))
|
||||
{
|
||||
OBJDAdded.Add(piffs.Key);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static HashSet<string> GetOBJDRewriteNames()
|
||||
{
|
||||
return OBJDAdded;
|
||||
}
|
||||
|
||||
public static string GetOTFRewrite(string srcFile)
|
||||
{
|
||||
string result = null;
|
||||
OtfRewrite.TryGetValue(srcFile, out result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static List<IffFile> GetPIFFs(string srcFile)
|
||||
{
|
||||
List<IffFile> result = null;
|
||||
PIFFsByName.TryGetValue(srcFile, out result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
105
server/tso.files/Formats/OTF/OTFFile.cs
Executable file
105
server/tso.files/Formats/OTF/OTFFile.cs
Executable file
|
@ -0,0 +1,105 @@
|
|||
using System.Linq;
|
||||
using System.IO;
|
||||
using System.Xml;
|
||||
using FSO.Files.Formats.IFF;
|
||||
|
||||
namespace FSO.Files.Formats.OTF
|
||||
{
|
||||
/// <summary>
|
||||
/// Object Tuning File (OTF) is an SGML format which defines tuning constants.
|
||||
/// </summary>
|
||||
public class OTFFile
|
||||
{
|
||||
public XmlDocument Document;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs an OTF instance from a filepath.
|
||||
/// </summary>
|
||||
/// <param name="filepath">Path to the OTF.</param>
|
||||
public OTFFile(string filepath)
|
||||
{
|
||||
using (var stream = File.OpenRead(filepath))
|
||||
{
|
||||
this.Read(stream);
|
||||
}
|
||||
}
|
||||
|
||||
public OTFFile()
|
||||
{
|
||||
//you can also create empty OTFs!
|
||||
}
|
||||
|
||||
public OTFTable[] Tables;
|
||||
|
||||
/// <summary>
|
||||
/// Gets an OTFTable instance from an ID.
|
||||
/// </summary>
|
||||
/// <param name="ID">The ID of the table.</param>
|
||||
/// <returns>An OTFTable instance.</returns>
|
||||
public OTFTable GetTable(int ID)
|
||||
{
|
||||
return Tables.FirstOrDefault(x => x?.ID == ID);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads an OTF from a stream.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to read from.</param>
|
||||
public void Read(Stream stream)
|
||||
{
|
||||
var doc = new XmlDocument();
|
||||
doc.Load(stream);
|
||||
|
||||
if (IffFile.RETAIN_CHUNK_DATA) Document = doc;
|
||||
|
||||
var tables = doc.GetElementsByTagName("T");
|
||||
Tables = new OTFTable[tables.Count];
|
||||
|
||||
for (var i = 0; i < tables.Count; i++)
|
||||
{
|
||||
var table = tables.Item(i);
|
||||
if (table.NodeType == XmlNodeType.Comment) continue;
|
||||
var tableEntry = new OTFTable();
|
||||
tableEntry.ID = int.Parse(table.Attributes["i"].Value);
|
||||
tableEntry.Name = table.Attributes["n"].Value;
|
||||
|
||||
var numKeys = table.ChildNodes.Count;
|
||||
tableEntry.Keys = new OTFTableKey[numKeys];
|
||||
|
||||
for (var x = 0; x < numKeys; x++)
|
||||
{
|
||||
var key = table.ChildNodes[x];
|
||||
|
||||
if (key.NodeType == XmlNodeType.Comment) continue;
|
||||
|
||||
var keyEntry = new OTFTableKey();
|
||||
keyEntry.ID = int.Parse(key.Attributes["i"].Value);
|
||||
keyEntry.Label = key.Attributes["l"].Value;
|
||||
keyEntry.Value = int.Parse(key.Attributes["v"].Value);
|
||||
tableEntry.Keys[x] = keyEntry;
|
||||
}
|
||||
|
||||
Tables[i] = tableEntry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class OTFTable
|
||||
{
|
||||
public int ID;
|
||||
public string Name;
|
||||
public OTFTableKey[] Keys;
|
||||
|
||||
public OTFTableKey GetKey(int id)
|
||||
{
|
||||
return Keys.FirstOrDefault(x => x?.ID == id);
|
||||
}
|
||||
}
|
||||
|
||||
public class OTFTableKey
|
||||
{
|
||||
public int ID;
|
||||
public string Label;
|
||||
public int Value;
|
||||
}
|
||||
}
|
267
server/tso.files/Formats/PiffEncoder.cs
Executable file
267
server/tso.files/Formats/PiffEncoder.cs
Executable file
|
@ -0,0 +1,267 @@
|
|||
using FSO.Files.Formats.IFF;
|
||||
using FSO.Files.Formats.IFF.Chunks;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace FSO.Files.Formats
|
||||
{
|
||||
public static class PiffEncoder
|
||||
{
|
||||
private static string FindComment(IffChunk chunk, PIFF oldPIFF)
|
||||
{
|
||||
var oldEntry = oldPIFF?.Entries?.FirstOrDefault(entry =>
|
||||
entry.Type == chunk.ChunkType &&
|
||||
((chunk.AddedByPatch) ? chunk.ChunkID : chunk.OriginalID) == entry.ChunkID);
|
||||
return oldEntry?.Comment ?? "";
|
||||
}
|
||||
|
||||
public static IffFile GeneratePiff(IffFile iff, HashSet<Type> allowedTypes, HashSet<Type> disallowedTypes, PIFF oldPIFF)
|
||||
{
|
||||
var piffFile = new IffFile();
|
||||
|
||||
var piff = new PIFF();
|
||||
piff.SourceIff = iff.Filename;
|
||||
if (oldPIFF != null) piff.Comment = oldPIFF.Comment;
|
||||
var entries = new List<PIFFEntry>();
|
||||
var chunks = iff.ListAll();
|
||||
|
||||
//write removals first
|
||||
foreach (var c in iff.RemovedOriginal)
|
||||
{
|
||||
lock (c)
|
||||
{
|
||||
if ((allowedTypes == null || allowedTypes.Contains(c.GetType()))
|
||||
&& (disallowedTypes == null || !disallowedTypes.Contains(c.GetType())))
|
||||
{
|
||||
entries.Add(new PIFFEntry {
|
||||
Type = c.ChunkType, ChunkID = c.OriginalID, EntryType = PIFFEntryType.Remove,
|
||||
ChunkLabel = c.ChunkLabel, ChunkFlags = c.ChunkFlags, Comment = FindComment(c, oldPIFF)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool anyAdded = false;
|
||||
foreach (var c in chunks)
|
||||
{
|
||||
//find a comment for this chunk
|
||||
lock (c)
|
||||
{
|
||||
if ((allowedTypes == null || allowedTypes.Contains(c.GetType()))
|
||||
&& (disallowedTypes == null || !disallowedTypes.Contains(c.GetType())))
|
||||
{
|
||||
if (c.AddedByPatch)
|
||||
{
|
||||
//this chunk has been newly added.
|
||||
var oldParent = c.ChunkParent;
|
||||
anyAdded = true;
|
||||
piffFile.AddChunk(c);
|
||||
c.ChunkParent = oldParent;
|
||||
|
||||
//make an entry for it!
|
||||
entries.Add(new PIFFEntry()
|
||||
{
|
||||
ChunkID = c.ChunkID,
|
||||
ChunkLabel = c.ChunkLabel,
|
||||
ChunkFlags = c.ChunkFlags,
|
||||
EntryType = PIFFEntryType.Add,
|
||||
NewDataSize = (uint)(c.ChunkData?.Length ?? 0),
|
||||
Type = c.ChunkType,
|
||||
Comment = FindComment(c, oldPIFF)
|
||||
});
|
||||
}
|
||||
else if ((c.RuntimeInfo == ChunkRuntimeState.Modified || c.RuntimeInfo == ChunkRuntimeState.Patched))
|
||||
{
|
||||
var chunkD = MakeChunkDiff(c);
|
||||
if (chunkD != null && (chunkD.Patches.Length > 0 || c.OriginalLabel != c.ChunkLabel || c.OriginalID != c.ChunkID))
|
||||
{
|
||||
chunkD.Comment = FindComment(c, oldPIFF);
|
||||
entries.Add(chunkD);
|
||||
}
|
||||
c.RuntimeInfo = ChunkRuntimeState.Patched;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (entries.Count == 0 && !anyAdded) return null; //no patch data...
|
||||
piff.Entries = entries.ToArray();
|
||||
piff.ChunkID = 256;
|
||||
piff.ChunkLabel = (piff.SourceIff + " patch");
|
||||
piff.ChunkProcessed = true;
|
||||
|
||||
piffFile.AddChunk(piff);
|
||||
piffFile.Filename = (oldPIFF != null) ? oldPIFF.ChunkParent.Filename : null; // (piff.SourceIff.Substring(0, piff.SourceIff.Length - 4)+".piff")
|
||||
return piffFile;
|
||||
}
|
||||
|
||||
public static PIFFEntry MakeChunkDiff(IffChunk chk)
|
||||
{
|
||||
var e = new PIFFEntry { Type = chk.ChunkType, ChunkID = chk.OriginalID, NewChunkID = chk.ChunkID };
|
||||
if (chk == null)
|
||||
{
|
||||
e.EntryType = PIFFEntryType.Remove;
|
||||
return e;
|
||||
}
|
||||
|
||||
byte[] newData = null;
|
||||
using (var stream = new MemoryStream())
|
||||
{
|
||||
if (!chk.Write(chk.ChunkParent, stream))
|
||||
{
|
||||
return null; //use original
|
||||
}
|
||||
newData = stream.ToArray();
|
||||
}
|
||||
|
||||
e.ChunkLabel = (chk.OriginalLabel==chk.ChunkLabel)?"":chk.ChunkLabel;
|
||||
e.ChunkFlags = chk.ChunkFlags;
|
||||
e.NewDataSize = (uint)newData.Length;
|
||||
|
||||
//encode difference as sequence of changes
|
||||
var oldData = chk.OriginalData;
|
||||
var patches = new List<PIFFPatch>();
|
||||
|
||||
int i;
|
||||
for (i=0; i<newData.Length; i += 1000)
|
||||
{
|
||||
if (i >= oldData.Length)
|
||||
{
|
||||
//no more comparisons, just add the remainder
|
||||
var remain = new byte[newData.Length-i];
|
||||
Array.Copy(newData, i, remain, 0, remain.Length);
|
||||
patches.Add(new PIFFPatch
|
||||
{
|
||||
Mode = PIFFPatchMode.Add,
|
||||
Data = remain,
|
||||
Offset = (uint)i,
|
||||
Size = (uint)remain.Length
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
//dynamic programming matrix.
|
||||
int m = Math.Min(1000, Math.Max(0, newData.Length - i))+1;
|
||||
int n = Math.Min(1000, Math.Max(0, oldData.Length - i))+1;
|
||||
ushort[,] comp = new ushort[m, n];
|
||||
for (int x=1; x<m; x++)
|
||||
{
|
||||
for (int y=1; y<n; y++)
|
||||
{
|
||||
if (newData[i+x-1] == oldData[i + y -1]) comp[x, y] = (ushort)(comp[x - 1, y - 1] + 1);
|
||||
else comp[x, y] = Math.Max(comp[x, y - 1], comp[x - 1, y]);
|
||||
}
|
||||
}
|
||||
|
||||
var changes = new Stack<byte>();
|
||||
//backtrack through compare
|
||||
{
|
||||
int x = m-1, y = n-1;
|
||||
while (true)
|
||||
{
|
||||
if (x>0 && y>0 && newData[i + x -1] == oldData[i + y -1])
|
||||
{
|
||||
x--; y--; changes.Push(0); //no change
|
||||
} else if (y>0 && (x==0 || comp[x,y-1] >= comp[x-1,y]))
|
||||
{
|
||||
y--; changes.Push(2); //remove
|
||||
} else if (x>0 && (y==0 || comp[x,y-1] < comp[x-1,y]))
|
||||
{
|
||||
x--; changes.Push(1); //add
|
||||
} else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
byte lastC = 0;
|
||||
PIFFPatch? curr = null;
|
||||
List<byte> addArray = null;
|
||||
int ptr = 0;
|
||||
foreach (var c in changes)
|
||||
{
|
||||
if (c != lastC && curr != null)
|
||||
{
|
||||
var patch = curr.Value;
|
||||
if (lastC == 1) patch.Data = addArray.ToArray();
|
||||
patches.Add(patch);
|
||||
curr = null;
|
||||
}
|
||||
if (c == 0) ptr++;
|
||||
else if (c == 1)
|
||||
{
|
||||
if (lastC != 1)
|
||||
{
|
||||
curr = new PIFFPatch { Mode = PIFFPatchMode.Add, Offset = (uint)(i + ptr), Size = 1 };
|
||||
addArray = new List<byte>();
|
||||
addArray.Add(newData[i + ptr]);
|
||||
}
|
||||
else
|
||||
{
|
||||
var patch = curr.Value;
|
||||
patch.Size++;
|
||||
curr = patch;
|
||||
addArray.Add(newData[i + ptr]);
|
||||
}
|
||||
ptr++;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (lastC != 2)
|
||||
curr = new PIFFPatch { Mode = PIFFPatchMode.Remove, Offset = (uint)(i + ptr), Size = 1 };
|
||||
else
|
||||
{
|
||||
var patch = curr.Value;
|
||||
patch.Size++;
|
||||
curr = patch;
|
||||
}
|
||||
}
|
||||
lastC = c;
|
||||
}
|
||||
|
||||
if (curr != null)
|
||||
{
|
||||
var patch = curr.Value;
|
||||
if (lastC == 1) patch.Data = addArray.ToArray();
|
||||
patches.Add(patch);
|
||||
}
|
||||
|
||||
if (m < n)
|
||||
{
|
||||
//remainder on src to be removed
|
||||
patches.Add(new PIFFPatch { Mode = PIFFPatchMode.Remove, Offset = (uint)(i+ptr), Size = (uint)(n-m) });
|
||||
}
|
||||
/*else if (m != n)
|
||||
{
|
||||
//remainder on dest to be added
|
||||
var remain = new byte[m-n];
|
||||
Array.Copy(newData, i+ptr, remain, 0, remain.Length);
|
||||
patches.Add(new PIFFPatch
|
||||
{
|
||||
Mode = PIFFPatchMode.Add,
|
||||
Data = remain,
|
||||
Offset = (uint)(i+ ptr),
|
||||
Size = (uint)remain.Length
|
||||
});
|
||||
}*/
|
||||
}
|
||||
|
||||
if (oldData.Length > i)
|
||||
{
|
||||
//ran out of new data, but old is still going. Remove the remainder.
|
||||
patches.Add(new PIFFPatch
|
||||
{
|
||||
Mode = PIFFPatchMode.Remove,
|
||||
Offset = (uint)newData.Length,
|
||||
Size = (uint)(oldData.Length - i)
|
||||
});
|
||||
}
|
||||
|
||||
e.Patches = patches.ToArray();
|
||||
|
||||
return e;
|
||||
}
|
||||
}
|
||||
}
|
91
server/tso.files/Formats/tsodata/BulletinItem.cs
Executable file
91
server/tso.files/Formats/tsodata/BulletinItem.cs
Executable file
|
@ -0,0 +1,91 @@
|
|||
using FSO.Files.Utils;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace FSO.Files.Formats.tsodata
|
||||
{
|
||||
public class BulletinItem
|
||||
{
|
||||
public static int CURRENT_VERSION = 1;
|
||||
public int Version = CURRENT_VERSION;
|
||||
public uint ID;
|
||||
public uint NhoodID;
|
||||
public uint SenderID;
|
||||
|
||||
public string Subject;
|
||||
public string Body;
|
||||
public string SenderName;
|
||||
|
||||
public long Time;
|
||||
|
||||
public BulletinType Type;
|
||||
public BulletinFlags Flags;
|
||||
|
||||
public uint LotID; //optional: for lot advertisements.
|
||||
|
||||
public BulletinItem()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public BulletinItem(Stream stream)
|
||||
{
|
||||
Read(stream);
|
||||
}
|
||||
|
||||
public void Save(Stream stream)
|
||||
{
|
||||
using (var writer = IoWriter.FromStream(stream))
|
||||
{
|
||||
writer.WriteCString("FSOB", 4);
|
||||
writer.WriteInt32(Version);
|
||||
writer.WriteUInt32(ID);
|
||||
writer.WriteUInt32(NhoodID);
|
||||
writer.WriteUInt32(SenderID);
|
||||
|
||||
writer.WriteLongPascalString(Subject);
|
||||
writer.WriteLongPascalString(Body);
|
||||
writer.WriteLongPascalString(SenderName);
|
||||
writer.WriteInt64(Time);
|
||||
writer.WriteInt32((int)Type);
|
||||
writer.WriteInt32((int)Flags);
|
||||
|
||||
writer.WriteUInt32(LotID);
|
||||
}
|
||||
}
|
||||
|
||||
public void Read(Stream stream)
|
||||
{
|
||||
using (var reader = IoBuffer.FromStream(stream))
|
||||
{
|
||||
var magic = reader.ReadCString(4);
|
||||
Version = reader.ReadInt32();
|
||||
ID = reader.ReadUInt32();
|
||||
NhoodID = reader.ReadUInt32();
|
||||
SenderID = reader.ReadUInt32();
|
||||
|
||||
Subject = reader.ReadLongPascalString();
|
||||
Body = reader.ReadLongPascalString();
|
||||
SenderName = reader.ReadLongPascalString();
|
||||
Time = reader.ReadInt64();
|
||||
Type = (BulletinType)reader.ReadInt32();
|
||||
Flags = (BulletinFlags)reader.ReadInt32();
|
||||
|
||||
LotID = reader.ReadUInt32();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum BulletinType
|
||||
{
|
||||
Mayor = 0,
|
||||
System = 1,
|
||||
Community = 2,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum BulletinFlags
|
||||
{
|
||||
PromotedByMayor = 1
|
||||
}
|
||||
}
|
87
server/tso.files/Formats/tsodata/MessageItem.cs
Executable file
87
server/tso.files/Formats/tsodata/MessageItem.cs
Executable file
|
@ -0,0 +1,87 @@
|
|||
using FSO.Files.Utils;
|
||||
using System.IO;
|
||||
|
||||
namespace FSO.Files.Formats.tsodata
|
||||
{
|
||||
public class MessageItem
|
||||
{
|
||||
public static int CURRENT_VERSION = 1;
|
||||
public int Version = CURRENT_VERSION;
|
||||
public int ID;
|
||||
public uint SenderID;
|
||||
public uint TargetID;
|
||||
public string Subject;
|
||||
public string Body;
|
||||
public string SenderName;
|
||||
public long Time;
|
||||
public int Type; //(message/vote/club/maxis/tso/house/roommate/call)
|
||||
public int Subtype; //(urgent?)
|
||||
public int ReadState;
|
||||
public int? ReplyID;
|
||||
|
||||
public MessageItem()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public MessageItem(Stream stream)
|
||||
{
|
||||
Read(stream);
|
||||
}
|
||||
|
||||
public void Save(Stream stream) {
|
||||
using (var writer = IoWriter.FromStream(stream))
|
||||
{
|
||||
writer.WriteCString("FSOI", 4);
|
||||
writer.WriteInt32(Version);
|
||||
writer.WriteInt32(ID);
|
||||
writer.WriteUInt32(SenderID);
|
||||
writer.WriteUInt32(TargetID);
|
||||
writer.WriteLongPascalString(Subject);
|
||||
writer.WriteLongPascalString(Body);
|
||||
writer.WriteLongPascalString(SenderName);
|
||||
writer.WriteInt64(Time);
|
||||
writer.WriteInt32(Type);
|
||||
writer.WriteInt32(Subtype);
|
||||
writer.WriteInt32(ReadState);
|
||||
writer.WriteByte((byte)((ReplyID == null) ? 0 : 1));
|
||||
if (ReplyID != null) writer.WriteInt32(ReplyID.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public void Read(Stream stream)
|
||||
{
|
||||
using (var reader = IoBuffer.FromStream(stream))
|
||||
{
|
||||
var magic = reader.ReadCString(4);
|
||||
Version = reader.ReadInt32();
|
||||
ID = reader.ReadInt32();
|
||||
SenderID = reader.ReadUInt32();
|
||||
TargetID = reader.ReadUInt32();
|
||||
Subject = reader.ReadLongPascalString();
|
||||
Body = reader.ReadLongPascalString();
|
||||
SenderName = reader.ReadLongPascalString();
|
||||
Time = reader.ReadInt64();
|
||||
Type = reader.ReadInt32();
|
||||
Subtype = reader.ReadInt32();
|
||||
ReadState = reader.ReadInt32();
|
||||
if (reader.ReadByte() > 0)
|
||||
{
|
||||
ReplyID = reader.ReadInt32();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum MessageSpecialType
|
||||
{
|
||||
Normal = 0,
|
||||
|
||||
//neighbourhoods
|
||||
Nominate = 1,
|
||||
Vote = 2,
|
||||
|
||||
AcceptNomination = 3,
|
||||
FreeVote = 4
|
||||
}
|
||||
}
|
517
server/tso.files/Formats/tsodata/TSODataDefinition.cs
Executable file
517
server/tso.files/Formats/tsodata/TSODataDefinition.cs
Executable file
|
@ -0,0 +1,517 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.IO;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace FSO.Files.Formats.tsodata
|
||||
{
|
||||
public class TSODataDefinition
|
||||
{
|
||||
public List<ListEntry> List1;
|
||||
public List<ListEntry> List2;
|
||||
public List<ListEntry> List3;
|
||||
public List<StringTableEntry> Strings;
|
||||
|
||||
public Struct[] Structs;
|
||||
public DerivedStruct[] DerivedStructs;
|
||||
public uint FileID;
|
||||
|
||||
public static TSODataDefinition Active;
|
||||
|
||||
private Dictionary<string, Struct> StructsByName = new Dictionary<string, Struct>();
|
||||
|
||||
public void Read(Stream stream)
|
||||
{
|
||||
using (var reader = new BinaryReader(stream))
|
||||
{
|
||||
FileID = reader.ReadUInt32();
|
||||
this.List1 = ReadList(reader, false);
|
||||
this.List2 = ReadList(reader, false);
|
||||
this.List3 = ReadList(reader, true);
|
||||
|
||||
var numStrings = reader.ReadUInt32();
|
||||
this.Strings = new List<StringTableEntry>();
|
||||
for (var i = 0; i < numStrings; i++)
|
||||
{
|
||||
var stringEntry = new StringTableEntry();
|
||||
stringEntry.ID = reader.ReadUInt32();
|
||||
stringEntry.Value = ReadNullTerminatedString(reader);
|
||||
stringEntry.Category = (StringTableType)reader.ReadByte();
|
||||
this.Strings.Add(stringEntry);
|
||||
}
|
||||
}
|
||||
Activate();
|
||||
}
|
||||
|
||||
public void Write(Stream stream)
|
||||
{
|
||||
using (var writer = new BinaryWriter(stream))
|
||||
{
|
||||
writer.Write(FileID);
|
||||
WriteList(List1, writer, false);
|
||||
WriteList(List2, writer, false);
|
||||
WriteList(List3, writer, true);
|
||||
|
||||
writer.Write(Strings.Count);
|
||||
foreach (var str in Strings)
|
||||
{
|
||||
writer.Write(str.ID);
|
||||
writer.Write(Encoding.ASCII.GetBytes(str.Value));
|
||||
writer.Write((byte)0);
|
||||
writer.Write((byte)str.Category);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Activate()
|
||||
{
|
||||
var Structs = new List<Struct>();
|
||||
var DerivedStructs = new List<DerivedStruct>();
|
||||
|
||||
//1st level structs. all fields are primitive types
|
||||
foreach (var item in List1)
|
||||
{
|
||||
var fields = new List<StructField>();
|
||||
|
||||
foreach (var field in item.Entries)
|
||||
{
|
||||
if (field.TypeStringID == 0xA99AF3AC) Console.WriteLine("unknown value: " + GetString(item.NameStringID) + "::" + GetString(field.NameStringID));
|
||||
fields.Add(new StructField
|
||||
{
|
||||
ID = field.NameStringID,
|
||||
Name = GetString(field.NameStringID),
|
||||
TypeID = field.TypeStringID,
|
||||
Classification = (StructFieldClassification)field.TypeClass,
|
||||
ParentID = item.NameStringID
|
||||
});
|
||||
}
|
||||
|
||||
Structs.Add(new Struct
|
||||
{
|
||||
ID = item.NameStringID,
|
||||
Name = GetString(item.NameStringID),
|
||||
Fields = fields.ToList()
|
||||
});
|
||||
}
|
||||
|
||||
//2nd level structs. fields can be first level structs
|
||||
//note: this might be a hard limit in tso, but it is not particularly important in freeso
|
||||
// the game will even behave correctly if a 2nd level references another 2nd level,
|
||||
// though a circular reference will likely still break everything.
|
||||
foreach (var item in List2)
|
||||
{
|
||||
var fields = new List<StructField>();
|
||||
|
||||
foreach (var field in item.Entries)
|
||||
{
|
||||
if (field.TypeStringID == 0xA99AF3AC) Console.WriteLine("unknown value: " + GetString(item.NameStringID) + "::" + GetString(field.NameStringID));
|
||||
fields.Add(new StructField
|
||||
{
|
||||
ID = field.NameStringID,
|
||||
Name = GetString(field.NameStringID),
|
||||
TypeID = field.TypeStringID,
|
||||
Classification = (StructFieldClassification)field.TypeClass,
|
||||
ParentID = item.NameStringID
|
||||
});
|
||||
}
|
||||
|
||||
Structs.Add(new Struct
|
||||
{
|
||||
ID = item.NameStringID,
|
||||
Name = GetString(item.NameStringID),
|
||||
Fields = fields.ToList()
|
||||
});
|
||||
}
|
||||
|
||||
//derived structs. serve as valid subsets of fields of an entity that the client can request.
|
||||
foreach (var item in List3)
|
||||
{
|
||||
var fields = new List<DerivedStructFieldMask>();
|
||||
|
||||
foreach (var field in item.Entries)
|
||||
{
|
||||
if (field.TypeStringID == 0xA99AF3AC) Console.WriteLine("unknown value: " + GetString(item.NameStringID) + "::" + GetString(field.NameStringID));
|
||||
fields.Add(new DerivedStructFieldMask
|
||||
{
|
||||
ID = field.NameStringID,
|
||||
Name = GetString(field.NameStringID),
|
||||
Type = (DerivedStructFieldMaskType)field.TypeClass
|
||||
});
|
||||
}
|
||||
|
||||
DerivedStructs.Add(new DerivedStruct
|
||||
{
|
||||
ID = item.NameStringID,
|
||||
Parent = item.ParentStringID,
|
||||
Name = GetString(item.NameStringID),
|
||||
FieldMasks = fields.ToArray()
|
||||
});
|
||||
}
|
||||
|
||||
this.Structs = Structs.ToArray();
|
||||
this.DerivedStructs = DerivedStructs.ToArray();
|
||||
|
||||
foreach (var _struct in Structs)
|
||||
{
|
||||
StructsByName.Add(_struct.Name, _struct);
|
||||
}
|
||||
|
||||
//InjectStructs();
|
||||
Active = this;
|
||||
}
|
||||
|
||||
private void InjectStructs()
|
||||
{
|
||||
//this is just an example of how to do this.
|
||||
//todo: a format we can easily create and read from to provide these new fields
|
||||
|
||||
StructsByName["Lot"].Fields.Add(new StructField()
|
||||
{
|
||||
Name = "Lot_SkillGamemode",
|
||||
ID = 0xaabbccdd,
|
||||
Classification = StructFieldClassification.SingleField,
|
||||
ParentID = StructsByName["Lot"].ID,
|
||||
TypeID = 1768755593 //uint32
|
||||
});
|
||||
|
||||
var fields = DerivedStructs[17].FieldMasks.ToList();
|
||||
fields.Add(new DerivedStructFieldMask()
|
||||
{
|
||||
ID = 0xaabbccdd,
|
||||
Name = "Lot_SkillGamemode",
|
||||
Type = DerivedStructFieldMaskType.KEEP
|
||||
});
|
||||
DerivedStructs[17].FieldMasks = fields.ToArray();
|
||||
}
|
||||
|
||||
public Struct GetStructFromValue(object value)
|
||||
{
|
||||
if (value == null) { return null; }
|
||||
return GetStruct(value.GetType());
|
||||
}
|
||||
|
||||
public Struct GetStruct(Type type)
|
||||
{
|
||||
return GetStruct(type.Name);
|
||||
}
|
||||
|
||||
public Struct GetStruct(string name)
|
||||
{
|
||||
if (StructsByName.ContainsKey(name))
|
||||
{
|
||||
return StructsByName[name];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Struct GetStruct(uint id)
|
||||
{
|
||||
return Structs.FirstOrDefault(x => x.ID == id);
|
||||
}
|
||||
|
||||
public StringTableType GetStringType(uint id)
|
||||
{
|
||||
var item = Strings.FirstOrDefault(x => x.ID == id);
|
||||
if (item == null)
|
||||
{
|
||||
return StringTableType.Field;
|
||||
}
|
||||
return item.Category;
|
||||
}
|
||||
|
||||
public string GetString(uint id)
|
||||
{
|
||||
var item = Strings.FirstOrDefault(x => x.ID == id);
|
||||
if (item == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return item.Value;
|
||||
}
|
||||
|
||||
|
||||
public void SetString(uint id, string value)
|
||||
{
|
||||
var item = Strings.FirstOrDefault(x => x.ID == id);
|
||||
if (item == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
item.Value = value;
|
||||
}
|
||||
|
||||
public void RemoveString(uint id)
|
||||
{
|
||||
Strings.RemoveAll(x => x.ID == id);
|
||||
}
|
||||
|
||||
public string GetString(List<StringTableEntry> strings, uint id)
|
||||
{
|
||||
var item = strings.FirstOrDefault(x => x.ID == id);
|
||||
if (item == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
return item.Value;
|
||||
}
|
||||
|
||||
private string ReadNullTerminatedString(BinaryReader reader)
|
||||
{
|
||||
var result = "";
|
||||
while (true)
|
||||
{
|
||||
var ch = (char)reader.ReadByte();
|
||||
if (ch == '\0')
|
||||
{
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
result += ch;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<ListEntry> ReadList(BinaryReader reader, bool parentID)
|
||||
{
|
||||
var list1Count = reader.ReadUInt32();
|
||||
|
||||
var list1 = new List<ListEntry>();
|
||||
for (int i = 0; i < list1Count; i++)
|
||||
{
|
||||
var entry = new ListEntry();
|
||||
entry.Parent = this;
|
||||
entry.NameStringID = reader.ReadUInt32();
|
||||
if (parentID)
|
||||
{
|
||||
entry.ParentStringID = reader.ReadUInt32();
|
||||
}
|
||||
entry.Entries = new List<ListEntryEntry>();
|
||||
|
||||
var subEntryCount = reader.ReadUInt32();
|
||||
for (int y = 0; y < subEntryCount; y++)
|
||||
{
|
||||
var subEntry = new ListEntryEntry();
|
||||
subEntry.Parent = entry;
|
||||
subEntry.NameStringID = reader.ReadUInt32();
|
||||
subEntry.TypeClass = reader.ReadByte();
|
||||
if (!parentID)
|
||||
{
|
||||
subEntry.TypeStringID = reader.ReadUInt32();
|
||||
}
|
||||
entry.Entries.Add(subEntry);
|
||||
}
|
||||
|
||||
list1.Add(entry);
|
||||
}
|
||||
return list1;
|
||||
}
|
||||
|
||||
private void WriteList(List<ListEntry> list, BinaryWriter writer, bool parentID)
|
||||
{
|
||||
writer.Write(list.Count);
|
||||
foreach (var entry in list)
|
||||
{
|
||||
writer.Write(entry.NameStringID);
|
||||
if (parentID) writer.Write(entry.ParentStringID);
|
||||
writer.Write(entry.Entries.Count);
|
||||
foreach (var subEntry in entry.Entries)
|
||||
{
|
||||
writer.Write(subEntry.NameStringID);
|
||||
writer.Write(subEntry.TypeClass);
|
||||
if (!parentID) writer.Write(subEntry.TypeStringID);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum StringTableType : byte
|
||||
{
|
||||
Field = 1,
|
||||
Primitive = 2,
|
||||
Level1 = 3,
|
||||
Level2 = 4,
|
||||
Derived = 5
|
||||
}
|
||||
|
||||
public class StringTableEntry
|
||||
{
|
||||
public uint ID;
|
||||
public string Value;
|
||||
public StringTableType Category;
|
||||
}
|
||||
|
||||
public class ListEntry
|
||||
{
|
||||
public TSODataDefinition Parent;
|
||||
|
||||
public uint NameStringID { get; set; }
|
||||
public uint ParentStringID { get; set; }
|
||||
public List<ListEntryEntry> Entries;
|
||||
}
|
||||
|
||||
public class ListEntryEntry
|
||||
{
|
||||
public ListEntry Parent;
|
||||
|
||||
public uint NameStringID;
|
||||
public byte TypeClass;
|
||||
public uint TypeStringID;
|
||||
|
||||
[Category("Struct Properties")]
|
||||
[Description("The name for this field/struct. (ONLY FOR STRUCTS)")]
|
||||
public string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
return Parent.Parent.GetString(NameStringID);
|
||||
}
|
||||
set
|
||||
{
|
||||
Parent.Parent.SetString(NameStringID, value);
|
||||
}
|
||||
}
|
||||
|
||||
[Category("Struct Properties")]
|
||||
[Description("The type this field should have. (ONLY FOR STRUCTS)")]
|
||||
[TypeConverter(typeof(TypeSelector))]
|
||||
public string FieldType {
|
||||
get
|
||||
{
|
||||
return Parent.Parent.GetString(TypeStringID);
|
||||
}
|
||||
set
|
||||
{
|
||||
TypeStringID = Parent.Parent.Strings.First(x => x.Value == value).ID;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("Struct Properties")]
|
||||
[Description("What kind of collection this field is. (ONLY FOR STRUCTS)")]
|
||||
public StructFieldClassification FieldClass {
|
||||
get {
|
||||
return (StructFieldClassification)TypeClass;
|
||||
}
|
||||
set
|
||||
{
|
||||
TypeClass = (byte)value;
|
||||
}
|
||||
}
|
||||
|
||||
private class TypeSelector : TypeConverter
|
||||
{
|
||||
public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
|
||||
{
|
||||
return new StandardValuesCollection(
|
||||
TSODataDefinition.Active.Strings
|
||||
.Where(x => x.Category == StringTableType.Level1 || x.Category == StringTableType.Primitive)
|
||||
.OrderBy(x => x.Category)
|
||||
.ThenBy(x => x.Value)
|
||||
.Select(x => x.Value)
|
||||
.ToList()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
[Category("Mask Properties")]
|
||||
[TypeConverter(typeof(NameSelector))]
|
||||
[Description("The field to mask. (ONLY FOR MASKS)")]
|
||||
public string MaskField
|
||||
{
|
||||
get
|
||||
{
|
||||
return Parent.Parent.GetString(NameStringID);
|
||||
}
|
||||
set
|
||||
{
|
||||
NameStringID = Parent.Parent.Strings.First(x => x.Value == value).ID;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("Mask Properties")]
|
||||
[Description("If this field should be kept or removed for this request. (ONLY FOR MASKS)")]
|
||||
public DerivedStructFieldMaskType MaskMode
|
||||
{
|
||||
get
|
||||
{
|
||||
return (DerivedStructFieldMaskType)TypeClass;
|
||||
}
|
||||
set
|
||||
{
|
||||
TypeClass = (byte)value;
|
||||
}
|
||||
}
|
||||
|
||||
private class NameSelector : TypeConverter
|
||||
{
|
||||
public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
|
||||
{
|
||||
return new StandardValuesCollection(
|
||||
TSODataDefinition.Active.Strings
|
||||
.Where(x => x.Category == StringTableType.Field)
|
||||
.OrderBy(x => x.Category)
|
||||
.ThenBy(x => x.Value)
|
||||
.Select(x => x.Value)
|
||||
.ToList()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class Struct {
|
||||
public uint ID;
|
||||
public string Name;
|
||||
|
||||
public List<StructField> Fields;
|
||||
}
|
||||
|
||||
public class StructField {
|
||||
public uint ID;
|
||||
public string Name;
|
||||
public StructFieldClassification Classification;
|
||||
public uint TypeID;
|
||||
public uint ParentID;
|
||||
}
|
||||
|
||||
public enum StructFieldClassification
|
||||
{
|
||||
SingleField = 0,
|
||||
Map = 1,
|
||||
List = 2
|
||||
}
|
||||
|
||||
public class DerivedStruct
|
||||
{
|
||||
public uint ID;
|
||||
public string Name;
|
||||
public uint Parent;
|
||||
|
||||
public DerivedStructFieldMask[] FieldMasks;
|
||||
}
|
||||
|
||||
public class DerivedStructFieldMask
|
||||
{
|
||||
public uint ID;
|
||||
public string Name;
|
||||
public DerivedStructFieldMaskType Type;
|
||||
}
|
||||
|
||||
public enum DerivedStructFieldMaskType
|
||||
{
|
||||
KEEP = 0x01,
|
||||
REMOVE = 0x02
|
||||
}
|
||||
}
|
183
server/tso.files/Formats/tsodata/TSOp.cs
Executable file
183
server/tso.files/Formats/tsodata/TSOp.cs
Executable file
|
@ -0,0 +1,183 @@
|
|||
using FSO.Files.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using deltaq;
|
||||
|
||||
namespace TSOVersionPatcher
|
||||
{
|
||||
public class TSOp
|
||||
{
|
||||
public List<FileEntry> Patches;
|
||||
public List<FileEntry> Additions;
|
||||
public List<string> Deletions;
|
||||
|
||||
private Stream Str;
|
||||
|
||||
public TSOp(Stream str)
|
||||
{
|
||||
Str = str;
|
||||
using (var io = IoBuffer.FromStream(str, ByteOrder.LITTLE_ENDIAN))
|
||||
{
|
||||
var magic = io.ReadCString(4);
|
||||
if (magic != "TSOp")
|
||||
throw new Exception("Not a TSO patch file!");
|
||||
var version = io.ReadInt32();
|
||||
|
||||
var ips = io.ReadCString(4);
|
||||
if (ips != "IPS_")
|
||||
throw new Exception("Invalid Patch Chunk!");
|
||||
|
||||
var patchCount = io.ReadInt32();
|
||||
Patches = new List<FileEntry>();
|
||||
for (int i = 0; i < patchCount; i++)
|
||||
{
|
||||
Patches.Add(new FileEntry()
|
||||
{
|
||||
FileTarget = io.ReadVariableLengthPascalString().Replace('\\', '/'),
|
||||
Length = io.ReadInt32(),
|
||||
Offset = str.Position
|
||||
});
|
||||
str.Seek(Patches.Last().Length, SeekOrigin.Current);
|
||||
}
|
||||
|
||||
|
||||
var add = io.ReadCString(4);
|
||||
if (add != "ADD_")
|
||||
throw new Exception("Invalid Addition Chunk!");
|
||||
|
||||
var addCount = io.ReadInt32();
|
||||
Additions = new List<FileEntry>();
|
||||
for (int i = 0; i < addCount; i++)
|
||||
{
|
||||
Additions.Add(new FileEntry()
|
||||
{
|
||||
FileTarget = io.ReadVariableLengthPascalString().Replace('\\', '/'),
|
||||
Length = io.ReadInt32(),
|
||||
Offset = str.Position
|
||||
});
|
||||
str.Seek(Additions.Last().Length, SeekOrigin.Current);
|
||||
}
|
||||
|
||||
var del = io.ReadCString(4);
|
||||
if (del != "DEL_")
|
||||
throw new Exception("Invalid Deletion Chunk!");
|
||||
|
||||
var delCount = io.ReadInt32();
|
||||
Deletions = new List<string>();
|
||||
for (int i = 0; i < delCount; i++)
|
||||
{
|
||||
Deletions.Add(io.ReadVariableLengthPascalString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RecursiveDirectoryScan(string folder, HashSet<string> fileNames, string basePath)
|
||||
{
|
||||
var files = Directory.GetFiles(folder);
|
||||
foreach (var file in files)
|
||||
{
|
||||
fileNames.Add(GetRelativePath(basePath, file));
|
||||
}
|
||||
|
||||
var dirs = Directory.GetDirectories(folder);
|
||||
foreach (var dir in dirs)
|
||||
{
|
||||
RecursiveDirectoryScan(dir, fileNames, basePath);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetRelativePath(string relativeTo, string path)
|
||||
{
|
||||
if (relativeTo.EndsWith("/") || relativeTo.EndsWith("\\")) relativeTo += "/";
|
||||
var uri = new Uri(relativeTo);
|
||||
var rel = Uri.UnescapeDataString(uri.MakeRelativeUri(new Uri(path)).ToString()).Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
|
||||
if (rel.Contains(Path.DirectorySeparatorChar.ToString()) == false)
|
||||
{
|
||||
rel = $".{ Path.DirectorySeparatorChar }{ rel }";
|
||||
}
|
||||
return rel;
|
||||
}
|
||||
|
||||
public void Apply(string source, string dest, Action<string, float> progress)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (source != dest)
|
||||
{
|
||||
progress("Copying Unchanged Files...", 0);
|
||||
//if our destination folder is different,
|
||||
//copy unchanged files first.
|
||||
var sourceFiles = new HashSet<string>();
|
||||
RecursiveDirectoryScan(source, sourceFiles, source);
|
||||
|
||||
sourceFiles.ExceptWith(new HashSet<string>(Patches.Select(x => x.FileTarget)));
|
||||
sourceFiles.ExceptWith(new HashSet<string>(Deletions));
|
||||
|
||||
foreach (var file in sourceFiles)
|
||||
{
|
||||
var destP = Path.Combine(dest, file);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(destP));
|
||||
|
||||
File.Copy(Path.Combine(source, file), destP);
|
||||
}
|
||||
}
|
||||
|
||||
var reader = new BinaryReader(Str);
|
||||
int total = Patches.Count + Additions.Count + Deletions.Count;
|
||||
int fileNum = 0;
|
||||
foreach (var patch in Patches)
|
||||
{
|
||||
progress($"Patching {patch.FileTarget}...", fileNum / (float)total);
|
||||
var path = Path.Combine(source, patch.FileTarget);
|
||||
var dpath = Path.Combine(dest, patch.FileTarget);
|
||||
var data = File.ReadAllBytes(path);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(dpath));
|
||||
|
||||
Str.Seek(patch.Offset, SeekOrigin.Begin);
|
||||
var patchd = reader.ReadBytes(patch.Length);
|
||||
BsPatch.Apply(data, patchd, File.Open(dpath, FileMode.Create, FileAccess.Write, FileShare.None));
|
||||
fileNum++;
|
||||
}
|
||||
|
||||
foreach (var add in Additions)
|
||||
{
|
||||
progress($"Adding {add.FileTarget}...", fileNum / (float)total);
|
||||
var dpath = Path.Combine(dest, add.FileTarget);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(dpath));
|
||||
|
||||
Str.Seek(add.Offset, SeekOrigin.Begin);
|
||||
var addData = reader.ReadBytes(add.Length);
|
||||
File.WriteAllBytes(dpath, addData);
|
||||
fileNum++;
|
||||
}
|
||||
|
||||
foreach (var del in Deletions)
|
||||
{
|
||||
try
|
||||
{
|
||||
progress($"Deleting {del}...", fileNum / (float)total);
|
||||
File.Delete(Path.Combine(dest, del));
|
||||
fileNum++;
|
||||
}
|
||||
catch
|
||||
{
|
||||
//file not found. not important - we wanted it deleted anyways.
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
progress(e.ToString(), -1f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class FileEntry
|
||||
{
|
||||
public string FileTarget;
|
||||
public long Offset;
|
||||
public int Length;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue