mirror of
https://github.com/simtactics/mysimulation.git
synced 2025-07-04 21:50:35 -04:00
Added FSO.Files for use with the API server
Don't ask me. FreeSO is the prime example of dependency hell.
This commit is contained in:
parent
4b5e584eeb
commit
8fec258215
104 changed files with 14653 additions and 163 deletions
91
server/tso.files/Formats/tsodata/BulletinItem.cs
Executable file
91
server/tso.files/Formats/tsodata/BulletinItem.cs
Executable file
|
@ -0,0 +1,91 @@
|
|||
using FSO.Files.Utils;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace FSO.Files.Formats.tsodata
|
||||
{
|
||||
public class BulletinItem
|
||||
{
|
||||
public static int CURRENT_VERSION = 1;
|
||||
public int Version = CURRENT_VERSION;
|
||||
public uint ID;
|
||||
public uint NhoodID;
|
||||
public uint SenderID;
|
||||
|
||||
public string Subject;
|
||||
public string Body;
|
||||
public string SenderName;
|
||||
|
||||
public long Time;
|
||||
|
||||
public BulletinType Type;
|
||||
public BulletinFlags Flags;
|
||||
|
||||
public uint LotID; //optional: for lot advertisements.
|
||||
|
||||
public BulletinItem()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public BulletinItem(Stream stream)
|
||||
{
|
||||
Read(stream);
|
||||
}
|
||||
|
||||
public void Save(Stream stream)
|
||||
{
|
||||
using (var writer = IoWriter.FromStream(stream))
|
||||
{
|
||||
writer.WriteCString("FSOB", 4);
|
||||
writer.WriteInt32(Version);
|
||||
writer.WriteUInt32(ID);
|
||||
writer.WriteUInt32(NhoodID);
|
||||
writer.WriteUInt32(SenderID);
|
||||
|
||||
writer.WriteLongPascalString(Subject);
|
||||
writer.WriteLongPascalString(Body);
|
||||
writer.WriteLongPascalString(SenderName);
|
||||
writer.WriteInt64(Time);
|
||||
writer.WriteInt32((int)Type);
|
||||
writer.WriteInt32((int)Flags);
|
||||
|
||||
writer.WriteUInt32(LotID);
|
||||
}
|
||||
}
|
||||
|
||||
public void Read(Stream stream)
|
||||
{
|
||||
using (var reader = IoBuffer.FromStream(stream))
|
||||
{
|
||||
var magic = reader.ReadCString(4);
|
||||
Version = reader.ReadInt32();
|
||||
ID = reader.ReadUInt32();
|
||||
NhoodID = reader.ReadUInt32();
|
||||
SenderID = reader.ReadUInt32();
|
||||
|
||||
Subject = reader.ReadLongPascalString();
|
||||
Body = reader.ReadLongPascalString();
|
||||
SenderName = reader.ReadLongPascalString();
|
||||
Time = reader.ReadInt64();
|
||||
Type = (BulletinType)reader.ReadInt32();
|
||||
Flags = (BulletinFlags)reader.ReadInt32();
|
||||
|
||||
LotID = reader.ReadUInt32();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum BulletinType
|
||||
{
|
||||
Mayor = 0,
|
||||
System = 1,
|
||||
Community = 2,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum BulletinFlags
|
||||
{
|
||||
PromotedByMayor = 1
|
||||
}
|
||||
}
|
87
server/tso.files/Formats/tsodata/MessageItem.cs
Executable file
87
server/tso.files/Formats/tsodata/MessageItem.cs
Executable file
|
@ -0,0 +1,87 @@
|
|||
using FSO.Files.Utils;
|
||||
using System.IO;
|
||||
|
||||
namespace FSO.Files.Formats.tsodata
|
||||
{
|
||||
public class MessageItem
|
||||
{
|
||||
public static int CURRENT_VERSION = 1;
|
||||
public int Version = CURRENT_VERSION;
|
||||
public int ID;
|
||||
public uint SenderID;
|
||||
public uint TargetID;
|
||||
public string Subject;
|
||||
public string Body;
|
||||
public string SenderName;
|
||||
public long Time;
|
||||
public int Type; //(message/vote/club/maxis/tso/house/roommate/call)
|
||||
public int Subtype; //(urgent?)
|
||||
public int ReadState;
|
||||
public int? ReplyID;
|
||||
|
||||
public MessageItem()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public MessageItem(Stream stream)
|
||||
{
|
||||
Read(stream);
|
||||
}
|
||||
|
||||
public void Save(Stream stream) {
|
||||
using (var writer = IoWriter.FromStream(stream))
|
||||
{
|
||||
writer.WriteCString("FSOI", 4);
|
||||
writer.WriteInt32(Version);
|
||||
writer.WriteInt32(ID);
|
||||
writer.WriteUInt32(SenderID);
|
||||
writer.WriteUInt32(TargetID);
|
||||
writer.WriteLongPascalString(Subject);
|
||||
writer.WriteLongPascalString(Body);
|
||||
writer.WriteLongPascalString(SenderName);
|
||||
writer.WriteInt64(Time);
|
||||
writer.WriteInt32(Type);
|
||||
writer.WriteInt32(Subtype);
|
||||
writer.WriteInt32(ReadState);
|
||||
writer.WriteByte((byte)((ReplyID == null) ? 0 : 1));
|
||||
if (ReplyID != null) writer.WriteInt32(ReplyID.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public void Read(Stream stream)
|
||||
{
|
||||
using (var reader = IoBuffer.FromStream(stream))
|
||||
{
|
||||
var magic = reader.ReadCString(4);
|
||||
Version = reader.ReadInt32();
|
||||
ID = reader.ReadInt32();
|
||||
SenderID = reader.ReadUInt32();
|
||||
TargetID = reader.ReadUInt32();
|
||||
Subject = reader.ReadLongPascalString();
|
||||
Body = reader.ReadLongPascalString();
|
||||
SenderName = reader.ReadLongPascalString();
|
||||
Time = reader.ReadInt64();
|
||||
Type = reader.ReadInt32();
|
||||
Subtype = reader.ReadInt32();
|
||||
ReadState = reader.ReadInt32();
|
||||
if (reader.ReadByte() > 0)
|
||||
{
|
||||
ReplyID = reader.ReadInt32();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum MessageSpecialType
|
||||
{
|
||||
Normal = 0,
|
||||
|
||||
//neighbourhoods
|
||||
Nominate = 1,
|
||||
Vote = 2,
|
||||
|
||||
AcceptNomination = 3,
|
||||
FreeVote = 4
|
||||
}
|
||||
}
|
517
server/tso.files/Formats/tsodata/TSODataDefinition.cs
Executable file
517
server/tso.files/Formats/tsodata/TSODataDefinition.cs
Executable file
|
@ -0,0 +1,517 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.IO;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace FSO.Files.Formats.tsodata
|
||||
{
|
||||
public class TSODataDefinition
|
||||
{
|
||||
public List<ListEntry> List1;
|
||||
public List<ListEntry> List2;
|
||||
public List<ListEntry> List3;
|
||||
public List<StringTableEntry> Strings;
|
||||
|
||||
public Struct[] Structs;
|
||||
public DerivedStruct[] DerivedStructs;
|
||||
public uint FileID;
|
||||
|
||||
public static TSODataDefinition Active;
|
||||
|
||||
private Dictionary<string, Struct> StructsByName = new Dictionary<string, Struct>();
|
||||
|
||||
public void Read(Stream stream)
|
||||
{
|
||||
using (var reader = new BinaryReader(stream))
|
||||
{
|
||||
FileID = reader.ReadUInt32();
|
||||
this.List1 = ReadList(reader, false);
|
||||
this.List2 = ReadList(reader, false);
|
||||
this.List3 = ReadList(reader, true);
|
||||
|
||||
var numStrings = reader.ReadUInt32();
|
||||
this.Strings = new List<StringTableEntry>();
|
||||
for (var i = 0; i < numStrings; i++)
|
||||
{
|
||||
var stringEntry = new StringTableEntry();
|
||||
stringEntry.ID = reader.ReadUInt32();
|
||||
stringEntry.Value = ReadNullTerminatedString(reader);
|
||||
stringEntry.Category = (StringTableType)reader.ReadByte();
|
||||
this.Strings.Add(stringEntry);
|
||||
}
|
||||
}
|
||||
Activate();
|
||||
}
|
||||
|
||||
public void Write(Stream stream)
|
||||
{
|
||||
using (var writer = new BinaryWriter(stream))
|
||||
{
|
||||
writer.Write(FileID);
|
||||
WriteList(List1, writer, false);
|
||||
WriteList(List2, writer, false);
|
||||
WriteList(List3, writer, true);
|
||||
|
||||
writer.Write(Strings.Count);
|
||||
foreach (var str in Strings)
|
||||
{
|
||||
writer.Write(str.ID);
|
||||
writer.Write(Encoding.ASCII.GetBytes(str.Value));
|
||||
writer.Write((byte)0);
|
||||
writer.Write((byte)str.Category);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Activate()
|
||||
{
|
||||
var Structs = new List<Struct>();
|
||||
var DerivedStructs = new List<DerivedStruct>();
|
||||
|
||||
//1st level structs. all fields are primitive types
|
||||
foreach (var item in List1)
|
||||
{
|
||||
var fields = new List<StructField>();
|
||||
|
||||
foreach (var field in item.Entries)
|
||||
{
|
||||
if (field.TypeStringID == 0xA99AF3AC) Console.WriteLine("unknown value: " + GetString(item.NameStringID) + "::" + GetString(field.NameStringID));
|
||||
fields.Add(new StructField
|
||||
{
|
||||
ID = field.NameStringID,
|
||||
Name = GetString(field.NameStringID),
|
||||
TypeID = field.TypeStringID,
|
||||
Classification = (StructFieldClassification)field.TypeClass,
|
||||
ParentID = item.NameStringID
|
||||
});
|
||||
}
|
||||
|
||||
Structs.Add(new Struct
|
||||
{
|
||||
ID = item.NameStringID,
|
||||
Name = GetString(item.NameStringID),
|
||||
Fields = fields.ToList()
|
||||
});
|
||||
}
|
||||
|
||||
//2nd level structs. fields can be first level structs
|
||||
//note: this might be a hard limit in tso, but it is not particularly important in freeso
|
||||
// the game will even behave correctly if a 2nd level references another 2nd level,
|
||||
// though a circular reference will likely still break everything.
|
||||
foreach (var item in List2)
|
||||
{
|
||||
var fields = new List<StructField>();
|
||||
|
||||
foreach (var field in item.Entries)
|
||||
{
|
||||
if (field.TypeStringID == 0xA99AF3AC) Console.WriteLine("unknown value: " + GetString(item.NameStringID) + "::" + GetString(field.NameStringID));
|
||||
fields.Add(new StructField
|
||||
{
|
||||
ID = field.NameStringID,
|
||||
Name = GetString(field.NameStringID),
|
||||
TypeID = field.TypeStringID,
|
||||
Classification = (StructFieldClassification)field.TypeClass,
|
||||
ParentID = item.NameStringID
|
||||
});
|
||||
}
|
||||
|
||||
Structs.Add(new Struct
|
||||
{
|
||||
ID = item.NameStringID,
|
||||
Name = GetString(item.NameStringID),
|
||||
Fields = fields.ToList()
|
||||
});
|
||||
}
|
||||
|
||||
//derived structs. serve as valid subsets of fields of an entity that the client can request.
|
||||
foreach (var item in List3)
|
||||
{
|
||||
var fields = new List<DerivedStructFieldMask>();
|
||||
|
||||
foreach (var field in item.Entries)
|
||||
{
|
||||
if (field.TypeStringID == 0xA99AF3AC) Console.WriteLine("unknown value: " + GetString(item.NameStringID) + "::" + GetString(field.NameStringID));
|
||||
fields.Add(new DerivedStructFieldMask
|
||||
{
|
||||
ID = field.NameStringID,
|
||||
Name = GetString(field.NameStringID),
|
||||
Type = (DerivedStructFieldMaskType)field.TypeClass
|
||||
});
|
||||
}
|
||||
|
||||
DerivedStructs.Add(new DerivedStruct
|
||||
{
|
||||
ID = item.NameStringID,
|
||||
Parent = item.ParentStringID,
|
||||
Name = GetString(item.NameStringID),
|
||||
FieldMasks = fields.ToArray()
|
||||
});
|
||||
}
|
||||
|
||||
this.Structs = Structs.ToArray();
|
||||
this.DerivedStructs = DerivedStructs.ToArray();
|
||||
|
||||
foreach (var _struct in Structs)
|
||||
{
|
||||
StructsByName.Add(_struct.Name, _struct);
|
||||
}
|
||||
|
||||
//InjectStructs();
|
||||
Active = this;
|
||||
}
|
||||
|
||||
private void InjectStructs()
|
||||
{
|
||||
//this is just an example of how to do this.
|
||||
//todo: a format we can easily create and read from to provide these new fields
|
||||
|
||||
StructsByName["Lot"].Fields.Add(new StructField()
|
||||
{
|
||||
Name = "Lot_SkillGamemode",
|
||||
ID = 0xaabbccdd,
|
||||
Classification = StructFieldClassification.SingleField,
|
||||
ParentID = StructsByName["Lot"].ID,
|
||||
TypeID = 1768755593 //uint32
|
||||
});
|
||||
|
||||
var fields = DerivedStructs[17].FieldMasks.ToList();
|
||||
fields.Add(new DerivedStructFieldMask()
|
||||
{
|
||||
ID = 0xaabbccdd,
|
||||
Name = "Lot_SkillGamemode",
|
||||
Type = DerivedStructFieldMaskType.KEEP
|
||||
});
|
||||
DerivedStructs[17].FieldMasks = fields.ToArray();
|
||||
}
|
||||
|
||||
public Struct GetStructFromValue(object value)
|
||||
{
|
||||
if (value == null) { return null; }
|
||||
return GetStruct(value.GetType());
|
||||
}
|
||||
|
||||
public Struct GetStruct(Type type)
|
||||
{
|
||||
return GetStruct(type.Name);
|
||||
}
|
||||
|
||||
public Struct GetStruct(string name)
|
||||
{
|
||||
if (StructsByName.ContainsKey(name))
|
||||
{
|
||||
return StructsByName[name];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Struct GetStruct(uint id)
|
||||
{
|
||||
return Structs.FirstOrDefault(x => x.ID == id);
|
||||
}
|
||||
|
||||
public StringTableType GetStringType(uint id)
|
||||
{
|
||||
var item = Strings.FirstOrDefault(x => x.ID == id);
|
||||
if (item == null)
|
||||
{
|
||||
return StringTableType.Field;
|
||||
}
|
||||
return item.Category;
|
||||
}
|
||||
|
||||
public string GetString(uint id)
|
||||
{
|
||||
var item = Strings.FirstOrDefault(x => x.ID == id);
|
||||
if (item == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return item.Value;
|
||||
}
|
||||
|
||||
|
||||
public void SetString(uint id, string value)
|
||||
{
|
||||
var item = Strings.FirstOrDefault(x => x.ID == id);
|
||||
if (item == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
item.Value = value;
|
||||
}
|
||||
|
||||
public void RemoveString(uint id)
|
||||
{
|
||||
Strings.RemoveAll(x => x.ID == id);
|
||||
}
|
||||
|
||||
public string GetString(List<StringTableEntry> strings, uint id)
|
||||
{
|
||||
var item = strings.FirstOrDefault(x => x.ID == id);
|
||||
if (item == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
return item.Value;
|
||||
}
|
||||
|
||||
private string ReadNullTerminatedString(BinaryReader reader)
|
||||
{
|
||||
var result = "";
|
||||
while (true)
|
||||
{
|
||||
var ch = (char)reader.ReadByte();
|
||||
if (ch == '\0')
|
||||
{
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
result += ch;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<ListEntry> ReadList(BinaryReader reader, bool parentID)
|
||||
{
|
||||
var list1Count = reader.ReadUInt32();
|
||||
|
||||
var list1 = new List<ListEntry>();
|
||||
for (int i = 0; i < list1Count; i++)
|
||||
{
|
||||
var entry = new ListEntry();
|
||||
entry.Parent = this;
|
||||
entry.NameStringID = reader.ReadUInt32();
|
||||
if (parentID)
|
||||
{
|
||||
entry.ParentStringID = reader.ReadUInt32();
|
||||
}
|
||||
entry.Entries = new List<ListEntryEntry>();
|
||||
|
||||
var subEntryCount = reader.ReadUInt32();
|
||||
for (int y = 0; y < subEntryCount; y++)
|
||||
{
|
||||
var subEntry = new ListEntryEntry();
|
||||
subEntry.Parent = entry;
|
||||
subEntry.NameStringID = reader.ReadUInt32();
|
||||
subEntry.TypeClass = reader.ReadByte();
|
||||
if (!parentID)
|
||||
{
|
||||
subEntry.TypeStringID = reader.ReadUInt32();
|
||||
}
|
||||
entry.Entries.Add(subEntry);
|
||||
}
|
||||
|
||||
list1.Add(entry);
|
||||
}
|
||||
return list1;
|
||||
}
|
||||
|
||||
private void WriteList(List<ListEntry> list, BinaryWriter writer, bool parentID)
|
||||
{
|
||||
writer.Write(list.Count);
|
||||
foreach (var entry in list)
|
||||
{
|
||||
writer.Write(entry.NameStringID);
|
||||
if (parentID) writer.Write(entry.ParentStringID);
|
||||
writer.Write(entry.Entries.Count);
|
||||
foreach (var subEntry in entry.Entries)
|
||||
{
|
||||
writer.Write(subEntry.NameStringID);
|
||||
writer.Write(subEntry.TypeClass);
|
||||
if (!parentID) writer.Write(subEntry.TypeStringID);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum StringTableType : byte
|
||||
{
|
||||
Field = 1,
|
||||
Primitive = 2,
|
||||
Level1 = 3,
|
||||
Level2 = 4,
|
||||
Derived = 5
|
||||
}
|
||||
|
||||
public class StringTableEntry
|
||||
{
|
||||
public uint ID;
|
||||
public string Value;
|
||||
public StringTableType Category;
|
||||
}
|
||||
|
||||
public class ListEntry
|
||||
{
|
||||
public TSODataDefinition Parent;
|
||||
|
||||
public uint NameStringID { get; set; }
|
||||
public uint ParentStringID { get; set; }
|
||||
public List<ListEntryEntry> Entries;
|
||||
}
|
||||
|
||||
public class ListEntryEntry
|
||||
{
|
||||
public ListEntry Parent;
|
||||
|
||||
public uint NameStringID;
|
||||
public byte TypeClass;
|
||||
public uint TypeStringID;
|
||||
|
||||
[Category("Struct Properties")]
|
||||
[Description("The name for this field/struct. (ONLY FOR STRUCTS)")]
|
||||
public string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
return Parent.Parent.GetString(NameStringID);
|
||||
}
|
||||
set
|
||||
{
|
||||
Parent.Parent.SetString(NameStringID, value);
|
||||
}
|
||||
}
|
||||
|
||||
[Category("Struct Properties")]
|
||||
[Description("The type this field should have. (ONLY FOR STRUCTS)")]
|
||||
[TypeConverter(typeof(TypeSelector))]
|
||||
public string FieldType {
|
||||
get
|
||||
{
|
||||
return Parent.Parent.GetString(TypeStringID);
|
||||
}
|
||||
set
|
||||
{
|
||||
TypeStringID = Parent.Parent.Strings.First(x => x.Value == value).ID;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("Struct Properties")]
|
||||
[Description("What kind of collection this field is. (ONLY FOR STRUCTS)")]
|
||||
public StructFieldClassification FieldClass {
|
||||
get {
|
||||
return (StructFieldClassification)TypeClass;
|
||||
}
|
||||
set
|
||||
{
|
||||
TypeClass = (byte)value;
|
||||
}
|
||||
}
|
||||
|
||||
private class TypeSelector : TypeConverter
|
||||
{
|
||||
public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
|
||||
{
|
||||
return new StandardValuesCollection(
|
||||
TSODataDefinition.Active.Strings
|
||||
.Where(x => x.Category == StringTableType.Level1 || x.Category == StringTableType.Primitive)
|
||||
.OrderBy(x => x.Category)
|
||||
.ThenBy(x => x.Value)
|
||||
.Select(x => x.Value)
|
||||
.ToList()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
[Category("Mask Properties")]
|
||||
[TypeConverter(typeof(NameSelector))]
|
||||
[Description("The field to mask. (ONLY FOR MASKS)")]
|
||||
public string MaskField
|
||||
{
|
||||
get
|
||||
{
|
||||
return Parent.Parent.GetString(NameStringID);
|
||||
}
|
||||
set
|
||||
{
|
||||
NameStringID = Parent.Parent.Strings.First(x => x.Value == value).ID;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("Mask Properties")]
|
||||
[Description("If this field should be kept or removed for this request. (ONLY FOR MASKS)")]
|
||||
public DerivedStructFieldMaskType MaskMode
|
||||
{
|
||||
get
|
||||
{
|
||||
return (DerivedStructFieldMaskType)TypeClass;
|
||||
}
|
||||
set
|
||||
{
|
||||
TypeClass = (byte)value;
|
||||
}
|
||||
}
|
||||
|
||||
private class NameSelector : TypeConverter
|
||||
{
|
||||
public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
|
||||
{
|
||||
return new StandardValuesCollection(
|
||||
TSODataDefinition.Active.Strings
|
||||
.Where(x => x.Category == StringTableType.Field)
|
||||
.OrderBy(x => x.Category)
|
||||
.ThenBy(x => x.Value)
|
||||
.Select(x => x.Value)
|
||||
.ToList()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class Struct {
|
||||
public uint ID;
|
||||
public string Name;
|
||||
|
||||
public List<StructField> Fields;
|
||||
}
|
||||
|
||||
public class StructField {
|
||||
public uint ID;
|
||||
public string Name;
|
||||
public StructFieldClassification Classification;
|
||||
public uint TypeID;
|
||||
public uint ParentID;
|
||||
}
|
||||
|
||||
public enum StructFieldClassification
|
||||
{
|
||||
SingleField = 0,
|
||||
Map = 1,
|
||||
List = 2
|
||||
}
|
||||
|
||||
public class DerivedStruct
|
||||
{
|
||||
public uint ID;
|
||||
public string Name;
|
||||
public uint Parent;
|
||||
|
||||
public DerivedStructFieldMask[] FieldMasks;
|
||||
}
|
||||
|
||||
public class DerivedStructFieldMask
|
||||
{
|
||||
public uint ID;
|
||||
public string Name;
|
||||
public DerivedStructFieldMaskType Type;
|
||||
}
|
||||
|
||||
public enum DerivedStructFieldMaskType
|
||||
{
|
||||
KEEP = 0x01,
|
||||
REMOVE = 0x02
|
||||
}
|
||||
}
|
183
server/tso.files/Formats/tsodata/TSOp.cs
Executable file
183
server/tso.files/Formats/tsodata/TSOp.cs
Executable file
|
@ -0,0 +1,183 @@
|
|||
using FSO.Files.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using deltaq;
|
||||
|
||||
namespace TSOVersionPatcher
|
||||
{
|
||||
public class TSOp
|
||||
{
|
||||
public List<FileEntry> Patches;
|
||||
public List<FileEntry> Additions;
|
||||
public List<string> Deletions;
|
||||
|
||||
private Stream Str;
|
||||
|
||||
public TSOp(Stream str)
|
||||
{
|
||||
Str = str;
|
||||
using (var io = IoBuffer.FromStream(str, ByteOrder.LITTLE_ENDIAN))
|
||||
{
|
||||
var magic = io.ReadCString(4);
|
||||
if (magic != "TSOp")
|
||||
throw new Exception("Not a TSO patch file!");
|
||||
var version = io.ReadInt32();
|
||||
|
||||
var ips = io.ReadCString(4);
|
||||
if (ips != "IPS_")
|
||||
throw new Exception("Invalid Patch Chunk!");
|
||||
|
||||
var patchCount = io.ReadInt32();
|
||||
Patches = new List<FileEntry>();
|
||||
for (int i = 0; i < patchCount; i++)
|
||||
{
|
||||
Patches.Add(new FileEntry()
|
||||
{
|
||||
FileTarget = io.ReadVariableLengthPascalString().Replace('\\', '/'),
|
||||
Length = io.ReadInt32(),
|
||||
Offset = str.Position
|
||||
});
|
||||
str.Seek(Patches.Last().Length, SeekOrigin.Current);
|
||||
}
|
||||
|
||||
|
||||
var add = io.ReadCString(4);
|
||||
if (add != "ADD_")
|
||||
throw new Exception("Invalid Addition Chunk!");
|
||||
|
||||
var addCount = io.ReadInt32();
|
||||
Additions = new List<FileEntry>();
|
||||
for (int i = 0; i < addCount; i++)
|
||||
{
|
||||
Additions.Add(new FileEntry()
|
||||
{
|
||||
FileTarget = io.ReadVariableLengthPascalString().Replace('\\', '/'),
|
||||
Length = io.ReadInt32(),
|
||||
Offset = str.Position
|
||||
});
|
||||
str.Seek(Additions.Last().Length, SeekOrigin.Current);
|
||||
}
|
||||
|
||||
var del = io.ReadCString(4);
|
||||
if (del != "DEL_")
|
||||
throw new Exception("Invalid Deletion Chunk!");
|
||||
|
||||
var delCount = io.ReadInt32();
|
||||
Deletions = new List<string>();
|
||||
for (int i = 0; i < delCount; i++)
|
||||
{
|
||||
Deletions.Add(io.ReadVariableLengthPascalString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RecursiveDirectoryScan(string folder, HashSet<string> fileNames, string basePath)
|
||||
{
|
||||
var files = Directory.GetFiles(folder);
|
||||
foreach (var file in files)
|
||||
{
|
||||
fileNames.Add(GetRelativePath(basePath, file));
|
||||
}
|
||||
|
||||
var dirs = Directory.GetDirectories(folder);
|
||||
foreach (var dir in dirs)
|
||||
{
|
||||
RecursiveDirectoryScan(dir, fileNames, basePath);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetRelativePath(string relativeTo, string path)
|
||||
{
|
||||
if (relativeTo.EndsWith("/") || relativeTo.EndsWith("\\")) relativeTo += "/";
|
||||
var uri = new Uri(relativeTo);
|
||||
var rel = Uri.UnescapeDataString(uri.MakeRelativeUri(new Uri(path)).ToString()).Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
|
||||
if (rel.Contains(Path.DirectorySeparatorChar.ToString()) == false)
|
||||
{
|
||||
rel = $".{ Path.DirectorySeparatorChar }{ rel }";
|
||||
}
|
||||
return rel;
|
||||
}
|
||||
|
||||
public void Apply(string source, string dest, Action<string, float> progress)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (source != dest)
|
||||
{
|
||||
progress("Copying Unchanged Files...", 0);
|
||||
//if our destination folder is different,
|
||||
//copy unchanged files first.
|
||||
var sourceFiles = new HashSet<string>();
|
||||
RecursiveDirectoryScan(source, sourceFiles, source);
|
||||
|
||||
sourceFiles.ExceptWith(new HashSet<string>(Patches.Select(x => x.FileTarget)));
|
||||
sourceFiles.ExceptWith(new HashSet<string>(Deletions));
|
||||
|
||||
foreach (var file in sourceFiles)
|
||||
{
|
||||
var destP = Path.Combine(dest, file);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(destP));
|
||||
|
||||
File.Copy(Path.Combine(source, file), destP);
|
||||
}
|
||||
}
|
||||
|
||||
var reader = new BinaryReader(Str);
|
||||
int total = Patches.Count + Additions.Count + Deletions.Count;
|
||||
int fileNum = 0;
|
||||
foreach (var patch in Patches)
|
||||
{
|
||||
progress($"Patching {patch.FileTarget}...", fileNum / (float)total);
|
||||
var path = Path.Combine(source, patch.FileTarget);
|
||||
var dpath = Path.Combine(dest, patch.FileTarget);
|
||||
var data = File.ReadAllBytes(path);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(dpath));
|
||||
|
||||
Str.Seek(patch.Offset, SeekOrigin.Begin);
|
||||
var patchd = reader.ReadBytes(patch.Length);
|
||||
BsPatch.Apply(data, patchd, File.Open(dpath, FileMode.Create, FileAccess.Write, FileShare.None));
|
||||
fileNum++;
|
||||
}
|
||||
|
||||
foreach (var add in Additions)
|
||||
{
|
||||
progress($"Adding {add.FileTarget}...", fileNum / (float)total);
|
||||
var dpath = Path.Combine(dest, add.FileTarget);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(dpath));
|
||||
|
||||
Str.Seek(add.Offset, SeekOrigin.Begin);
|
||||
var addData = reader.ReadBytes(add.Length);
|
||||
File.WriteAllBytes(dpath, addData);
|
||||
fileNum++;
|
||||
}
|
||||
|
||||
foreach (var del in Deletions)
|
||||
{
|
||||
try
|
||||
{
|
||||
progress($"Deleting {del}...", fileNum / (float)total);
|
||||
File.Delete(Path.Combine(dest, del));
|
||||
fileNum++;
|
||||
}
|
||||
catch
|
||||
{
|
||||
//file not found. not important - we wanted it deleted anyways.
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
progress(e.ToString(), -1f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class FileEntry
|
||||
{
|
||||
public string FileTarget;
|
||||
public long Offset;
|
||||
public int Length;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue