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:
Tony Bark 2024-05-01 04:38:12 -04:00
parent 4b5e584eeb
commit 8fec258215
104 changed files with 14653 additions and 163 deletions

View 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;
}
}

View 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
}
}

View 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() { }
}
}

View 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.
}
}

View 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)"
}
}

View 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;
}
}
}
}

View 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
}
}

View 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;
}
}
}

View 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);
}
}
}

View 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
{
}
}

View 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
}
}

View 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;
}
}
}

View file

@ -0,0 +1,6 @@
namespace FSO.Files.Formats.IFF.Chunks
{
public class FAMs : STR
{
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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();
}
}
}
}

View 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
{
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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);
}
}
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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}]";
}
}
}

View 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;
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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
}
}

View 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
{
}
}

View 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);
}
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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++;
}
}
}

View 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
}
}
}

View 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];
}
}

View 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();
}
}
}
}

View 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;
}
}
}

View 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);
}
}
}

View 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
}
}

View 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,
}
}

View 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;
}
}
}

View 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!
}
}

View 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?
}
}
}

View 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);
}
}
}
}
}

View 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,
}
}

View 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;
}
}
}

View 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;
}
}

View 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;
}
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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;
}
}