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