commit bbce3c5cac75c5da53356dbbabd4e45eec5e706f Author: wincent Date: Wed Nov 13 19:45:48 2019 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..048a113 --- /dev/null +++ b/.gitignore @@ -0,0 +1,571 @@ + +# Created by https://www.gitignore.io/api/rider,csharp,visualstudio +# Edit at https://www.gitignore.io/?templates=rider,csharp,visualstudio + +### Csharp ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +### Rider ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### VisualStudio ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files + +# User-specific files (MonoDevelop/Xamarin Studio) + +# Build results + +# Visual Studio 2015/2017 cache/options directory +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files + +# MSTest test Results + +# NUNIT + +# Build Results of an ATL Project + +# Benchmark Results + +# .NET Core + +# StyleCop + +# Files built by Visual Studio + +# Chutzpah Test files + +# Visual C++ cache files + +# Visual Studio profiler + +# Visual Studio Trace Files + +# TFS 2012 Local Workspace + +# Guidance Automation Toolkit + +# ReSharper is a .NET coding add-in + +# JustCode is a .NET coding add-in + +# TeamCity is a build add-in + +# DotCover is a Code Coverage Tool + +# AxoCover is a Code Coverage Tool + +# Visual Studio code coverage results + +# NCrunch + +# MightyMoose + +# Web workbench (sass) + +# Installshield output folder + +# DocProject is a documentation generator add-in + +# Click-Once directory + +# Publish Web Output +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted + +# NuGet Packages +# The packages folder can be ignored because of Package Restore +# except build/, which is used as an MSBuild target. +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files + +# Microsoft Azure Build Output + +# Microsoft Azure Emulator + +# Windows Store app package directories and files + +# Visual Studio cache files +# files ending in .cache can be ignored +# but keep track of directories ending in .cache + +# Others + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) + +# SQL Server files + +# Business Intelligence projects + +# Microsoft Fakes + +# GhostDoc plugin setting file + +# Node.js Tools for Visual Studio + +# Visual Studio 6 build log + +# Visual Studio 6 workspace options file + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) + +# Visual Studio LightSwitch build output + +# Paket dependency manager + +# FAKE - F# Make + +# JetBrains Rider + +# CodeRush personal settings + +# Python Tools for Visual Studio (PTVS) + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio + +# Telerik's JustMock configuration file + +# BizTalk build output + +# OpenCover UI analysis results + +# Azure Stream Analytics local run output + +# MSBuild Binary and Structured Log + +# NVidia Nsight GPU debugger configuration file + +# MFractors (Xamarin productivity tool) working folder + +# Local History for Visual Studio + +# End of https://www.gitignore.io/api/rider,csharp,visualstudio + +.DS_Store + diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..aeed8ee --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "RakDotNet.IO"] + path = "RakDotNet.IO" + url = https://github.com/yuwui/RakDotNet.IO.git diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..04b6ef8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ + +# Default ignored files +/.idea.InfectedRose/.idea/workspace.xml \ No newline at end of file diff --git a/InfectedRose.Core/Extensions/BitReaderExtensions.cs b/InfectedRose.Core/Extensions/BitReaderExtensions.cs new file mode 100644 index 0000000..c51f449 --- /dev/null +++ b/InfectedRose.Core/Extensions/BitReaderExtensions.cs @@ -0,0 +1,46 @@ +using System; +using System.Numerics; +using RakDotNet.IO; + +namespace InfectedRose.Core +{ + public static class BitReaderExtensions + { + public static string ReadNiString(this BitReader @this, bool wide = false, bool small = false) + { + var len = small ? @this.Read() : @this.Read(); + var str = new char[len]; + + for (var i = 0; i < len; i++) + { + str[i] = (char) (wide ? @this.Read() : @this.Read()); + } + + + return new string(str); + } + + public static Quaternion ReadNiQuaternion(this BitReader @this) + { + return new Quaternion + { + W = @this.Read(), + X = @this.Read(), + Y = @this.Read(), + Z = @this.Read() + }; + } + + public static byte[] ReadBuffer(this BitReader @this, uint length) + { + var buffer = new byte[length]; + + for (var i = 0; i < length; i++) + { + buffer[i] = @this.Read(); + } + + return buffer; + } + } +} \ No newline at end of file diff --git a/InfectedRose.Core/Extensions/BitWriterExtensions.cs b/InfectedRose.Core/Extensions/BitWriterExtensions.cs new file mode 100644 index 0000000..840dcc3 --- /dev/null +++ b/InfectedRose.Core/Extensions/BitWriterExtensions.cs @@ -0,0 +1,36 @@ +using System.Numerics; +using RakDotNet.IO; + +namespace InfectedRose.Core +{ + public static class BitWriterExtensions + { + public static void WriteNiString(this BitWriter @this, string str, bool wide = false, bool small = false) + { + if (small) @this.Write((byte) str.Length); + else @this.Write((uint) str.Length); + + foreach (var c in str) + { + if (wide) @this.Write((ushort) c); + else @this.Write((byte) c); + } + } + + public static void WriteNiQuaternion(this BitWriter @this, Quaternion quaternion) + { + @this.Write(quaternion.W); + @this.Write(quaternion.X); + @this.Write(quaternion.Y); + @this.Write(quaternion.Z); + } + + public static void Write(this BitWriter @this, byte[] buffer) + { + foreach (var value in buffer) + { + @this.Write(value); + } + } + } +} \ No newline at end of file diff --git a/InfectedRose.Core/IConstruct.cs b/InfectedRose.Core/IConstruct.cs new file mode 100644 index 0000000..fef599c --- /dev/null +++ b/InfectedRose.Core/IConstruct.cs @@ -0,0 +1,8 @@ +using RakDotNet.IO; + +namespace InfectedRose.Core +{ + public interface IConstruct : ISerializable, IDeserializable + { + } +} \ No newline at end of file diff --git a/InfectedRose.Core/InfectedRose.Core.csproj b/InfectedRose.Core/InfectedRose.Core.csproj new file mode 100644 index 0000000..4b62469 --- /dev/null +++ b/InfectedRose.Core/InfectedRose.Core.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp3.0 + + + + + + + diff --git a/InfectedRose.Database/AccessDatabase.cs b/InfectedRose.Database/AccessDatabase.cs new file mode 100644 index 0000000..b181511 --- /dev/null +++ b/InfectedRose.Database/AccessDatabase.cs @@ -0,0 +1,122 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using InfectedRose.Database.Generic; + +namespace InfectedRose.Database +{ + public class AccessDatabase : IList + { + public DatabaseFile File { get; private set; } + + public AccessDatabase(DatabaseFile file) + { + File = file; + } + + public IEnumerator
GetEnumerator() + { + return File.TableHeader.Tables.Select(t => new Table(t.info, t.data)).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public void Add(Table item) + { + if (item == default) return; + + var list = File.TableHeader.Tables.ToList(); + list.Add((item.Info, item.Data)); + File.TableHeader.Tables = list.ToArray(); + } + + public void Clear() + { + File.TableHeader.Tables = new (FdbColumnHeader info, FdbRowBucket data)[0]; + } + + public bool Contains(Table item) + { + return this.Any(value => value == item); + } + + public void CopyTo(Table[] array, int arrayIndex) + { + var list = File.TableHeader.Tables.Select(t => new Table(t.info, t.data)).ToList(); + list.CopyTo(array, arrayIndex); + } + + public bool Remove(Table item) + { + if (item == default) return false; + + var removal = File.TableHeader.Tables.FirstOrDefault(t => t.info == item.Info && t.data == item.Data); + + if (removal == default) return false; + + var list = File.TableHeader.Tables.ToList(); + list.Remove(removal); + File.TableHeader.Tables = list.ToArray(); + + return true; + } + + public int Count => File.TableHeader.Tables.Length; + + public bool IsReadOnly => false; + + public int IndexOf(Table item) + { + if (item == default) return -1; + + return File.TableHeader.Tables.ToList().IndexOf( + File.TableHeader.Tables.First(t => t.info == item.Info && t.data == item.Data) + ); + } + + public void Insert(int index, Table item) + { + if (item == default) return; + + var list = File.TableHeader.Tables.ToList(); + list.Insert(index, (item.Info, item.Data)); + File.TableHeader.Tables = list.ToArray(); + } + + public void RemoveAt(int index) + { + var list = File.TableHeader.Tables.ToList(); + list.RemoveAt(index); + File.TableHeader.Tables = list.ToArray(); + } + + public Table this[int index] + { + get + { + var (info, data) = File.TableHeader.Tables[index]; + + return new Table(info, data); + } + set => File.TableHeader.Tables[index] = (value.Info, value.Data); + } + + public Table this[string name] + { + get + { + foreach (var (info, data) in File.TableHeader.Tables) + { + if (info.TableName == name) + return new Table(info, data); + } + + return default; + } + } + } +} \ No newline at end of file diff --git a/InfectedRose.Database/Column.cs b/InfectedRose.Database/Column.cs new file mode 100644 index 0000000..0881174 --- /dev/null +++ b/InfectedRose.Database/Column.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace InfectedRose.Database +{ + public class Column : IList + { + internal FdbRowInfo Data { get; private set; } + + internal Table Table { get; private set; } + + internal Column(FdbRowInfo data, Table table) + { + Data = data; + Table = table; + } + + public IEnumerator GetEnumerator() + { + int index = default; + + return Data.DataHeader.Data.Fields.Select( + field => new Field(Data.DataHeader.Data, index++, Table) + ).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public void Add(Field item) + { + throw new NotSupportedException("Fields should fallow the table structure, and their signature should not be changed."); + } + + public void Clear() + { + throw new NotSupportedException("Fields should fallow the table structure, and their signature should not be changed."); + } + + public bool Contains(Field item) + { + if (item == default) return false; + + int index = default; + + var list = Data.DataHeader.Data.Fields.Select( + field => new Field(Data.DataHeader.Data, index++, Table) + ).ToList(); + + return list.Any(c => c.Data == item.Data && c.Index == item.Index); + } + + public void CopyTo(Field[] array, int arrayIndex) + { + int index = default; + + var list = Data.DataHeader.Data.Fields.Select( + field => new Field(Data.DataHeader.Data, index++, Table) + ).ToList(); + + list.CopyTo(array, arrayIndex); + } + + public bool Remove(Field item) + { + throw new NotSupportedException("Fields should fallow the table structure, and their signature should not be changed."); + } + + public int Count => Data.DataHeader.Data.Fields.Length; + + public bool IsReadOnly => false; + + public int IndexOf(Field item) + { + return item?.Index ?? -1; + } + + public void Insert(int index, Field item) + { + throw new NotSupportedException("Fields should fallow the table structure, and their signature should not be changed."); + } + + public void RemoveAt(int index) + { + throw new NotSupportedException("Fields should fallow the table structure, and their signature should not be changed."); + } + + public Field this[int index] + { + get => new Field(Data.DataHeader.Data, index, Table); + set => throw new NotSupportedException("Fields should fallow the table structure, and their signature should not be changed."); + } + + public Field this[string name] + { + get + { + int index = default; + + for (var i = 0; i < Table.TableInfo.Count; i++) + { + var column = Table.TableInfo[i]; + + if (column.Name == name) index = i; + } + + return new Field(Data.DataHeader.Data, index, Table); + } + } + } +} \ No newline at end of file diff --git a/InfectedRose.Database/ColumnInfo.cs b/InfectedRose.Database/ColumnInfo.cs new file mode 100644 index 0000000..d1a9011 --- /dev/null +++ b/InfectedRose.Database/ColumnInfo.cs @@ -0,0 +1,44 @@ +namespace InfectedRose.Database +{ + public class ColumnInfo + { + internal Table Table { get; private set; } + + internal int Index { get; private set; } + + public DataType Type + { + get => Table.Info.Data.Fields[Index].type; + set + { + var dataField = Table.Info.Data.Fields[Index]; + + dataField.type = value; + + Table.Info.Data.Fields[Index] = dataField; + } + } + + public string Name + { + get => Table.Info.Data.Fields[Index].name; + set + { + var dataField = Table.Info.Data.Fields[Index]; + + dataField.name = new FdbString + { + Value = value + }; + + Table.Info.Data.Fields[Index] = dataField; + } + } + + internal ColumnInfo(Table table, int index) + { + Table = table; + Index = index; + } + } +} \ No newline at end of file diff --git a/InfectedRose.Database/DataType.cs b/InfectedRose.Database/DataType.cs new file mode 100644 index 0000000..6881162 --- /dev/null +++ b/InfectedRose.Database/DataType.cs @@ -0,0 +1,15 @@ +namespace InfectedRose.Database +{ + public enum DataType : uint + { + Nothing, // can’t remember if those are just skipped/ignored or even showed up + Integer, + Unknown1, // never used? + Float, + Text, // called STRING in MSSQL? + Boolean, + Bigint, // or DATETIME? + Unknown2, // never used? + Varchar // called TEXT in MSSQL? + } +} \ No newline at end of file diff --git a/InfectedRose.Database/DatabaseData.cs b/InfectedRose.Database/DatabaseData.cs new file mode 100644 index 0000000..41eb723 --- /dev/null +++ b/InfectedRose.Database/DatabaseData.cs @@ -0,0 +1,11 @@ +using RakDotNet.IO; + +namespace InfectedRose.Database +{ + public abstract class DatabaseData : IDeserializable + { + internal abstract void Compile(DatabaseFile databaseFile); + + public abstract void Deserialize(BitReader reader); + } +} \ No newline at end of file diff --git a/InfectedRose.Database/DatabaseFile.cs b/InfectedRose.Database/DatabaseFile.cs new file mode 100644 index 0000000..899f7bc --- /dev/null +++ b/InfectedRose.Database/DatabaseFile.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using RakDotNet.IO; + +namespace InfectedRose.Database +{ + public class DatabaseFile : IDeserializable + { + public List Structure { get; set; } + + internal FdbTableHeader TableHeader { get; set; } + + public void Deserialize(BitReader reader) + { + var tableCount = reader.Read(); + + using (new DatabaseScope(reader)) + { + TableHeader = new FdbTableHeader(tableCount); + + TableHeader.Deserialize(reader); + } + } + + /// + /// Compile the database to a hash-map + /// + /// + /// This is a really long process and should be run in a Task. + /// + /// Compiled database + public byte[] Compile(Action onData) + { + Structure = new List + { + (uint) TableHeader.Tables.Length, + TableHeader + }; + + TableHeader.Compile(this); + + var fdb = new List(); + var pointers = new List<(DatabaseData, int)>(); + + int index = default; + foreach (var obj in Structure) + { + onData(++index); + + switch (obj) + { + case null: + fdb.AddRange(ToBytes(-1)); + break; + case DatabaseData data: + { + var pointer = pointers.Where(p => p.Item1 == data).ToArray(); + if (pointer.Any()) + { + var bytes = ToBytes(fdb.Count); + + foreach (var tuple in pointer) + { + pointers.Remove(tuple); + for (var j = 0; j < 4; j++) fdb[tuple.Item2 + j] = bytes[j]; + } + } + else + { + // Save pointers for last + pointers.Add((data, fdb.Count)); + + // Reserve space for pointer + fdb.AddRange(new byte[4]); + } + + break; + } + default: + fdb.AddRange(ToBytes(obj)); + break; + } + } + + return fdb.ToArray(); + } + + private static byte[] ToBytes(object obj) + { + if (!obj.GetType().IsValueType) + { + Console.WriteLine($"{obj} is not a valid struct"); + + return new byte[0]; + } + + var size = Marshal.SizeOf(obj); + var buf = new byte[size]; + var ptr = Marshal.AllocHGlobal(size); + + Marshal.StructureToPtr(obj, ptr, false); + Marshal.Copy(ptr, buf, 0, size); + + return buf; + } + } +} \ No newline at end of file diff --git a/InfectedRose.Database/DatabaseScope.cs b/InfectedRose.Database/DatabaseScope.cs new file mode 100644 index 0000000..da33d14 --- /dev/null +++ b/InfectedRose.Database/DatabaseScope.cs @@ -0,0 +1,30 @@ +using System; +using RakDotNet.IO; + +namespace InfectedRose.Database +{ + internal class DatabaseScope : IDisposable + { + private readonly long _current; + private readonly int _pointer; + private readonly BitReader _reader; + + public DatabaseScope(BitReader reader, bool signed = false) + { + _current = reader.BaseStream.Position + 4; + _pointer = signed ? reader.Read() : (int) reader.Read(); + if (_pointer != -1) reader.BaseStream.Position = _pointer; + _reader = reader; + } + + public void Dispose() + { + _reader.BaseStream.Position = _current; + } + + public static implicit operator bool(DatabaseScope s) + { + return s._pointer != -1; + } + } +} \ No newline at end of file diff --git a/InfectedRose.Database/FdbBitInt.cs b/InfectedRose.Database/FdbBitInt.cs new file mode 100644 index 0000000..ef16322 --- /dev/null +++ b/InfectedRose.Database/FdbBitInt.cs @@ -0,0 +1,22 @@ +using RakDotNet.IO; + +namespace InfectedRose.Database +{ + internal class FdbBitInt : DatabaseData + { + public long Value { get; set; } + + internal override void Compile(DatabaseFile databaseFile) + { + databaseFile.Structure.Add(this); + databaseFile.Structure.Add(Value); + } + + public override void Deserialize(BitReader reader) + { + using var s = new DatabaseScope(reader, true); + + if (s) Value = reader.Read(); + } + } +} \ No newline at end of file diff --git a/InfectedRose.Database/FdbColumnData.cs b/InfectedRose.Database/FdbColumnData.cs new file mode 100644 index 0000000..3015f8d --- /dev/null +++ b/InfectedRose.Database/FdbColumnData.cs @@ -0,0 +1,41 @@ +using RakDotNet.IO; + +namespace InfectedRose.Database +{ + internal class FdbColumnData : DatabaseData + { + private readonly uint _columnCount; + + public FdbColumnData(uint columnCount) + { + _columnCount = columnCount; + } + + public (DataType type, FdbString name)[] Fields { get; set; } + + internal override void Compile(DatabaseFile databaseFile) + { + databaseFile.Structure.Add(this); + for (var i = 0; i < Fields.Length; i++) + { + databaseFile.Structure.Add((uint) Fields[i].type); + databaseFile.Structure.Add(Fields[i].name); + } + + foreach (var s in Fields) s.name.Compile(databaseFile); + } + + public override void Deserialize(BitReader reader) + { + Fields = new (DataType type, FdbString name)[_columnCount]; + + for (var i = 0; i < _columnCount; i++) + { + Fields[i].type = (DataType) reader.Read(); + + Fields[i].name = new FdbString(); + Fields[i].name.Deserialize(reader); + } + } + } +} \ No newline at end of file diff --git a/InfectedRose.Database/FdbColumnHeader.cs b/InfectedRose.Database/FdbColumnHeader.cs new file mode 100644 index 0000000..1b84f8e --- /dev/null +++ b/InfectedRose.Database/FdbColumnHeader.cs @@ -0,0 +1,41 @@ +using RakDotNet.IO; + +namespace InfectedRose.Database +{ + internal class FdbColumnHeader : DatabaseData + { + public FdbString TableName { get; set; } + + public FdbColumnData Data { get; set; } + + internal override void Compile(DatabaseFile databaseFile) + { + databaseFile.Structure.Add(this); + databaseFile.Structure.Add((uint) Data.Fields.Length); + databaseFile.Structure.Add(TableName); + databaseFile.Structure.Add(Data); + + TableName.Compile(databaseFile); + Data.Compile(databaseFile); + } + + public override void Deserialize(BitReader reader) + { + var columnCount = reader.Read(); + + TableName = new FdbString(); + TableName.Deserialize(reader); + + using (new DatabaseScope(reader)) + { + Data = new FdbColumnData(columnCount); + Data.Deserialize(reader); + } + } + + public override string ToString() + { + return TableName; + } + } +} \ No newline at end of file diff --git a/InfectedRose.Database/FdbRowBucket.cs b/InfectedRose.Database/FdbRowBucket.cs new file mode 100644 index 0000000..5e16d3b --- /dev/null +++ b/InfectedRose.Database/FdbRowBucket.cs @@ -0,0 +1,51 @@ +using System; +using RakDotNet.IO; + +namespace InfectedRose.Database +{ + internal class FdbRowBucket : DatabaseData + { + public FdbRowHeader RowHeader { get; set; } + + internal override void Compile(DatabaseFile databaseFile) + { + databaseFile.Structure.Add(this); + Console.WriteLine(NextPowerOf2(RowHeader.RowInfos.Length)); + databaseFile.Structure.Add(NextPowerOf2(RowHeader.RowInfos.Length)); + + databaseFile.Structure.Add(RowHeader); + + RowHeader?.Compile(databaseFile); + } + + public override void Deserialize(BitReader reader) + { + var rowCount = reader.Read(); + + using var s = new DatabaseScope(reader, true); + + if (!s) return; + + RowHeader = new FdbRowHeader(rowCount); + RowHeader.Deserialize(reader); + } + + internal static uint NextPowerOf2(int n) + { + var count = 0; + + // First n in the below condition + // is for the case where n is 0 + if (n > 0 && (n & (n - 1)) == 0) + return (uint) n; + + while (n != 0) + { + n >>= 1; + count += 1; + } + + return (uint) (1 << count); + } + } +} \ No newline at end of file diff --git a/InfectedRose.Database/FdbRowData.cs b/InfectedRose.Database/FdbRowData.cs new file mode 100644 index 0000000..7412c89 --- /dev/null +++ b/InfectedRose.Database/FdbRowData.cs @@ -0,0 +1,80 @@ +using System; +using RakDotNet.IO; + +namespace InfectedRose.Database +{ + internal class FdbRowData : DatabaseData + { + public FdbRowData(uint columnCount) + { + Fields = new (DataType, object)[columnCount]; + } + + public (DataType type, object value)[] Fields { get; set; } + + internal override void Compile(DatabaseFile databaseFile) + { + databaseFile.Structure.Add(this); + + for (var i = 0; i < Fields.Length; i++) + { + databaseFile.Structure.Add((uint) Fields[i].type); + databaseFile.Structure.Add(Fields[i].value); + } + + foreach (var o in Fields) + if (o.value is DatabaseData data) + data.Compile(databaseFile); + } + + public override void Deserialize(BitReader reader) + { + for (var i = 0; i < Fields.Length; i++) + { + Fields[i].type = (DataType) reader.Read(); + + switch (Fields[i].type) + { + case DataType.Nothing: + Fields[i].value = reader.Read(); + break; + case DataType.Integer: + Fields[i].value = reader.Read(); + break; + case DataType.Unknown1: + Fields[i].value = reader.Read(); + break; + case DataType.Float: + Fields[i].value = reader.Read(); + break; + case DataType.Text: + var str = new FdbString(); + str.Deserialize(reader); + + Fields[i].value = str; + break; + case DataType.Boolean: + Fields[i].value = reader.Read() != 0; + break; + case DataType.Bigint: + var bigInt = new FdbBitInt(); + bigInt.Deserialize(reader); + + Fields[i].value = bigInt; + break; + case DataType.Unknown2: + Fields[i].value = reader.Read(); + break; + case DataType.Varchar: + var str1 = new FdbString(); + str1.Deserialize(reader); + + Fields[i].value = str1; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + } +} \ No newline at end of file diff --git a/InfectedRose.Database/FdbRowDataHeader.cs b/InfectedRose.Database/FdbRowDataHeader.cs new file mode 100644 index 0000000..0092a81 --- /dev/null +++ b/InfectedRose.Database/FdbRowDataHeader.cs @@ -0,0 +1,31 @@ +using RakDotNet.IO; + +namespace InfectedRose.Database +{ + internal class FdbRowDataHeader : DatabaseData + { + public FdbRowData Data { get; set; } + + internal override void Compile(DatabaseFile databaseFile) + { + databaseFile.Structure.Add(this); + databaseFile.Structure.Add((uint) Data.Fields.Length); + databaseFile.Structure.Add(Data); + + Data?.Compile(databaseFile); + } + + public override void Deserialize(BitReader reader) + { + var columnCount = reader.Read(); + + using var s = new DatabaseScope(reader, true); + + if (!s) return; + + Data = new FdbRowData(columnCount); + + Data.Deserialize(reader); + } + } +} \ No newline at end of file diff --git a/InfectedRose.Database/FdbRowHeader.cs b/InfectedRose.Database/FdbRowHeader.cs new file mode 100644 index 0000000..36d6a51 --- /dev/null +++ b/InfectedRose.Database/FdbRowHeader.cs @@ -0,0 +1,45 @@ +using RakDotNet.IO; + +namespace InfectedRose.Database +{ + internal class FdbRowHeader : DatabaseData + { + private readonly uint _rowCount; + + public FdbRowHeader(uint rowCount) + { + _rowCount = rowCount; + } + + public FdbRowInfo[] RowInfos; + + internal override void Compile(DatabaseFile databaseFile) + { + databaseFile.Structure.Add(this); + + foreach (var rowInfo in RowInfos) databaseFile.Structure.Add(rowInfo); + + for (var i = 0; i < FdbRowBucket.NextPowerOf2(RowInfos.Length) - RowInfos.Length; i++) + { + databaseFile.Structure.Add(-1); + } + + foreach (var rowInfo in RowInfos) rowInfo?.Compile(databaseFile); + } + + public override void Deserialize(BitReader reader) + { + RowInfos = new FdbRowInfo[_rowCount]; + + for (var i = 0; i < _rowCount; i++) + { + using var s = new DatabaseScope(reader, true); + + if (!s) continue; + + RowInfos[i] = new FdbRowInfo(); + RowInfos[i].Deserialize(reader); + } + } + } +} \ No newline at end of file diff --git a/InfectedRose.Database/FdbRowInfo.cs b/InfectedRose.Database/FdbRowInfo.cs new file mode 100644 index 0000000..ee0ffb2 --- /dev/null +++ b/InfectedRose.Database/FdbRowInfo.cs @@ -0,0 +1,47 @@ +using RakDotNet.IO; + +namespace InfectedRose.Database +{ + internal class FdbRowInfo : DatabaseData + { + public FdbRowDataHeader DataHeader { get; set; } + + public FdbRowInfo Linked { get; set; } + + internal override void Compile(DatabaseFile databaseFile) + { + databaseFile.Structure.Add(this); + databaseFile.Structure.Add(DataHeader); + + if (Linked == default) + databaseFile.Structure.Add(-1); + else + databaseFile.Structure.Add(Linked); + + DataHeader?.Compile(databaseFile); + Linked?.Compile(databaseFile); + } + + public override void Deserialize(BitReader reader) + { + using (var s = new DatabaseScope(reader, true)) + { + if (s) + { + DataHeader = new FdbRowDataHeader(); + + DataHeader.Deserialize(reader); + } + } + + using (var s = new DatabaseScope(reader, true)) + { + if (!s) return; + + Linked = new FdbRowInfo(); + + Linked.Deserialize(reader); + } + } + } +} \ No newline at end of file diff --git a/InfectedRose.Database/FdbString.cs b/InfectedRose.Database/FdbString.cs new file mode 100644 index 0000000..68d68ff --- /dev/null +++ b/InfectedRose.Database/FdbString.cs @@ -0,0 +1,45 @@ +using System.Text; +using RakDotNet.IO; + +namespace InfectedRose.Database +{ + internal class FdbString : DatabaseData + { + public string Value { get; set; } + + public static implicit operator string(FdbString s) + { + return s.Value; + } + + public override string ToString() + { + return Value; + } + + internal override void Compile(DatabaseFile databaseFile) + { + databaseFile.Structure.Add(this); + foreach (var c in Value) databaseFile.Structure.Add((byte) c); + + databaseFile.Structure.Add((byte) 0); + } + + public override void Deserialize(BitReader reader) + { + var builder = new StringBuilder(); + + using (new DatabaseScope(reader)) + { + while (true) + { + var c = reader.Read(); + if (c == 0) break; + builder.Append((char) c); + } + } + + Value = builder.ToString(); + } + } +} \ No newline at end of file diff --git a/InfectedRose.Database/FdbTableHeader.cs b/InfectedRose.Database/FdbTableHeader.cs new file mode 100644 index 0000000..3f14d07 --- /dev/null +++ b/InfectedRose.Database/FdbTableHeader.cs @@ -0,0 +1,53 @@ +using RakDotNet.IO; + +namespace InfectedRose.Database +{ + internal class FdbTableHeader : DatabaseData + { + private readonly uint _tableCount; + + public FdbTableHeader(uint tableCount) + { + _tableCount = tableCount; + } + + public (FdbColumnHeader info, FdbRowBucket data)[] Tables { get; set; } + + internal override void Compile(DatabaseFile databaseFile) + { + databaseFile.Structure.Add(this); + + for (var i = 0; i < Tables.Length; i++) + { + databaseFile.Structure.Add(Tables[i].info); + databaseFile.Structure.Add(Tables[i].data); + } + + for (var i = 0; i < Tables.Length; i++) + { + Tables[i].data.Compile(databaseFile); + Tables[i].info.Compile(databaseFile); + } + } + + public override void Deserialize(BitReader reader) + { + Tables = new (FdbColumnHeader info, FdbRowBucket data)[_tableCount]; + + for (var i = 0; i < _tableCount; i++) + { + using (new DatabaseScope(reader)) + { + Tables[i].info = new FdbColumnHeader(); + Tables[i].info.Deserialize(reader); + } + + using (new DatabaseScope(reader)) + { + Tables[i].data = new FdbRowBucket(); + Tables[i].data.Deserialize(reader); + } + } + } + } +} \ No newline at end of file diff --git a/InfectedRose.Database/Field.cs b/InfectedRose.Database/Field.cs new file mode 100644 index 0000000..9983beb --- /dev/null +++ b/InfectedRose.Database/Field.cs @@ -0,0 +1,48 @@ +namespace InfectedRose.Database +{ + public class Field + { + internal FdbRowData Data { get; private set; } + + internal int Index { get; private set; } + + public DataType Type + { + get => Data.Fields[Index].type; + set + { + var dataField = Data.Fields[Index]; + + dataField.type = value; + + Data.Fields[Index] = dataField; + } + } + + public object Value + { + get => Data.Fields[Index].value; + set + { + var dataField = Data.Fields[Index]; + + value = value switch + { + string str => new FdbString {Value = str}, + long lon => new FdbBitInt {Value = lon}, + _ => value + }; + + dataField.value = value; + + Data.Fields[Index] = dataField; + } + } + + internal Field(FdbRowData data, int index, Table table) + { + Data = data; + Index = index; + } + } +} \ No newline at end of file diff --git a/InfectedRose.Database/Generic/AccessDatabaseExtensions.cs b/InfectedRose.Database/Generic/AccessDatabaseExtensions.cs new file mode 100644 index 0000000..460d62c --- /dev/null +++ b/InfectedRose.Database/Generic/AccessDatabaseExtensions.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations.Schema; +using System.Reflection; + +namespace InfectedRose.Database.Generic +{ + public static class TableExtensions + { + public static TypedTable Typed(this AccessDatabase @this) where T : class + { + var type = typeof(T); + + var attribute = type.GetCustomAttribute(); + + var id = attribute?.Name ?? type.Name; + + var table = @this[id]; + + return new TypedTable(table.Info, table.Data); + } + } +} \ No newline at end of file diff --git a/InfectedRose.Database/Generic/TypedTable.cs b/InfectedRose.Database/Generic/TypedTable.cs new file mode 100644 index 0000000..d56f354 --- /dev/null +++ b/InfectedRose.Database/Generic/TypedTable.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Reflection; + +namespace InfectedRose.Database.Generic +{ + public class TypedTable : Table where T : class + { + private readonly Dictionary _managed; + + internal TypedTable(FdbColumnHeader info, FdbRowBucket data) : base(info, data) + { + _managed = new Dictionary(); + } + + public new T this[int index] + { + get + { + if (_managed.ContainsValue(index)) + { + return _managed.First(m => m.Value == index).Key; + } + + var type = typeof(T); + + var instance = (T) Activator.CreateInstance(type, true); + + var baseColumn = base[index]; + + foreach (var property in type.GetProperties()) + { + var attribute = property.GetCustomAttribute(); + + var id = attribute?.Name ?? property.Name; + + var data = baseColumn[id].Value; + + property.SetValue(instance, data); + } + + _managed[instance] = index; + + return instance; + } + } + + public new T Create() + { + base.Create(out var index); + + return this[index]; + } + + public void Save() + { + var type = typeof(T); + + var properties = type.GetProperties(); + + foreach (var (column, index) in _managed) + { + var baseColumn = base[index]; + + foreach (var property in properties) + { + var attribute = property.GetCustomAttribute(); + + var id = attribute?.Name ?? property.Name; + + var value = property.GetValue(column); + + var data = value switch + { + long lon => new FdbBitInt {Value = lon}, + string str => new FdbString {Value = str}, + _ => value + }; + + baseColumn[id].Value = data; + } + } + } + } +} \ No newline at end of file diff --git a/InfectedRose.Database/InfectedRose.Database.csproj b/InfectedRose.Database/InfectedRose.Database.csproj new file mode 100644 index 0000000..543ea48 --- /dev/null +++ b/InfectedRose.Database/InfectedRose.Database.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp3.0 + + + + + + + diff --git a/InfectedRose.Database/Table.cs b/InfectedRose.Database/Table.cs new file mode 100644 index 0000000..dfd2b9b --- /dev/null +++ b/InfectedRose.Database/Table.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace InfectedRose.Database +{ + public class Table : IList + { + internal FdbColumnHeader Info { get; private set; } + + internal FdbRowBucket Data { get; private set; } + + public string Name + { + get => Info.TableName; + set => Info.TableName = new FdbString + { + Value = value + }; + } + + public TableInfo TableInfo => new TableInfo(this); + + internal Table(FdbColumnHeader info, FdbRowBucket data) + { + Info = info; + Data = data; + } + + private List Fields + { + get + { + var columns = new List(); + + foreach (var info in Data.RowHeader.RowInfos) + { + var linked = info; + + while (linked != default) + { + columns.Add(new Column(linked, this)); + + linked = linked.Linked; + } + } + + return columns; + } + } + + public IEnumerator GetEnumerator() + { + return Fields.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public void Add(Column item) + { + if (item == default) return; + + var list = Data.RowHeader.RowInfos.ToList(); + + list.Add(item.Data); + + Data.RowHeader.RowInfos = list.ToArray(); + } + + public void Clear() + { + Data.RowHeader.RowInfos = new FdbRowInfo[0]; + } + + public bool Contains(Column item) + { + return Fields.Contains(item); + } + + public void CopyTo(Column[] array, int arrayIndex) + { + Fields.CopyTo(array, arrayIndex); + } + + public bool Remove(Column item) + { + if (item == default) return false; + + foreach (var info in Data.RowHeader.RowInfos) + { + var linked = info; + + if (linked == item.Data) + { + var fields = Data.RowHeader.RowInfos.ToList(); + + fields.Remove(linked); + + Data.RowHeader.RowInfos = fields.ToArray(); + + return true; + } + + while (linked != default) + { + if (linked.Linked == item.Data) + { + linked.Linked = default; + + return true; + } + + linked = linked.Linked; + } + } + + return false; + } + + public int Count => Fields.Count; + + public bool IsReadOnly => false; + + public int IndexOf(Column item) + { + return Fields.IndexOf(item); + } + + public void Insert(int index, Column item) + { + if (item == default) return; + + var list = Data.RowHeader.RowInfos.ToList(); + + list.Insert(index, item.Data); + + Data.RowHeader.RowInfos = list.ToArray(); + } + + public void RemoveAt(int index) + { + Remove(this[index]); + } + + public Column this[int index] + { + get => Fields[index]; + set => throw new NotSupportedException(); + } + + public Column Create() => Create(out _); + + public Column Create(out int index) + { + var list = Data.RowHeader.RowInfos.ToList(); + + var column = new FdbRowInfo + { + DataHeader = new FdbRowDataHeader + { + Data = new FdbRowData((uint) Info.Data.Fields.Length) + } + }; + + for (var i = 0; i < Info.Data.Fields.Length; i++) + { + var type = Info.Data.Fields[i].type; + + column.DataHeader.Data.Fields[i] = (type, GetDefault(type)); + } + + index = list.Count; + + list.Add(column); + + Data.RowHeader.RowInfos = list.ToArray(); + + return new Column(column, this); + } + + private object GetDefault(DataType type) + { + return type switch + { + DataType.Nothing => (object) 0, + DataType.Integer => 0, + DataType.Unknown1 => 0, + DataType.Float => 0, + DataType.Boolean => 0, + DataType.Unknown2 => 0, + DataType.Varchar => new FdbString(), + DataType.Text => new FdbString(), + DataType.Bigint => new FdbBitInt(), + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }; + } + + public override string ToString() + { + return Name; + } + } +} \ No newline at end of file diff --git a/InfectedRose.Database/TableInfo.cs b/InfectedRose.Database/TableInfo.cs new file mode 100644 index 0000000..ef12fc7 --- /dev/null +++ b/InfectedRose.Database/TableInfo.cs @@ -0,0 +1,120 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace InfectedRose.Database +{ + public class TableInfo : IList + { + internal Table Table { get; private set; } + + internal TableInfo(Table table) + { + Table = table; + } + + public IEnumerator GetEnumerator() + { + int index = default; + + return Table.Info.Data.Fields.Select( + field => new ColumnInfo(Table, index++) + ).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public void Add(ColumnInfo item) + { + if (item == default) return; + + var list = Table.Info.Data.Fields.ToList(); + + list.Add((item.Type, new FdbString + { + Value = item.Name + })); + + Table.Info.Data.Fields = list.ToArray(); + } + + public void Clear() + { + Table.Info.Data.Fields = new (DataType type, FdbString name)[0]; + } + + public bool Contains(ColumnInfo item) + { + return item != default && Table.Info.Data.Fields.Any(f => f.name == item.Name); + } + + public void CopyTo(ColumnInfo[] array, int arrayIndex) + { + int index = default; + + var list = Table.Info.Data.Fields.Select(field => new ColumnInfo(Table, index++)).ToList(); + + list.CopyTo(array, arrayIndex); + } + + public bool Remove(ColumnInfo item) + { + if (item == default) return false; + + var list = Table.Info.Data.Fields.ToList(); + + list.Remove(list.First(l => l.name == item.Name)); + + Table.Info.Data.Fields = list.ToArray(); + + return true; + } + + public int Count => Table.Info.Data.Fields.Length; + + public bool IsReadOnly => false; + + public int IndexOf(ColumnInfo item) + { + return item?.Index ?? -1; + } + + public void Insert(int index, ColumnInfo item) + { + if (item == default) return; + + var list = Table.Info.Data.Fields.ToList(); + + list.Insert(index, (item.Type, new FdbString + { + Value = item.Name + })); + + Table.Info.Data.Fields = list.ToArray(); + } + + public void RemoveAt(int index) + { + Remove(this[index]); + } + + public ColumnInfo this[int index] + { + get => new ColumnInfo(Table, index); + set + { + var list = Table.Info.Data.Fields.ToList(); + + list[index] = (value.Type, new FdbString + { + Value = value.Name + }); + + Table.Info.Data.Fields = list.ToArray(); + } + } + } +} \ No newline at end of file diff --git a/InfectedRose.Examples/InfectedRose.Examples.csproj b/InfectedRose.Examples/InfectedRose.Examples.csproj new file mode 100644 index 0000000..ea33805 --- /dev/null +++ b/InfectedRose.Examples/InfectedRose.Examples.csproj @@ -0,0 +1,15 @@ + + + + Exe + netcoreapp3.0 + + + + + + + + + + diff --git a/InfectedRose.Examples/Program.cs b/InfectedRose.Examples/Program.cs new file mode 100644 index 0000000..f7c46f9 --- /dev/null +++ b/InfectedRose.Examples/Program.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using InfectedRose.Database; +using InfectedRose.Database.Generic; +using InfectedRose.Luz; +using InfectedRose.Lvl; +using InfectedRose.Terrain; +using RakDotNet.IO; + +namespace InfectedRose.Examples +{ + internal static class Program + { + private static async Task Main(string[] args) + { + Console.Write("Type: "); + + if (!int.TryParse(Console.ReadLine(), out var i)) return; + + switch (i) + { + case 0: + await Terrain(args); + return; + case 1: + await Luz(args); + return; + case 2: + await Lvl(args); + return; + case 3: + await LvlTest(args); + return; + case 4: + await Database(args); + return; + } + } + + private static async Task Database(IReadOnlyList args) + { + string file; + + if (args.Count > 0) + { + file = args[0]; + } + else + { + Console.Write("File: "); + + file = Console.ReadLine(); + } + + var databaseFile = new DatabaseFile(); + + await using (var fileStream = File.OpenRead(file)) + { + using var reader = new BitReader(fileStream); + + databaseFile.Deserialize(reader); + } + + var database = new AccessDatabase(databaseFile); + + var table = database.Typed(); + + foreach (var col in table) + { + Console.WriteLine(col["ZoneId"].Value); + } + + var newColumn = table.Create(); + + newColumn.ZoneId = 42; + + table.Save(); + + var margin = 0; + var bytes = databaseFile.Compile(i => + { + margin++; + + if (margin != 100000) return; + + margin = default; + + Console.WriteLine($"{Math.Round(i / (double) databaseFile.Structure.Count * 100, 2)}%"); + }); + + File.WriteAllBytes($"{file}.new.fdb", bytes); + } + + private static async Task Terrain(IReadOnlyList args) + { + string file; + + if (args.Count > 0) + { + file = args[0]; + } + else + { + Console.Write("File: "); + + file = Console.ReadLine(); + } + + var terrain = new TerrainFile(); + + await using (var fileStream = File.OpenRead(file)) + { + using var reader = new BitReader(fileStream); + + terrain.Deserialize(reader); + + foreach (var chunk in terrain.Chunks) + { + var data = chunk.HeightMap.Data; + + var min = data.Min(); + var max = data.Max(); + + uint randomIndex = default; + for (var i = 0; i < data.Length; i++) + { + if (randomIndex++ == 100) + { + data[i] = max; + } + else if (randomIndex == 200) + { + data[i] = min; + + randomIndex = default; + } + } + + chunk.HeightMap.Data = data; + } + } + + await using var writeStream = File.OpenWrite(file); + using var writer = new BitWriter(writeStream); + + terrain.Serialize(writer); + + Console.Write("Done"); + } + + private static async Task Luz(IReadOnlyList args) + { + string file; + + if (args.Count > 0) + { + file = args[0]; + } + else + { + Console.Write("File: "); + + file = Console.ReadLine(); + } + + var luz = new LuzFile(); + + await using (var fileStream = File.OpenRead(file)) + { + using var reader = new BitReader(fileStream); + + luz.Deserialize(reader); + } + + await using var writeStream = File.OpenWrite(file); + using var writer = new BitWriter(writeStream); + + luz.Serialize(writer); + + Console.Write("Done"); + } + + private static async Task Lvl(IReadOnlyList args) + { + string file; + + if (args.Count > 0) + { + file = args[0]; + } + else + { + Console.Write("File: "); + + file = Console.ReadLine(); + } + + var lvl = new LvlFile(); + + await using (var fileStream = File.OpenRead(file)) + { + using var reader = new BitReader(fileStream); + + lvl.Deserialize(reader); + } + + await using var writeStream = File.OpenWrite(file); + using var writer = new BitWriter(writeStream); + + lvl.Serialize(writer); + + Console.Write("Done"); + } + + private static async Task LvlTest(IReadOnlyList args) + { + string dir; + + if (args.Count > 0) + { + dir = args[0]; + } + else + { + Console.Write("Dir: "); + + dir = Console.ReadLine(); + } + + foreach (var file in Directory.GetFiles(dir, "*.lvl", SearchOption.AllDirectories)) + { + var lvl = new LvlFile(); + + await using var fileStream = File.OpenRead(file); + + using var reader = new BitReader(fileStream); + + Console.Write($"{file} -> "); + + lvl.Deserialize(reader); + + Console.Write('\n'); + } + + Console.Write("Done"); + } + } +} \ No newline at end of file diff --git a/InfectedRose.Examples/ZoneSummary.cs b/InfectedRose.Examples/ZoneSummary.cs new file mode 100644 index 0000000..8989301 --- /dev/null +++ b/InfectedRose.Examples/ZoneSummary.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace InfectedRose.Examples +{ + [Table("ZoneSummary")] + public class ZoneSummary + { + [Column("zoneId")] public int ZoneId { get; set; } + + [Column("type")] public int Type { get; set; } + + [Column("value")] public int Value { get; set; } + + [Column("_uniqueID")] public int UniqueId { get; set; } + } +} \ No newline at end of file diff --git a/InfectedRose.Luz/Enums/AchievementRequired.cs b/InfectedRose.Luz/Enums/AchievementRequired.cs new file mode 100644 index 0000000..a86beda --- /dev/null +++ b/InfectedRose.Luz/Enums/AchievementRequired.cs @@ -0,0 +1,17 @@ +namespace InfectedRose.Luz +{ + public enum AchievementRequired + { + None, + Builder, + Craftsman, + SeniorBuilder, + Journeyman, + MasterBuilder, + Architect, + SeniorArchitect, + MasterArchitect, + Visionary, + Exemplar + } +} \ No newline at end of file diff --git a/InfectedRose.Luz/Enums/PathBehavior.cs b/InfectedRose.Luz/Enums/PathBehavior.cs new file mode 100644 index 0000000..2d9fddc --- /dev/null +++ b/InfectedRose.Luz/Enums/PathBehavior.cs @@ -0,0 +1,9 @@ +namespace InfectedRose.Luz +{ + public enum PathBehavior : uint + { + Loop, + Bounce, + Once + } +} \ No newline at end of file diff --git a/InfectedRose.Luz/Enums/PathType.cs b/InfectedRose.Luz/Enums/PathType.cs new file mode 100644 index 0000000..76b5187 --- /dev/null +++ b/InfectedRose.Luz/Enums/PathType.cs @@ -0,0 +1,14 @@ +namespace InfectedRose.Luz +{ + public enum PathType : uint + { + Movement, + MovingPlatform, + Property, + Camera, + Spawner, + Showcase, + Race, + Rail + } +} \ No newline at end of file diff --git a/InfectedRose.Luz/Enums/RentalTimeUnit.cs b/InfectedRose.Luz/Enums/RentalTimeUnit.cs new file mode 100644 index 0000000..ea22e37 --- /dev/null +++ b/InfectedRose.Luz/Enums/RentalTimeUnit.cs @@ -0,0 +1,14 @@ +namespace InfectedRose.Luz +{ + public enum RentalTimeUnit + { + Forever, + Seconds, + Minutes, + Hours, + Days, + Weeks, + Months, + Years + } +} \ No newline at end of file diff --git a/InfectedRose.Luz/InfectedRose.Luz.csproj b/InfectedRose.Luz/InfectedRose.Luz.csproj new file mode 100644 index 0000000..543ea48 --- /dev/null +++ b/InfectedRose.Luz/InfectedRose.Luz.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp3.0 + + + + + + + diff --git a/InfectedRose.Luz/LuzCameraPath.cs b/InfectedRose.Luz/LuzCameraPath.cs new file mode 100644 index 0000000..6408af0 --- /dev/null +++ b/InfectedRose.Luz/LuzCameraPath.cs @@ -0,0 +1,39 @@ +using System; +using InfectedRose.Core; +using RakDotNet.IO; + +namespace InfectedRose.Luz +{ + public class LuzCameraPath : LuzPathData + { + public string NextPath { get; set; } + + public bool UnknownBool { get; set; } + + public LuzCameraPath(uint version) : base(version) + { + } + + public override void Serialize(BitWriter writer) + { + base.Serialize(writer); + + writer.WriteNiString(NextPath, true, true); + + if (Version >= 14) + writer.Write((byte) (UnknownBool ? 1 : 0)); + } + + public override void Deserialize(BitReader reader) + { + base.Deserialize(reader); + + NextPath = reader.ReadNiString(true, true); + + Console.WriteLine(NextPath); + + if (Version >= 14) + UnknownBool = reader.Read() != 0; + } + } +} \ No newline at end of file diff --git a/InfectedRose.Luz/LuzCameraWaypoint.cs b/InfectedRose.Luz/LuzCameraWaypoint.cs new file mode 100644 index 0000000..b2e69e2 --- /dev/null +++ b/InfectedRose.Luz/LuzCameraWaypoint.cs @@ -0,0 +1,60 @@ +using System.IO; +using System.Numerics; +using InfectedRose.Core; +using RakDotNet.IO; + +namespace InfectedRose.Luz +{ + public class LuzCameraWaypoint : LuzPathWaypoint + { + public Quaternion Rotation { get; set; } + + public float Time { get; set; } + + public float Tension { get; set; } + + public float Continuity { get; set; } + + public float Bias { get; set; } + + public float FieldOfView { get; set; } + + public LuzCameraWaypoint(uint version) : base(version) + { + } + + public override void Serialize(BitWriter writer) + { + base.Serialize(writer); + + writer.WriteNiQuaternion(Rotation); + + writer.Write(Time); + + writer.Write(FieldOfView); + + writer.Write(Tension); + + writer.Write(Continuity); + + writer.Write(Bias); + } + + public override void Deserialize(BitReader reader) + { + base.Deserialize(reader); + + Rotation = reader.ReadNiQuaternion(); + + Time = reader.Read(); + + FieldOfView = reader.Read(); + + Tension = reader.Read(); + + Continuity = reader.Read(); + + Bias = reader.Read(); + } + } +} \ No newline at end of file diff --git a/InfectedRose.Luz/LuzFile.cs b/InfectedRose.Luz/LuzFile.cs new file mode 100644 index 0000000..c5af81a --- /dev/null +++ b/InfectedRose.Luz/LuzFile.cs @@ -0,0 +1,239 @@ +using System; +using System.IO; +using System.Numerics; +using InfectedRose.Core; +using RakDotNet.IO; + +namespace InfectedRose.Luz +{ + public class LuzFile : IConstruct + { + public uint Version { get; set; } + + public uint RevisionNumber { get; set; } + + public uint WorldId { get; set; } + + public Vector3 SpawnPoint { get; set; } + + public Quaternion SpawnRotation { get; set; } + + public LuzScene[] Scenes { get; set; } + + public byte UnknownByte { get; set; } + + public string TerrainFileName { get; set; } + + public string TerrainFile { get; set; } + + public string TerrainDescription { get; set; } + + public LuzSceneTransition[] Transitions { get; set; } + + public LuzPathData[] PathData { get; set; } + + //public LvlFile[] LvlFiles { get; set; } + + public void Serialize(BitWriter writer) + { + writer.Write(Version); + + if (Version >= 0x24) + { + writer.Write(RevisionNumber); + } + + writer.Write(WorldId); + + if (Version >= 0x26) + { + writer.Write(SpawnPoint); + writer.Write(SpawnRotation); + } + + if (Version < 0x25) + { + writer.Write((byte) Scenes.Length); + } + else + { + writer.Write((uint) Scenes.Length); + } + + foreach (var scene in Scenes) + { + scene.Serialize(writer); + } + + writer.Write(UnknownByte); + + writer.WriteNiString(TerrainFileName, false, true); + + writer.WriteNiString(TerrainFile, false, true); + + writer.WriteNiString(TerrainDescription, false, true); + + if (Version >= 20) + { + writer.Write((uint) Transitions.Length); + + foreach (var transition in Transitions) + { + transition.Serialize(writer); + } + } + + if (Version < 0x23) return; + + var position = writer.BaseStream.Position; + + WritePaths(writer); + + var finishPosition = writer.BaseStream.Position; + + writer.BaseStream.Position = position; + + writer.Write(finishPosition - position - 4); + } + + private void WritePaths(BitWriter writer) + { + writer.BaseStream.Position += 4; + + writer.Write((uint) 1); + + writer.Write((uint) PathData.Length); + + foreach (var pathData in PathData) + { + writer.Write(pathData.Version); + + writer.WriteNiString(pathData.PathName, true, true); + + writer.Write((uint) pathData.Type); + + writer.Write(pathData.UnknownInt); + + pathData.Serialize(writer); + + writer.Write((uint) pathData.Waypoints.Length); + + foreach (var waypoint in pathData.Waypoints) + { + waypoint.Serialize(writer); + } + } + } + + public void Deserialize(BitReader reader) + { + Version = reader.Read(); + + if (Version >= 0x24) + { + RevisionNumber = reader.Read(); + } + + WorldId = reader.Read(); + + if (Version >= 0x26) + { + SpawnPoint = reader.Read(); + SpawnRotation = reader.Read(); + } + + var sceneCount = Version < 0x25 ? reader.Read() : reader.Read(); + + Scenes = new LuzScene[sceneCount]; + //LvlFiles = new LvlFile[sceneCount]; + + for (var i = 0; i < sceneCount; i++) + { + Scenes[i] = new LuzScene(); + Scenes[i].Deserialize(reader); + + //LvlFiles[i] = new LvlFile($"{Path.GetDirectoryName(path)}/{Scenes[i].FileName}"); + } + + UnknownByte = reader.Read(); + + TerrainFileName = reader.ReadNiString(false, true); + + TerrainFile = reader.ReadNiString(false, true); + + TerrainDescription = reader.ReadNiString(false, true); + + if (Version >= 0x20) + { + var sceneTransitionCount = reader.Read(); + + Transitions = new LuzSceneTransition[sceneTransitionCount]; + + for (var i = 0; i < sceneTransitionCount; i++) + { + Transitions[i] = new LuzSceneTransition(Version); + Transitions[i].Deserialize(reader); + } + } + + if (Version < 0x23) return; + { + Console.WriteLine(reader.Read()); + reader.Read(); + + var pathDataCount = reader.Read(); + + PathData = new LuzPathData[pathDataCount]; + + for (var i = 0; i < pathDataCount; i++) + { + var version = reader.Read(); + var name = reader.ReadNiString(true, true); + var type = (PathType) reader.Read(); + + Console.WriteLine($"[{i}:{name}] -> [{type}]"); + + PathData[i] = type switch + { + PathType.Movement => new LuzPathData(version), + PathType.MovingPlatform => new LuzMovingPlatformPath(version), + PathType.Property => new LuzPropertyPath(version), + PathType.Camera => new LuzCameraPath(version), + PathType.Spawner => new LuzSpawnerPath(version), + PathType.Showcase => new LuzPathData(version), + PathType.Race => new LuzPathData(version), + PathType.Rail => new LuzPathData(version), + _ => throw new ArgumentOutOfRangeException() + }; + + PathData[i].UnknownInt = reader.Read(); + + PathData[i].PathName = name; + PathData[i].Type = type; + PathData[i].Deserialize(reader); + + var count = reader.Read(); + PathData[i].Waypoints = new LuzPathWaypoint[count]; + + for (var j = 0; j < count; j++) + { + PathData[i].Waypoints[j] = type switch + { + PathType.Movement => new LuzMovementWaypoint(version), + PathType.MovingPlatform => new LuzMovingPlatformWaypoint(version), + PathType.Property => new LuzPathWaypoint(version), + PathType.Camera => new LuzCameraWaypoint(version), + PathType.Spawner => new LuzSpawnerWaypoint(version), + PathType.Showcase => new LuzPathWaypoint(version), + PathType.Race => new LuzRaceWaypoint(version), + PathType.Rail => new LuzRailWaypoint(version), + _ => throw new ArgumentOutOfRangeException() + }; + + PathData[i].Waypoints[j].Deserialize(reader); + } + } + } + } + } +} \ No newline at end of file diff --git a/InfectedRose.Luz/LuzMovementWaypoint.cs b/InfectedRose.Luz/LuzMovementWaypoint.cs new file mode 100644 index 0000000..cf920ad --- /dev/null +++ b/InfectedRose.Luz/LuzMovementWaypoint.cs @@ -0,0 +1,40 @@ +using System.IO; +using RakDotNet.IO; + +namespace InfectedRose.Luz +{ + public class LuzMovementWaypoint : LuzPathWaypoint + { + public LuzPathConfig[] Configs { get; set; } + + public LuzMovementWaypoint(uint version) : base(version) + { + } + + public override void Serialize(BitWriter writer) + { + base.Serialize(writer); + + writer.Write((uint) Configs.Length); + + foreach (var config in Configs) + { + config.Serialize(writer); + } + } + + public override void Deserialize(BitReader reader) + { + base.Deserialize(reader); + + var configCount = reader.Read(); + Configs = new LuzPathConfig[configCount]; + + for (var i = 0; i < configCount; i++) + { + Configs[i] = new LuzPathConfig(); + Configs[i].Deserialize(reader); + } + } + } +} \ No newline at end of file diff --git a/InfectedRose.Luz/LuzMovingPlatformPath.cs b/InfectedRose.Luz/LuzMovingPlatformPath.cs new file mode 100644 index 0000000..76b73fd --- /dev/null +++ b/InfectedRose.Luz/LuzMovingPlatformPath.cs @@ -0,0 +1,38 @@ +using System; +using System.IO; +using InfectedRose.Core; +using RakDotNet.IO; + +namespace InfectedRose.Luz +{ + public class LuzMovingPlatformPath : LuzPathData + { + public string MovingPlatformSound { get; set; } + + public bool TimeBased { get; set; } + + public LuzMovingPlatformPath(uint version) : base(version) + { + } + + public override void Serialize(BitWriter writer) + { + base.Serialize(writer); + + if (Version >= 18) + writer.Write((byte) (TimeBased ? 1 : 0)); + else if (Version >= 13) + writer.WriteNiString(MovingPlatformSound, true, true); + } + + public override void Deserialize(BitReader reader) + { + base.Deserialize(reader); + + if (Version >= 18) + TimeBased = reader.Read() != 0; + else if (Version >= 13) + MovingPlatformSound = reader.ReadNiString(true, true); + } + } +} \ No newline at end of file diff --git a/InfectedRose.Luz/LuzMovingPlatformWaypoint.cs b/InfectedRose.Luz/LuzMovingPlatformWaypoint.cs new file mode 100644 index 0000000..255ddf0 --- /dev/null +++ b/InfectedRose.Luz/LuzMovingPlatformWaypoint.cs @@ -0,0 +1,67 @@ +using System; +using System.IO; +using System.Numerics; +using InfectedRose.Core; +using RakDotNet.IO; + +namespace InfectedRose.Luz +{ + public class LuzMovingPlatformWaypoint : LuzPathWaypoint + { + public Quaternion Rotation { get; set; } + + public bool LockPlayer { get; set; } + + public float Speed { get; set; } + + public float Wait { get; set; } + + public string DepartSound { get; set; } + + public string ArriveSound { get; set; } + + public LuzMovingPlatformWaypoint(uint version) : base(version) + { + } + + public override void Serialize(BitWriter writer) + { + base.Serialize(writer); + + writer.WriteNiQuaternion(Rotation); + + writer.Write((byte) (LockPlayer ? 1 : 0)); + + writer.Write(Speed); + + writer.Write(Wait); + + if (Version < 13) return; + + writer.WriteNiString(DepartSound, true, true); + + writer.WriteNiString(ArriveSound, true, true); + } + + public override void Deserialize(BitReader reader) + { + base.Deserialize(reader); + + Rotation = reader.ReadNiQuaternion(); + + LockPlayer = reader.Read() != 0; + + Speed = reader.Read(); + + Wait = reader.Read(); + + if (Version < 13) return; + + DepartSound = reader.ReadNiString(true, true); + + ArriveSound = reader.ReadNiString(true, true); + + Console.WriteLine($"{DepartSound} -> {ArriveSound}"); + } + } +} \ No newline at end of file diff --git a/InfectedRose.Luz/LuzPathConfig.cs b/InfectedRose.Luz/LuzPathConfig.cs new file mode 100644 index 0000000..feda0de --- /dev/null +++ b/InfectedRose.Luz/LuzPathConfig.cs @@ -0,0 +1,25 @@ +using System.IO; +using InfectedRose.Core; +using RakDotNet.IO; + +namespace InfectedRose.Luz +{ + public class LuzPathConfig : IConstruct + { + public string ConfigName { get; set; } + + public string ConfigTypeAndValue { get; set; } + + public void Serialize(BitWriter writer) + { + writer.WriteNiString(ConfigName, true, true); + writer.WriteNiString(ConfigTypeAndValue, true, true); + } + + public void Deserialize(BitReader reader) + { + ConfigName = reader.ReadNiString(true, true); + ConfigTypeAndValue = reader.ReadNiString(true, true); + } + } +} \ No newline at end of file diff --git a/InfectedRose.Luz/LuzPathData.cs b/InfectedRose.Luz/LuzPathData.cs new file mode 100644 index 0000000..2cf2c80 --- /dev/null +++ b/InfectedRose.Luz/LuzPathData.cs @@ -0,0 +1,38 @@ +using System; +using System.IO; +using InfectedRose.Core; +using RakDotNet.IO; + +namespace InfectedRose.Luz +{ + [Serializable] + public class LuzPathData : IConstruct + { + public uint Version { get; set; } + + public string PathName { get; set; } + + public uint UnknownInt { get; set; } + + public PathType Type { get; set; } + + public PathBehavior Behavior { get; set; } + + public LuzPathWaypoint[] Waypoints { get; set; } + + public LuzPathData(uint version) + { + Version = version; + } + + public virtual void Serialize(BitWriter writer) + { + writer.Write((uint) Behavior); + } + + public virtual void Deserialize(BitReader reader) + { + Behavior = (PathBehavior) reader.Read(); + } + } +} \ No newline at end of file diff --git a/InfectedRose.Luz/LuzPathWaypoint.cs b/InfectedRose.Luz/LuzPathWaypoint.cs new file mode 100644 index 0000000..ac4f11e --- /dev/null +++ b/InfectedRose.Luz/LuzPathWaypoint.cs @@ -0,0 +1,29 @@ +using System.IO; +using System.Numerics; +using InfectedRose.Core; +using RakDotNet.IO; + +namespace InfectedRose.Luz +{ + public class LuzPathWaypoint : IConstruct + { + public Vector3 Position { get; set; } + + public uint Version { get; set; } + + public LuzPathWaypoint(uint version) + { + Version = version; + } + + public virtual void Serialize(BitWriter writer) + { + writer.Write(Position); + } + + public virtual void Deserialize(BitReader reader) + { + Position = reader.Read(); + } + } +} \ No newline at end of file diff --git a/InfectedRose.Luz/LuzPropertyPath.cs b/InfectedRose.Luz/LuzPropertyPath.cs new file mode 100644 index 0000000..1d72870 --- /dev/null +++ b/InfectedRose.Luz/LuzPropertyPath.cs @@ -0,0 +1,102 @@ +using System.IO; +using System.Numerics; +using InfectedRose.Core; +using RakDotNet.IO; + +namespace InfectedRose.Luz +{ + public class LuzPropertyPath : LuzPathData + { + public int UnknownInt0 { get; set; } + + public int UnknownInt1 { get; set; } + + public int Price { get; set; } + + public int RentalTime { get; set; } + + public ulong AssociatedZone { get; set; } + + public string DisplayName { get; set; } + + public string DisplayDescription { get; set; } + + public int CloneLimit { get; set; } + + public float ReputationMultiplier { get; set; } + + public RentalTimeUnit TimeUnit { get; set; } + + public AchievementRequired Achievement { get; set; } + + public Vector3 PlayerZonePoint { get; set; } + + public float MaxBuildHeight { get; set; } + + public LuzPropertyPath(uint version) : base(version) + { + } + + public override void Serialize(BitWriter writer) + { + base.Serialize(writer); + + writer.Write(UnknownInt0); + + writer.Write(Price); + + writer.Write(RentalTime); + + writer.Write(AssociatedZone); + + writer.WriteNiString(DisplayName, true, true); + + writer.WriteNiString(DisplayDescription, true); + + writer.Write(UnknownInt1); + + writer.Write(CloneLimit); + + writer.Write(ReputationMultiplier); + + writer.Write((int) TimeUnit); + + writer.Write((int) Achievement); + + writer.Write(PlayerZonePoint); + + writer.Write(MaxBuildHeight); + } + + public override void Deserialize(BitReader reader) + { + base.Deserialize(reader); + + UnknownInt0 = reader.Read(); + + Price = reader.Read(); + + RentalTime = reader.Read(); + + AssociatedZone = reader.Read(); + + DisplayName = reader.ReadNiString(true, true); + + DisplayDescription = reader.ReadNiString(true); + + UnknownInt1 = reader.Read(); + + CloneLimit = reader.Read(); + + ReputationMultiplier = reader.Read(); + + TimeUnit = (RentalTimeUnit) reader.Read(); + + Achievement = (AchievementRequired) reader.Read(); + + PlayerZonePoint = reader.Read(); + + MaxBuildHeight = reader.Read(); + } + } +} \ No newline at end of file diff --git a/InfectedRose.Luz/LuzRaceWaypoint.cs b/InfectedRose.Luz/LuzRaceWaypoint.cs new file mode 100644 index 0000000..15b4510 --- /dev/null +++ b/InfectedRose.Luz/LuzRaceWaypoint.cs @@ -0,0 +1,42 @@ +using System.IO; +using System.Numerics; +using InfectedRose.Core; +using RakDotNet.IO; + +namespace InfectedRose.Luz +{ + public class LuzRaceWaypoint : LuzPathWaypoint + { + public Quaternion Rotation { get; set; } + + public byte[] UnknownBytes { get; set; } + + public Vector3 UnknownVector { get; set; } + + public LuzRaceWaypoint(uint version) : base(version) + { + } + + public override void Serialize(BitWriter writer) + { + base.Serialize(writer); + + writer.WriteNiQuaternion(Rotation); + + writer.Write(UnknownBytes); + + writer.Write(UnknownVector); + } + + public override void Deserialize(BitReader reader) + { + base.Deserialize(reader); + + Rotation = reader.ReadNiQuaternion(); + + UnknownBytes = reader.ReadBuffer(2); + + UnknownVector = reader.Read(); + } + } +} \ No newline at end of file diff --git a/InfectedRose.Luz/LuzRailWaypoint.cs b/InfectedRose.Luz/LuzRailWaypoint.cs new file mode 100644 index 0000000..5891a6f --- /dev/null +++ b/InfectedRose.Luz/LuzRailWaypoint.cs @@ -0,0 +1,60 @@ +using System.IO; +using System.Numerics; +using InfectedRose.Core; +using RakDotNet.IO; + +namespace InfectedRose.Luz +{ + public class LuzRailWaypoint : LuzPathWaypoint + { + public LuzPathConfig[] Configs { get; set; } + + public Quaternion UnknownQuaternion { get; set; } + + public float Speed { get; set; } + + public LuzRailWaypoint(uint version) : base(version) + { + } + + public override void Serialize(BitWriter writer) + { + base.Serialize(writer); + + writer.WriteNiQuaternion(UnknownQuaternion); + + if (Version >= 17) + { + writer.Write(Speed); + } + + writer.Write((uint) Configs.Length); + + foreach (var config in Configs) + { + config.Serialize(writer); + } + } + + public override void Deserialize(BitReader reader) + { + base.Deserialize(reader); + + UnknownQuaternion = reader.ReadNiQuaternion(); + + if (Version >= 17) + { + Speed = reader.Read(); + } + + var configCount = reader.Read(); + Configs = new LuzPathConfig[configCount]; + + for (var i = 0; i < configCount; i++) + { + Configs[i] = new LuzPathConfig(); + Configs[i].Deserialize(reader); + } + } + } +} \ No newline at end of file diff --git a/InfectedRose.Luz/LuzScene.cs b/InfectedRose.Luz/LuzScene.cs new file mode 100644 index 0000000..c654c21 --- /dev/null +++ b/InfectedRose.Luz/LuzScene.cs @@ -0,0 +1,64 @@ +using System; +using System.IO; +using InfectedRose.Core; +using RakDotNet.IO; + +namespace InfectedRose.Luz +{ + [Serializable] + public class LuzScene : IConstruct + { + public string FileName { get; set; } + + public byte SceneId { get; set; } + + public bool IsAudioScene { get; set; } + + public string SceneName { get; set; } + + public byte[] UnknownByteArray0 { get; set; } = new byte[3]; + + public byte[] UnknownByteArray1 { get; set; } = new byte[3]; + + public byte[] UnknownByteArray2 { get; set; } = new byte[3]; + + public void Serialize(BitWriter writer) + { + writer.WriteNiString(FileName, false, true); + + writer.Write(SceneId); + + writer.Write(UnknownByteArray0); + + writer.Write((byte) (IsAudioScene ? 1 : 0)); + + writer.Write(UnknownByteArray1); + + writer.WriteNiString(SceneName, false, true); + + writer.Write(UnknownByteArray2); + } + + public void Deserialize(BitReader reader) + { + FileName = reader.ReadNiString(false, true); + + SceneId = reader.Read(); + + UnknownByteArray0 = reader.ReadBuffer(3); + + IsAudioScene = reader.Read() != 0; + + UnknownByteArray1 = reader.ReadBuffer(3); + + SceneName = reader.ReadNiString(false, true); + + UnknownByteArray2 = reader.ReadBuffer(3); + } + + public override string ToString() + { + return FileName; + } + } +} \ No newline at end of file diff --git a/InfectedRose.Luz/LuzSceneTransition.cs b/InfectedRose.Luz/LuzSceneTransition.cs new file mode 100644 index 0000000..227a4d0 --- /dev/null +++ b/InfectedRose.Luz/LuzSceneTransition.cs @@ -0,0 +1,53 @@ +using System; +using System.IO; +using InfectedRose.Core; +using RakDotNet.IO; + +namespace InfectedRose.Luz +{ + [Serializable] + public class LuzSceneTransition : IConstruct + { + public string SceneTransitionName { get; set; } + + public LuzSceneTransitionPoint[] TransitionPoints { get; set; } + + public uint Version { get; set; } + + public LuzSceneTransition(uint version) + { + Version = version; + } + + public void Serialize(BitWriter writer) + { + if (Version < 0x25) + { + writer.WriteNiString(SceneTransitionName, false, true); + } + + foreach (var transitionPoint in TransitionPoints) + { + transitionPoint.Serialize(writer); + } + } + + public void Deserialize(BitReader reader) + { + if (Version < 0x25) + { + SceneTransitionName = reader.ReadNiString(false, true); + } + + var pointCount = Version <= 21 || Version >= 0x27 ? 2 : 5; + + TransitionPoints = new LuzSceneTransitionPoint[pointCount]; + + for (var i = 0; i < pointCount; i++) + { + TransitionPoints[i] = new LuzSceneTransitionPoint(); + TransitionPoints[i].Deserialize(reader); + } + } + } +} \ No newline at end of file diff --git a/InfectedRose.Luz/LuzSceneTransitionPoint.cs b/InfectedRose.Luz/LuzSceneTransitionPoint.cs new file mode 100644 index 0000000..99ccb80 --- /dev/null +++ b/InfectedRose.Luz/LuzSceneTransitionPoint.cs @@ -0,0 +1,30 @@ +using System; +using System.IO; +using System.Numerics; +using InfectedRose.Core; +using RakDotNet.IO; + +namespace InfectedRose.Luz +{ + [Serializable] + public class LuzSceneTransitionPoint : IConstruct + { + public ulong SceneId { get; set; } + + public Vector3 Point { get; set; } + + public void Serialize(BitWriter writer) + { + writer.Write(SceneId); + + writer.Write(Point); + } + + public void Deserialize(BitReader reader) + { + SceneId = reader.Read(); + + Point = reader.Read(); + } + } +} \ No newline at end of file diff --git a/InfectedRose.Luz/LuzSpawnerPath.cs b/InfectedRose.Luz/LuzSpawnerPath.cs new file mode 100644 index 0000000..9634e66 --- /dev/null +++ b/InfectedRose.Luz/LuzSpawnerPath.cs @@ -0,0 +1,60 @@ +using System.IO; +using RakDotNet.IO; + +namespace InfectedRose.Luz +{ + public class LuzSpawnerPath : LuzPathData + { + public uint SpawnedLot { get; set; } + + public uint RespawnTime { get; set; } + + public int MaxSpawnCount { get; set; } + + public uint NumberToMaintain { get; set; } + + public long SpawnerObjectId { get; set; } + + public bool ActivateSpawnerNetworkOnLoad { get; set; } + + public LuzSpawnerPath(uint version) : base(version) + { + } + + public override void Serialize(BitWriter writer) + { + base.Serialize(writer); + + writer.Write(SpawnedLot); + + writer.Write(RespawnTime); + + writer.Write(MaxSpawnCount); + + writer.Write(NumberToMaintain); + + writer.Write(SpawnerObjectId); + + if (Version > 8) + writer.Write((byte) (ActivateSpawnerNetworkOnLoad ? 1 : 0)); + } + + public override void Deserialize(BitReader reader) + { + base.Deserialize(reader); + + SpawnedLot = reader.Read(); + + RespawnTime = reader.Read(); + + MaxSpawnCount = reader.Read(); + + NumberToMaintain = reader.Read(); + + SpawnerObjectId = reader.Read(); + + if (Version > 8) + ActivateSpawnerNetworkOnLoad = reader.Read() != 0; + } + } +} \ No newline at end of file diff --git a/InfectedRose.Luz/LuzSpawnerWaypoint.cs b/InfectedRose.Luz/LuzSpawnerWaypoint.cs new file mode 100644 index 0000000..8edc956 --- /dev/null +++ b/InfectedRose.Luz/LuzSpawnerWaypoint.cs @@ -0,0 +1,48 @@ +using System.Numerics; +using InfectedRose.Core; +using RakDotNet.IO; + +namespace InfectedRose.Luz +{ + public class LuzSpawnerWaypoint : LuzPathWaypoint + { + public Quaternion Rotation { get; set; } + + public LuzPathConfig[] Configs { get; set; } + + public LuzSpawnerWaypoint(uint version) : base(version) + { + } + + public override void Serialize(BitWriter writer) + { + base.Serialize(writer); + + writer.WriteNiQuaternion(Quaternion.Identity); + + writer.Write((uint) Configs.Length); + + foreach (var config in Configs) + { + config.Serialize(writer); + } + } + + public override void Deserialize(BitReader reader) + { + base.Deserialize(reader); + + Rotation = reader.ReadNiQuaternion(); + + var configCount = reader.Read(); + + Configs = new LuzPathConfig[configCount]; + + for (var i = 0; i < configCount; i++) + { + Configs[i] = new LuzPathConfig(); + Configs[i].Deserialize(reader); + } + } + } +} \ No newline at end of file diff --git a/InfectedRose.Lvl/ChunkBase.cs b/InfectedRose.Lvl/ChunkBase.cs new file mode 100644 index 0000000..26d651b --- /dev/null +++ b/InfectedRose.Lvl/ChunkBase.cs @@ -0,0 +1,16 @@ +using InfectedRose.Core; +using RakDotNet.IO; + +namespace InfectedRose.Lvl +{ + public abstract class ChunkBase : IConstruct + { + public ushort Index { get; set; } + + public abstract uint ChunkType { get; } + + public abstract void Serialize(BitWriter writer); + + public abstract void Deserialize(BitReader reader); + } +} \ No newline at end of file diff --git a/InfectedRose.Lvl/IdStruct.cs b/InfectedRose.Lvl/IdStruct.cs new file mode 100644 index 0000000..336d441 --- /dev/null +++ b/InfectedRose.Lvl/IdStruct.cs @@ -0,0 +1,33 @@ +using System.IO; +using InfectedRose.Core; +using RakDotNet.IO; + +namespace InfectedRose.Lvl +{ + public class IdStruct : IConstruct + { + public uint Id { get; set; } + + public float UnknownFloat0 { get; set; } + + public float UnknownFloat1 { get; set; } + + public void Serialize(BitWriter writer) + { + writer.Write(Id); + + writer.Write(UnknownFloat0); + + writer.Write(UnknownFloat1); + } + + public void Deserialize(BitReader reader) + { + Id = reader.Read(); + + UnknownFloat0 = reader.Read(); + + UnknownFloat1 = reader.Read(); + } + } +} \ No newline at end of file diff --git a/InfectedRose.Lvl/InfectedRose.Lvl.csproj b/InfectedRose.Lvl/InfectedRose.Lvl.csproj new file mode 100644 index 0000000..543ea48 --- /dev/null +++ b/InfectedRose.Lvl/InfectedRose.Lvl.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp3.0 + + + + + + + diff --git a/InfectedRose.Lvl/LegoDataDictionary.cs b/InfectedRose.Lvl/LegoDataDictionary.cs new file mode 100644 index 0000000..5d6c1a5 --- /dev/null +++ b/InfectedRose.Lvl/LegoDataDictionary.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Numerics; +using System.Text; + +namespace Lvl +{ + public class LegoDataDictionary : IDictionary + { + public const char InfoSeparator = '\u001F'; + private readonly Dictionary _map; + + public int Count => _map.Count; + public bool IsReadOnly => false; + + public ICollection Keys => _map.Keys; + public ICollection Values => _map.Values.Select(v => v.Item2).ToArray(); + + public object this[string key] + { + get => _map[key].Item2; + set => Add(key, value); + } + + public object this[string key, byte type] + { + get => _map[key].Item2; + set => Add(key, value, type); + } + + public LegoDataDictionary() + { + _map = new Dictionary(); + } + + public void Add(string key, object value, byte type) + => _map[key] = (type, value); + + public void Add(string key, object value) + { + var type = + value is int ? 1 : + value is float ? 3 : + value is double ? 4 : + value is uint ? 5 : + value is bool ? 7 : + value is long ? 8 : + value is byte[] ? 13 : 0; + + Add(key, value, (byte) type); + } + + public void Add(KeyValuePair item) + => Add(item.Key, item.Value); + + public void Clear() + => _map.Clear(); + + public bool ContainsKey(string key) + => _map.ContainsKey(key); + + public bool Contains(KeyValuePair item) + => _map.ContainsKey(item.Key) && _map[item.Key].Item2 == item.Value; + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + public bool Remove(string key) + => _map.Remove(key); + + public bool Remove(KeyValuePair item) + => _map.Remove(item.Key); + + public bool TryGetValue(string key, out object value) + { + if (_map.TryGetValue(key, out var temp)) + { + value = temp.Item2; + + return true; + } + + value = null; + + return false; + } + + public IEnumerator> GetEnumerator() + { + foreach (var k in _map) + { + yield return new KeyValuePair(k.Key, k.Value.Item2); + } + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public override string ToString() + => ToString("\r\n"); + + public string ToString(string separator) + { + var str = new StringBuilder(); + + foreach (var k in _map) + { + string val; + + switch (k.Value.Item2) + { + case Vector2 vec2: + val = $"{vec2.X}{InfoSeparator}{vec2.Y}"; + break; + + case Vector3 vec3: + val = $"{vec3.X}{InfoSeparator}{vec3.Z}{InfoSeparator}{vec3.Y}"; + break; + + case Vector4 vec4: + val = $"{vec4.X}{InfoSeparator}{vec4.Z}{InfoSeparator}{vec4.Y}{InfoSeparator}{vec4.W}"; + break; + + case LegoDataList list: + val = list.ToString(); + break; + + default: + val = k.Value.Item2.ToString(); + break; + } + + str.Append($"{k}={k.Value.Item1}:{val}"); + + var i = _map.Keys.ToList().IndexOf(k.Key); + + if (i + 1 < Count) + str.Append(separator); + } + + return str.ToString(); + } + + public static LegoDataDictionary FromDictionary(Dictionary dict) + { + var ldd = new LegoDataDictionary(); + + foreach (var k in dict) + { + ldd[k.Key] = k.Value; + } + + return ldd; + } + + public static LegoDataDictionary FromString(string text, char separator = '\n') + { + var dict = new LegoDataDictionary(); + var lines = text.Replace("\r", "").Split(separator); + + foreach (var line in lines) + { + var firstEqual = line.IndexOf('='); + var firstColon = line.IndexOf(':'); + var key = line.Substring(0, firstEqual); + var type = int.Parse(line.Substring(firstEqual + 1, firstColon - firstEqual - 1)); + var val = line.Substring(firstColon + 1); + + object v; + + switch (type) + { + case 1: + case 2: + v = int.Parse(val); + break; + + case 3: + v = float.Parse(val, CultureInfo.InvariantCulture); + break; + + case 4: + v = double.Parse(val); + break; + + case 5: + case 6: + v = uint.Parse(val); + break; + + case 7: + v = int.Parse(val) == 1; + break; + + case 8: + case 9: + v = long.Parse(val); + break; + + default: + if (val.Contains('+')) + { + v = LegoDataList.FromString(val); + } + else if (val.Contains('\u001F')) + { + var floats = val.Split('\u001F').Select(s => float.Parse(s, CultureInfo.InvariantCulture)).ToArray(); + + v = + floats.Length == 1 ? floats[0] : + floats.Length == 2 ? new Vector2(floats[0], floats[1]) : + floats.Length == 3 ? new Vector3(floats[0], floats[1], floats[2]) : + floats.Length == 4 ? new Vector4(floats[0], floats[1], floats[2], floats[3]) : + (object) val; + } + else + { + v = val; + } + break; + } + + dict[key, (byte) type] = v; + } + + return dict; + } + } +} \ No newline at end of file diff --git a/InfectedRose.Lvl/LegoDataList.cs b/InfectedRose.Lvl/LegoDataList.cs new file mode 100644 index 0000000..fdfdf9d --- /dev/null +++ b/InfectedRose.Lvl/LegoDataList.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Numerics; +using System.Text; + +namespace Lvl +{ + public class LegoDataList : IList + { + public const char InfoSeparator = '\u001F'; + private readonly List<(byte, object)> _list; + + public int Count => _list.Count; + public bool IsReadOnly => false; + + public object this[int index] + { + get => _list[index].Item2; + set => Insert(index, value); + } + + public object this[int index, byte type] + { + get => _list[index].Item2; + set => Insert(index, value, type); + } + + public LegoDataList() + { + _list = new List<(byte, object)>(); + } + + public void Add(object item, byte type) + => _list.Add((type, item)); + + public void Add(object item) + { + var type = + item is int ? 1 : + item is float ? 3 : + item is double ? 4 : + item is uint ? 5 : + item is bool ? 7 : + item is long ? 8 : + item is byte[] ? 13 : 0; + + Add(item, (byte) type); + } + + public void Insert(int index, object item, byte type) + => _list.Insert(index, (type, item)); + + public void Insert(int index, object item) + { + var type = + item is int ? 1 : + item is float ? 3 : + item is double ? 4 : + item is uint ? 5 : + item is bool ? 7 : + item is long ? 8 : + item is byte[] ? 13 : 0; + + Insert(index, item, (byte) type); + } + + public void Clear() + => _list.Clear(); + + public bool Contains(object item) + => _list.Exists(i => i.Item2 == item); + + public void RemoveAt(int index) + => _list.RemoveAt(index); + + public bool Remove(object item) + => _list.Remove(_list.Find(i => i.Item2 == item)); + + public int IndexOf(object item) + => _list.FindIndex(i => i.Item2 == item); + + public void CopyTo(object[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + public IEnumerator GetEnumerator() + { + foreach (var (_, v) in _list) yield return v; + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public override string ToString() + => ToString("+"); + + public string ToString(string separator) + { + var str = new StringBuilder(); + + foreach (var (t, v) in _list) + { + string val; + + switch (v) + { + case Vector2 vec2: + val = $"{vec2.X}{InfoSeparator}{vec2.Y}"; + break; + + case Vector3 vec3: + val = $"{vec3.X}{InfoSeparator}{vec3.Z}{InfoSeparator}{vec3.Y}"; + break; + + case Vector4 vec4: + val = $"{vec4.X}{InfoSeparator}{vec4.Z}{InfoSeparator}{vec4.Y}{InfoSeparator}{vec4.W}"; + break; + + case LegoDataList list: + val = list.ToString(); + break; + + default: + val = v.ToString(); + break; + } + + str.Append($"{t}:{val}"); + + if (IndexOf(v) + 1 < Count) + str.Append(separator); + } + + return str.ToString(); + } + + public static LegoDataList FromEnumerable(IEnumerable list) + { + var ldl = new LegoDataList(); + + foreach (var item in list) ldl.Add(item); + + return ldl; + } + + public static LegoDataList FromString(string text) + { + var list = new LegoDataList(); + var entries = text.Split('+'); + + for (var i = 0; i < entries.Length; i++) + { + var entry = entries[i]; + var firstColon = entry.IndexOf(':'); + var type = int.Parse(entry.Substring(0, firstColon)); + var val = entry.Substring(firstColon + 1); + + object v = null; + + switch (type) + { + case 1: + case 2: + v = int.Parse(val); + break; + + case 3: + v = float.Parse(val, CultureInfo.InvariantCulture); + break; + + case 4: + v = double.Parse(val); + break; + + case 5: + case 6: + v = uint.Parse(val); + break; + + case 7: + v = int.Parse(val) == 1; + break; + + case 8: + case 9: + v = long.Parse(val); + break; + + default: + if (val.Contains('+')) + { + v = FromString(val); + } + else if (val.Contains('\u001F')) + { + var floats = val.Split('\u001F').Select(s => float.Parse(s, CultureInfo.InvariantCulture)).ToArray(); + + v = + floats.Length == 1 ? floats[0] : + floats.Length == 2 ? new Vector2(floats[0], floats[1]) : + floats.Length == 3 ? new Vector3(floats[0], floats[1], floats[2]) : + floats.Length == 4 ? new Vector4(floats[0], floats[1], floats[2], floats[3]) : + (object) val; + } + else + { + v = val; + } + break; + } + + list[i, (byte) type] = v; + } + + return list; + } + } +} \ No newline at end of file diff --git a/InfectedRose.Lvl/LevelEnvironmentConfig.cs b/InfectedRose.Lvl/LevelEnvironmentConfig.cs new file mode 100644 index 0000000..685cd44 --- /dev/null +++ b/InfectedRose.Lvl/LevelEnvironmentConfig.cs @@ -0,0 +1,35 @@ +using System.IO; +using RakDotNet.IO; + +namespace InfectedRose.Lvl +{ + public class LevelEnvironmentConfig : ChunkBase + { + public ParticleStruct[] ParticleStructs { get; set; } + + public override uint ChunkType => 2002; + + public override void Serialize(BitWriter writer) + { + writer.Write((uint) ParticleStructs.Length); + + foreach (var particleStruct in ParticleStructs) + { + particleStruct.Serialize(writer); + } + } + + public override void Deserialize(BitReader reader) + { + ParticleStructs = new ParticleStruct[reader.Read()]; + + for (var i = 0; i < ParticleStructs.Length; i++) + { + var particle = new ParticleStruct(); + particle.Deserialize(reader); + + ParticleStructs[i] = particle; + } + } + } +} \ No newline at end of file diff --git a/InfectedRose.Lvl/LevelInfo.cs b/InfectedRose.Lvl/LevelInfo.cs new file mode 100644 index 0000000..b68bba9 --- /dev/null +++ b/InfectedRose.Lvl/LevelInfo.cs @@ -0,0 +1,46 @@ +using System.IO; +using RakDotNet.IO; + +namespace InfectedRose.Lvl +{ + public class LevelInfo : ChunkBase + { + public uint LvlVersion { get; set; } + + public uint RevisionNumber { get; set; } + + public uint AddressChunk2000 { get; set; } + + public uint AddressChunk2001 { get; set; } + + public uint AddressChunk2002 { get; set; } + + public override uint ChunkType => 1000; + + public override void Serialize(BitWriter writer) + { + writer.Write(LvlVersion); + + writer.Write(RevisionNumber); + + writer.Write(AddressChunk2000); + + writer.Write(AddressChunk2001); + + writer.Write(AddressChunk2002); + } + + public override void Deserialize(BitReader reader) + { + LvlVersion = reader.Read(); + + RevisionNumber = reader.Read(); + + AddressChunk2000 = reader.Read(); + + AddressChunk2001 = reader.Read(); + + AddressChunk2002 = reader.Read(); + } + } +} \ No newline at end of file diff --git a/InfectedRose.Lvl/LevelObjectTemplate.cs b/InfectedRose.Lvl/LevelObjectTemplate.cs new file mode 100644 index 0000000..5b69688 --- /dev/null +++ b/InfectedRose.Lvl/LevelObjectTemplate.cs @@ -0,0 +1,84 @@ +using System.IO; +using System.Numerics; +using InfectedRose.Core; +using Lvl; +using RakDotNet.IO; + +namespace InfectedRose.Lvl +{ + public class LevelObjectTemplate : IConstruct + { + public ulong ObjectId { get; set; } + + public int Lot { get; set; } + + public uint AssetType { get; set; } + + public uint UnknownInt { get; set; } + + public Vector3 Position { get; set; } + + public Quaternion Rotation { get; set; } + + public float Scale { get; set; } + + public LegoDataDictionary LegoInfo { get; set; } + + public uint LvlVersion { get; set; } + + public LevelObjectTemplate(uint lvlVersion) + { + LvlVersion = lvlVersion; + } + + public void Serialize(BitWriter writer) + { + writer.Write(ObjectId); + + writer.Write(Lot); + + if (LvlVersion >= 0x26) + writer.Write(AssetType); + + if (LvlVersion >= 0x20) + writer.Write(UnknownInt); + + writer.Write(Position); + + writer.WriteNiQuaternion(Rotation); + + writer.Write(Scale); + + writer.WriteNiString(LegoInfo.ToString(), true); + + if (LvlVersion >= 0x7) + writer.Write(0u); + } + + public void Deserialize(BitReader reader) + { + ObjectId = reader.Read(); + + Lot = reader.Read(); + + if (LvlVersion >= 0x26) + AssetType = reader.Read(); + + if (LvlVersion >= 0x20) + UnknownInt = reader.Read(); + + Position = reader.Read(); + + Rotation = reader.ReadNiQuaternion(); + + Scale = reader.Read(); + + var legoInfo = reader.ReadNiString(true); + + LegoInfo = LegoDataDictionary.FromString(legoInfo); + + if (LvlVersion >= 0x7) + reader.Read(); + } + } +} \ No newline at end of file diff --git a/InfectedRose.Lvl/LevelObjects.cs b/InfectedRose.Lvl/LevelObjects.cs new file mode 100644 index 0000000..dab22a6 --- /dev/null +++ b/InfectedRose.Lvl/LevelObjects.cs @@ -0,0 +1,42 @@ +using System.IO; +using RakDotNet.IO; + +namespace InfectedRose.Lvl +{ + public class LevelObjects : ChunkBase + { + public LevelObjectTemplate[] Templates { get; set; } + + public uint LvlVersion { get; set; } + + public override uint ChunkType => 2001; + + public LevelObjects(uint lvlVersion) + { + LvlVersion = lvlVersion; + } + + public override void Serialize(BitWriter writer) + { + writer.Write((uint) Templates.Length); + + foreach (var template in Templates) + { + template.Serialize(writer); + } + } + + public override void Deserialize(BitReader reader) + { + Templates = new LevelObjectTemplate[reader.Read()]; + + for (var i = 0; i < Templates.Length; i++) + { + var template = new LevelObjectTemplate(LvlVersion); + template.Deserialize(reader); + + Templates[i] = template; + } + } + } +} \ No newline at end of file diff --git a/InfectedRose.Lvl/LevelSkyConfig.cs b/InfectedRose.Lvl/LevelSkyConfig.cs new file mode 100644 index 0000000..525c968 --- /dev/null +++ b/InfectedRose.Lvl/LevelSkyConfig.cs @@ -0,0 +1,138 @@ +using System; +using System.IO; +using InfectedRose.Core; +using RakDotNet.IO; + +namespace InfectedRose.Lvl +{ + public class LevelSkyConfig : ChunkBase + { + public IdStruct[] Identifiers { get; set; } + + public string[] SkyFilesPaths { get; set; } = new string[6]; + + public float[] UnknownFloatArray0 { get; set; } + + public float[] UnknownFloatArray1 { get; set; } = new float[3]; + + public float[] UnknownFloatArray2 { get; set; } = new float[3]; + + public byte[] UnknownSectionData { get; set; } + + public override uint ChunkType => 2000; + + public override void Serialize(BitWriter writer) + { + writer.Write(8); + + // TODO: Add UnknownFloatArray0? + + var addressPosition = writer.BaseStream.Position; + + writer.BaseStream.Position += 8; + + writer.Write((uint) Identifiers.Length); + + foreach (var identifier in Identifiers) + { + identifier.Serialize(writer); + } + + for (var i = 0; i < 3; i++) + { + writer.Write(UnknownFloatArray1[i]); + } + + for (var i = 0; i < 3; i++) + { + writer.Write(UnknownFloatArray2[i]); + } + + var skySectionAddress = writer.BaseStream.Position; + + writer.Write(WriteSkySection()); + + var otherSectionAddress = writer.BaseStream.Position; + + writer.Write(WriteOtherSection()); + + writer.BaseStream.Position = addressPosition; + + writer.Write((uint) skySectionAddress); + + writer.Write((uint) otherSectionAddress); + + writer.BaseStream.Position = otherSectionAddress + 4; + } + + private byte[] WriteSkySection() + { + using var stream = new MemoryStream(); + using var writer = new BitWriter(stream); + + for (var i = 0; i < 6; i++) + { + writer.WriteNiString(SkyFilesPaths[i]); + } + + return stream.ToArray(); + } + + private byte[] WriteOtherSection() + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + + writer.Write(0u); + + return stream.ToArray(); + } + + public override void Deserialize(BitReader reader) + { + var sizeOfData = reader.Read(); + + var skySectionAddress = reader.Read(); + + var otherSectionAddress = reader.Read(); + + UnknownFloatArray0 = new float[(sizeOfData - 8) / 4]; + + for (var i = 0; i < UnknownFloatArray0.Length; i++) + { + UnknownFloatArray0[i] = reader.Read(); + } + + Identifiers = new IdStruct[reader.Read()]; + + for (var i = 0; i < Identifiers.Length; i++) + { + var idStruct = new IdStruct(); + idStruct.Deserialize(reader); + + Identifiers[i] = idStruct; + } + + for (var i = 0; i < 3; i++) + { + UnknownFloatArray1[i] = reader.Read(); + } + + for (var i = 0; i < 3; i++) + { + UnknownFloatArray2[i] = reader.Read(); + } + + reader.BaseStream.Position = skySectionAddress; + + for (var i = 0; i < 6; i++) + { + SkyFilesPaths[i] = reader.ReadNiString(); + } + + reader.BaseStream.Position = otherSectionAddress; + + UnknownSectionData = reader.ReadBuffer(reader.Read()); + } + } +} \ No newline at end of file diff --git a/InfectedRose.Lvl/LvlFile.cs b/InfectedRose.Lvl/LvlFile.cs new file mode 100644 index 0000000..250f504 --- /dev/null +++ b/InfectedRose.Lvl/LvlFile.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using InfectedRose.Core; +using RakDotNet.IO; + +namespace InfectedRose.Lvl +{ + public class LvlFile : IConstruct + { + public uint LvlVersion { get; set; } + + public LevelInfo LevelInfo; + + public LevelSkyConfig LevelSkyConfig; + + public LevelObjects LevelObjects; + + public LevelEnvironmentConfig LevelEnvironmentConfig; + + private static readonly byte[] ChunkHeader = "CHNK".Select(c => (byte) c).ToArray(); + + public void Serialize(BitWriter writer) + { + var levelInfoData = WriteChunk(LevelInfo); + var levelSkyData = WriteChunk(LevelSkyConfig); + var levelObjectsData = WriteChunk(LevelObjects); + var levelEnvData = WriteChunk(LevelEnvironmentConfig); + + Console.WriteLine($"{levelInfoData.Length} - {levelSkyData.Length} - {levelObjectsData.Length} - {levelEnvData.Length}"); + } + + private static byte[] WriteChunk(IConstruct chunk) + { + using var stream = new MemoryStream(); + using var writer = new BitWriter(stream); + + chunk.Serialize(writer); + + return stream.ToArray(); + } + + public void Deserialize(BitReader reader) + { + var magic = new string(reader.ReadBuffer(4).Select(s => (char) s).ToArray()); + + if (magic != "CHNK") + { + Console.WriteLine(new NotImplementedException("Older LVL files not yet supported.")); + + return; + } + + reader.BaseStream.Position = 0; + + while (reader.BaseStream.Position != reader.BaseStream.Length) + { + var startPosition = reader.BaseStream.Position; + + magic = new string(reader.ReadBuffer(4).Select(s => (char) s).ToArray()); + + if (startPosition % 16 != 0 || magic != "CHNK") + { + throw new Exception($"{startPosition} % 16 = {startPosition % 16} | {magic}"); + } + + var chunkType = reader.Read(); + + reader.Read(); + + var index = reader.Read(); + + var chunkLength = reader.Read(); + + var chunkAddress = reader.Read(); + + reader.BaseStream.Position = chunkAddress; + + if (reader.BaseStream.Position % 16 != 0) break; + + switch (chunkType) + { + case 1000: + var chunk1000 = new LevelInfo + { + Index = index + }; + + chunk1000.Deserialize(reader); + + LvlVersion = chunk1000.LvlVersion; + + LevelInfo = chunk1000; + break; + case 2000: + var chunk2000 = new LevelSkyConfig + { + Index = index + }; + + chunk2000.Deserialize(reader); + + LevelSkyConfig = chunk2000; + break; + case 2001: + var chunk2001 = new LevelObjects(LvlVersion) + { + Index = index + }; + + chunk2001.Deserialize(reader); + + LevelObjects = chunk2001; + break; + case 2002: + var chunk2002 = new LevelEnvironmentConfig + { + Index = index + }; + + chunk2002.Deserialize(reader); + + LevelEnvironmentConfig = chunk2002; + break; + default: + throw new ArgumentOutOfRangeException($"{chunkType} is not a valid chunk type."); + } + + while (reader.BaseStream.Position != startPosition + chunkLength) + { + reader.Read(); + } + + reader.BaseStream.Position = startPosition + chunkLength; + + Console.WriteLine($"[END] -> {reader.BaseStream.Position}"); + } + } + } +} \ No newline at end of file diff --git a/InfectedRose.Lvl/ParticleStruct.cs b/InfectedRose.Lvl/ParticleStruct.cs new file mode 100644 index 0000000..80e4dc8 --- /dev/null +++ b/InfectedRose.Lvl/ParticleStruct.cs @@ -0,0 +1,57 @@ +using System; +using System.Numerics; +using InfectedRose.Core; +using RakDotNet.IO; + +namespace InfectedRose.Lvl +{ + public class ParticleStruct : IConstruct + { + public byte[] UnknownByteArray0 { get; set; } = new byte[2]; + + public Vector3 Position { get; set; } + + public Quaternion Rotation { get; set; } + + public string ParticleName { get; set; } + + public byte[] UnknownByteArray1 { get; set; } = new byte[4]; + + public void Serialize(BitWriter writer) + { + writer.Write(UnknownByteArray0); + + writer.Write(Position); + + writer.WriteNiQuaternion(Rotation); + + writer.WriteNiString(ParticleName, true); + + writer.Write(UnknownByteArray1); + } + + public void Deserialize(BitReader reader) + { + UnknownByteArray0 = reader.ReadBuffer(2); + + Position = reader.Read(); + + Rotation = reader.ReadNiQuaternion(); + + var position = reader.BaseStream.Position; + + try + { + ParticleName = reader.ReadNiString(true); + + Console.WriteLine(ParticleName); + } + catch + { + reader.BaseStream.Position = position + 4; + } + + UnknownByteArray1 = reader.ReadBuffer(4); + } + } +} \ No newline at end of file diff --git a/InfectedRose.Nif/BlockInfo.cs b/InfectedRose.Nif/BlockInfo.cs new file mode 100644 index 0000000..1a5be88 --- /dev/null +++ b/InfectedRose.Nif/BlockInfo.cs @@ -0,0 +1,9 @@ +namespace InfectedRose.Nif +{ + public class BlockInfo + { + public ushort TypeIndex { get; set; } + + public uint Size { get; set; } + } +} \ No newline at end of file diff --git a/InfectedRose.Nif/Enums/Endian.cs b/InfectedRose.Nif/Enums/Endian.cs new file mode 100644 index 0000000..ca60525 --- /dev/null +++ b/InfectedRose.Nif/Enums/Endian.cs @@ -0,0 +1,8 @@ +namespace InfectedRose.Nif +{ + public enum Endian : byte + { + EndianBig, + EndianLittle + } +} \ No newline at end of file diff --git a/InfectedRose.Nif/Enums/NifVersion.cs b/InfectedRose.Nif/Enums/NifVersion.cs new file mode 100644 index 0000000..178d66c --- /dev/null +++ b/InfectedRose.Nif/Enums/NifVersion.cs @@ -0,0 +1,170 @@ +namespace InfectedRose.Nif +{ + public enum NifVersion : uint + { + /// + /// The ve R_2_3 + /// + Ver23 = 33751040u, + + /// + /// The ve R_3_0 + /// + Ver30 = 50331648u, + + /// + /// The ve R_3_03 + /// + Ver303 = 50332416u, + + /// + /// The ve R_3_1 + /// + Ver31 = 50397184u, + + /// + /// The ve R_3_3_0_13 + /// + Ver33013 = 50528269u, + + /// + /// The ve R_4_0_0_0 + /// + Ver4000 = 67108864u, + + /// + /// The ve R_4_0_0_2 + /// + Ver4002 = 67108866u, + + /// + /// The ve R_4_1_0_12 + /// + Ver4101 = 0x04010001, + + /// + /// The ve R_4_1_0_12 + /// + Ver41012 = 67174412u, + + /// + /// The ve R_4_2_0_2 + /// + Ver4202 = 67239938u, + + /// + /// The ve R_4_2_1_0 + /// + Ver4210 = 67240192u, + + /// + /// The ve R_4_2_2_0 + /// + Ver4220 = 67240448u, + + /// + /// The ve R_5_0_0_1 + /// + Ver5001 = 83886081u, + + /// + /// The ve R_10_0_1_0 + /// + Ver10010 = 167772416u, + + /// + /// The ve R_10_0_1_2 + /// + Ver10012 = 167772418u, + + /// + /// The ve R_10_0_1_3 + /// + Ver10013, + + /// + /// The ve R_10_1_0_0 + /// + Ver10100 = 167837696u, + + /// + /// The ve R_10_1_0_101 + /// + Ver1010101 = 167837797u, + + /// + /// The ve R_10_1_0_106 + /// + Ver1010106 = 167837802u, + + /// + /// The ve R_10_2_0_0 + /// + Ver10200 = 167903232u, + + /// + /// The ve R_10_4_0_1 + /// + Ver10401 = 168034305u, + + /// + /// The ve R_20_0_0_4 + /// + Ver20004 = 335544324u, + + /// + /// The ve R_20_0_0_5 + /// + Ver20005, + + /// + /// The ve R_20_1_0_3 + /// + Ver20103 = 335609859u, + + /// + /// The ve R_20_2_0_7 + /// + Ver20207 = 335675399u, + + /// + /// The ve R_20_2_0_8 + /// + Ver20208, + + /// + /// The ve R_20_3_0_1 + /// + Ver20301 = 335740929u, + + /// + /// The ve R_20_3_0_2 + /// + Ver20302, + + /// + /// The ve R_20_3_0_3 + /// + Ver20303, + + /// + /// The ve R_20_3_0_6 + /// + Ver20306 = 335740934u, + + /// + /// The ve R_20_3_0_9 + /// + Ver20309 = 335740937u, // LEGO Universe + + /// + /// The ve r_ unsupported + /// + VerUnsupported = 4294967295u, + + /// + /// The ve r_ invalid + /// + VerInvalid = 4294967294u + } +} \ No newline at end of file diff --git a/InfectedRose.Nif/Header.cs b/InfectedRose.Nif/Header.cs new file mode 100644 index 0000000..03e540c --- /dev/null +++ b/InfectedRose.Nif/Header.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using System.Text; +using InfectedRose.Core; +using RakDotNet.IO; + +namespace InfectedRose.Nif +{ + public class Header : IConstruct + { + public string HeaderString { get; set; } + + public NifVersion Version { get; set; } + + public Endian Endian { get; set; } + + public string VersionString { get; set; } + + public uint UserVersion { get; set; } + + public BlockInfo[] NodeInfo { get; set; } + + public NifString[] NodeTypes { get; set; } + + public uint MaxStringLength { get; set; } + + public uint[] Groups { get; set; } + + public void Serialize(BitWriter writer) + { + throw new System.NotImplementedException(); + } + + public void Deserialize(BitReader reader) + { + var versionStringBuilder = new StringBuilder(); + + var character = reader.Read(); + while (character != 0xA) + { + versionStringBuilder.Append((char) character); + + character = reader.Read(); + } + + VersionString = versionStringBuilder.ToString(); + + reader.Read(); + + Version = (NifVersion) reader.Read(); + + Endian = (Endian) reader.Read(); + + UserVersion = reader.Read(); + + NodeInfo = new BlockInfo[reader.Read()]; + + NodeTypes = new NifString[reader.Read()]; + + for (var i = 0; i < NodeTypes.Length; i++) + { + var str = new NifString(); + + str.Deserialize(reader); + + NodeTypes[i] = str; + } + + for (var i = 0; i < NodeInfo.Length; i++) + { + NodeInfo[i] = new BlockInfo + { + TypeIndex = reader.Read() + }; + } + + foreach (var info in NodeInfo) + { + info.Size = reader.Read(); + } + } + } +} \ No newline at end of file diff --git a/InfectedRose.Nif/InfectedRose.Nif.csproj b/InfectedRose.Nif/InfectedRose.Nif.csproj new file mode 100644 index 0000000..c1c8a3e --- /dev/null +++ b/InfectedRose.Nif/InfectedRose.Nif.csproj @@ -0,0 +1,15 @@ + + + + netcoreapp3.0 + + + + + + + + + + + diff --git a/InfectedRose.Nif/NifFile.cs b/InfectedRose.Nif/NifFile.cs new file mode 100644 index 0000000..d92120d --- /dev/null +++ b/InfectedRose.Nif/NifFile.cs @@ -0,0 +1,18 @@ +using InfectedRose.Core; +using RakDotNet.IO; + +namespace InfectedRose.Nif +{ + public class NifFile : IConstruct + { + public void Serialize(BitWriter writer) + { + throw new System.NotImplementedException(); + } + + public void Deserialize(BitReader reader) + { + throw new System.NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/InfectedRose.Nif/NifString.cs b/InfectedRose.Nif/NifString.cs new file mode 100644 index 0000000..64711ce --- /dev/null +++ b/InfectedRose.Nif/NifString.cs @@ -0,0 +1,40 @@ +using InfectedRose.Core; +using RakDotNet.IO; + +namespace InfectedRose.Nif +{ + public class NifString : IConstruct + { + public string Value { get; set; } + + public bool Wide { get; set; } + + public bool Small { get; set; } + + public void Serialize(BitWriter writer) + { + if (Small) writer.Write((byte) Value.Length); + else writer.Write((uint) Value.Length); + + foreach (var character in Value) + { + if (Wide) writer.Write((ushort) character); + else writer.Write((byte) character); + } + } + + public void Deserialize(BitReader reader) + { + var length = Small ? reader.Read() : reader.Read(); + + var chars = new char[length]; + + for (var i = 0; i < length; i++) + { + chars[i] = (char) (Wide ? reader.Read() : reader.Read()); + } + + Value = new string(chars); + } + } +} \ No newline at end of file diff --git a/InfectedRose.Terrain/Chunk.cs b/InfectedRose.Terrain/Chunk.cs new file mode 100644 index 0000000..d66addd --- /dev/null +++ b/InfectedRose.Terrain/Chunk.cs @@ -0,0 +1,134 @@ +using InfectedRose.Core; +using RakDotNet.IO; + +namespace InfectedRose.Terrain +{ + public class Chunk : IConstruct + { + public int ChunkIndex { get; set; } + + public HeightMap HeightMap { get; set; } + + public ColorMap Colormap0 { get; set; } + + public TerrainDirectDraw Lightmap { get; set; } + + public ColorMap Colormap1 { get; set; } + + public byte UnknownByte { get; set; } + + public TerrainDirectDraw Blendmap { get; set; } + + public WeirdStruct[] WeirdStructs { get; set; } + + public byte[] UnknownByteArray0 { get; set; } + + public ShortMap ShortMap { get; set; } + + public short[][] UnknownShortArray { get; set; } + + public byte[] UnknownByteArray1 { get; set; } + + public void Serialize(BitWriter writer) + { + writer.Write(ChunkIndex); + + HeightMap.Serialize(writer); + + Colormap0.Serialize(writer); + + Lightmap.Serialize(writer); + + Colormap1.Serialize(writer); + + writer.Write(UnknownByte); + + Blendmap.Serialize(writer); + + writer.Write(WeirdStructs.Length); + + foreach (var weirdStruct in WeirdStructs) + { + weirdStruct.Serialize(writer); + } + + foreach (var unknownByte in UnknownByteArray0) + { + writer.Write(unknownByte); + } + + ShortMap.Serialize(writer); + + if (ShortMap.Data.Length == default) return; + + for (var i = 0; i < 32; i++) + { + writer.Write(UnknownByteArray1[i]); + } + + for (var i = 0; i < 16; i++) + { + writer.Write((short) UnknownShortArray[i].Length); + + for (var j = 0; j < UnknownShortArray[i].Length; j++) + { + writer.Write(UnknownShortArray[i][j]); + } + } + } + + public void Deserialize(BitReader reader) + { + ChunkIndex = reader.Read(); + + HeightMap = new HeightMap(); + HeightMap.Deserialize(reader); + + Colormap0 = new ColorMap(); + Colormap0.Deserialize(reader); + + Lightmap = new TerrainDirectDraw(); + Lightmap.Deserialize(reader); + + Colormap1 = new ColorMap(); + Colormap1.Deserialize(reader); + + UnknownByte = reader.Read(); + + Blendmap = new TerrainDirectDraw(); + Blendmap.Deserialize(reader); + + WeirdStructs = new WeirdStruct[reader.Read()]; + + for (var i = 0; i < WeirdStructs.Length; i++) + { + var weirdStruct = new WeirdStruct(); + weirdStruct.Deserialize(reader); + + WeirdStructs[i] = weirdStruct; + } + + UnknownByteArray0 = reader.ReadBuffer((uint) (Colormap0.Width * Colormap0.Height)); + + ShortMap = new ShortMap(); + ShortMap.Deserialize(reader); + + if (ShortMap.Data.Length == default) return; + + UnknownByteArray1 = reader.ReadBuffer(32); + + UnknownShortArray = new short[16][]; + + for (var i = 0; i < 16; i++) + { + var length = reader.Read(); + UnknownShortArray[i] = new short[length]; + + for (var j = 0; j < length; j++) + { + UnknownShortArray[i][j] = reader.Read(); + } + } + } + } +} \ No newline at end of file diff --git a/InfectedRose.Terrain/ColorMap.cs b/InfectedRose.Terrain/ColorMap.cs new file mode 100644 index 0000000..64b828b --- /dev/null +++ b/InfectedRose.Terrain/ColorMap.cs @@ -0,0 +1,41 @@ +using InfectedRose.Core; +using RakDotNet.IO; + +namespace InfectedRose.Terrain +{ + public class ColorMap : IConstruct + { + public int Width { get; set; } + + public int Height { get; set; } + + public uint[] Data { get; set; } + + public uint GetValue(int x, int y) + { + return Data[y * Width + x]; + } + + public void Serialize(BitWriter writer) + { + writer.Write(Width); + + for (var i = 0; i < Width * Height; i++) + { + writer.Write(Data[i]); + } + } + + public void Deserialize(BitReader reader) + { + Width = Height = reader.Read(); + + Data = new uint[Width * Height]; + + for (var i = 0; i < Data.Length; i++) + { + Data[i] = reader.Read(); + } + } + } +} \ No newline at end of file diff --git a/InfectedRose.Terrain/HeightMap.cs b/InfectedRose.Terrain/HeightMap.cs new file mode 100644 index 0000000..529e3bc --- /dev/null +++ b/InfectedRose.Terrain/HeightMap.cs @@ -0,0 +1,66 @@ +using InfectedRose.Core; +using RakDotNet.IO; + +namespace InfectedRose.Terrain +{ + public class HeightMap : IConstruct + { + public int Width { get; set; } + + public int Height { get; set; } + + public float UnknownFloat0 { get; set; } + public float UnknownFloat1 { get; set; } + public float UnknownFloat2 { get; set; } + + public int[] UnknownIntArray { get; set; } + + public float[] Data { get; set; } + + public void Serialize(BitWriter writer) + { + writer.Write(Width); + writer.Write(Height); + + writer.Write(UnknownFloat0); + writer.Write(UnknownFloat1); + + for (var i = 0; i < 4; i++) + { + writer.Write(UnknownIntArray[i]); + } + + writer.Write(UnknownFloat2); + + for (var i = 0; i < Width * Height; i++) + { + writer.Write(Data[i]); + } + } + + public void Deserialize(BitReader reader) + { + Width = reader.Read(); + Height = reader.Read(); + + UnknownFloat0 = reader.Read(); + UnknownFloat1 = reader.Read(); + + UnknownIntArray = new int[4]; + + for (var i = 0; i < 4; i++) + { + UnknownIntArray[i] = reader.Read(); + } + + UnknownFloat2 = reader.Read(); + + Data = new float[Width * Height]; + + for (var i = 0; i < Data.Length; i++) + { + Data[i] = reader.Read(); + } + } + } +} \ No newline at end of file diff --git a/InfectedRose.Terrain/InfectedRose.Terrain.csproj b/InfectedRose.Terrain/InfectedRose.Terrain.csproj new file mode 100644 index 0000000..543ea48 --- /dev/null +++ b/InfectedRose.Terrain/InfectedRose.Terrain.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp3.0 + + + + + + + diff --git a/InfectedRose.Terrain/ShortMap.cs b/InfectedRose.Terrain/ShortMap.cs new file mode 100644 index 0000000..577678d --- /dev/null +++ b/InfectedRose.Terrain/ShortMap.cs @@ -0,0 +1,30 @@ +using InfectedRose.Core; +using RakDotNet.IO; + +namespace InfectedRose.Terrain +{ + public class ShortMap : IConstruct + { + public short[] Data { get; set; } + + public void Serialize(BitWriter writer) + { + writer.Write(Data.Length); + + foreach (var data in Data) + { + writer.Write(data); + } + } + + public void Deserialize(BitReader reader) + { + Data = new short[reader.Read()]; + + for (var i = 0; i < Data.Length; i++) + { + Data[i] = reader.Read(); + } + } + } +} \ No newline at end of file diff --git a/InfectedRose.Terrain/TerrainDirectDraw.cs b/InfectedRose.Terrain/TerrainDirectDraw.cs new file mode 100644 index 0000000..dff5fda --- /dev/null +++ b/InfectedRose.Terrain/TerrainDirectDraw.cs @@ -0,0 +1,25 @@ +using InfectedRose.Core; +using RakDotNet.IO; + +namespace InfectedRose.Terrain +{ + public class TerrainDirectDraw : IConstruct + { + public byte[] Data { get; set; } + + public void Serialize(BitWriter writer) + { + writer.Write(Data.Length); + + foreach (var data in Data) + { + writer.Write(data); + } + } + + public void Deserialize(BitReader reader) + { + Data = reader.ReadBuffer((uint) reader.Read()); + } + } +} \ No newline at end of file diff --git a/InfectedRose.Terrain/TerrainEditor.cs b/InfectedRose.Terrain/TerrainEditor.cs new file mode 100644 index 0000000..b502cdb --- /dev/null +++ b/InfectedRose.Terrain/TerrainEditor.cs @@ -0,0 +1,7 @@ +namespace InfectedRose.Terrain +{ + public class TerrainEditor + { + + } +} \ No newline at end of file diff --git a/InfectedRose.Terrain/TerrainFile.cs b/InfectedRose.Terrain/TerrainFile.cs new file mode 100644 index 0000000..74c1222 --- /dev/null +++ b/InfectedRose.Terrain/TerrainFile.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using InfectedRose.Core; +using RakDotNet.IO; + +namespace InfectedRose.Terrain +{ + public class TerrainFile : IConstruct + { + public List Chunks; + + public int ChunkTotalCount { get; set; } + + public int ChunkCountX { get; set; } + + public int ChunkCountY { get; set; } + + public byte[] UnknownHeader { get; set; } + + public void Serialize(BitWriter writer) + { + writer.Write(UnknownHeader); + + writer.Write(ChunkTotalCount); + writer.Write(ChunkCountX); + writer.Write(ChunkCountY); + + foreach (var chunk in Chunks) + { + chunk.Serialize(writer); + } + } + + public void Deserialize(BitReader reader) + { + Chunks = new List(); + + UnknownHeader = reader.ReadBuffer(3); + + ChunkTotalCount = reader.Read(); + ChunkCountX = reader.Read(); + ChunkCountY = reader.Read(); + + for (var i = 0; i < ChunkTotalCount; i++) + { + var chunk = new Chunk(); + chunk.Deserialize(reader); + + Chunks.Add(chunk); + } + } + } +} \ No newline at end of file diff --git a/InfectedRose.Terrain/WeirdStruct.cs b/InfectedRose.Terrain/WeirdStruct.cs new file mode 100644 index 0000000..f4f0816 --- /dev/null +++ b/InfectedRose.Terrain/WeirdStruct.cs @@ -0,0 +1,51 @@ +using System.Numerics; +using InfectedRose.Core; +using RakDotNet.IO; + +namespace InfectedRose.Terrain +{ + public class WeirdStruct : IConstruct + { + public int Type { get; set; } + + public Vector3 Position { get; set; } + + public Quaternion Rotation { get; set; } + + public uint UnknownInt { get; set; } + + public void Serialize(BitWriter writer) + { + writer.Write(Type); + + writer.Write(Rotation.W); + + writer.Write(Position); + + writer.Write(Rotation.X); + writer.Write(Rotation.Y); + writer.Write(Rotation.Z); + + writer.Write(UnknownInt); + } + + public void Deserialize(BitReader reader) + { + Type = reader.Read(); + + var rotW = reader.Read(); + + Position = reader.Read(); + + Rotation = new Quaternion + { + X = reader.Read(), + Y = reader.Read(), + Z = reader.Read(), + W = rotW + }; + + UnknownInt = reader.Read(); + } + } +} \ No newline at end of file diff --git a/InfectedRose.sln b/InfectedRose.sln new file mode 100644 index 0000000..5e8b33e --- /dev/null +++ b/InfectedRose.sln @@ -0,0 +1,58 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InfectedRose.Terrain", "InfectedRose.Terrain\InfectedRose.Terrain.csproj", "{C5948668-31F0-457F-A1D7-A32DB85A6A5D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InfectedRose.Examples", "InfectedRose.Examples\InfectedRose.Examples.csproj", "{135A3A1D-3C81-45AB-9195-DEB44D2581F4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InfectedRose.Core", "InfectedRose.Core\InfectedRose.Core.csproj", "{2D0A3B97-ACED-487E-AF18-F6E81627D0F6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InfectedRose.Luz", "InfectedRose.Luz\InfectedRose.Luz.csproj", "{0C38A6FC-7EF9-4B97-B767-6A2F40962D29}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InfectedRose.Lvl", "InfectedRose.Lvl\InfectedRose.Lvl.csproj", "{8BC6E972-A4B6-4D79-9948-A3265FAAF2FC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InfectedRose.Database", "InfectedRose.Database\InfectedRose.Database.csproj", "{94A2F951-BA5B-4C51-9EC5-C909D97D30A3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RakDotNet.IO", "RakDotNet.IO\RakDotNet.IO\RakDotNet.IO.csproj", "{C5BEDCE5-382D-4963-B68F-F098A9EB3CA8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InfectedRose.Nif", "InfectedRose.Nif\InfectedRose.Nif.csproj", "{BDCD4970-7BFF-4C69-B866-F022583D9E18}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C5948668-31F0-457F-A1D7-A32DB85A6A5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C5948668-31F0-457F-A1D7-A32DB85A6A5D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C5948668-31F0-457F-A1D7-A32DB85A6A5D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C5948668-31F0-457F-A1D7-A32DB85A6A5D}.Release|Any CPU.Build.0 = Release|Any CPU + {135A3A1D-3C81-45AB-9195-DEB44D2581F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {135A3A1D-3C81-45AB-9195-DEB44D2581F4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {135A3A1D-3C81-45AB-9195-DEB44D2581F4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {135A3A1D-3C81-45AB-9195-DEB44D2581F4}.Release|Any CPU.Build.0 = Release|Any CPU + {2D0A3B97-ACED-487E-AF18-F6E81627D0F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D0A3B97-ACED-487E-AF18-F6E81627D0F6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D0A3B97-ACED-487E-AF18-F6E81627D0F6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D0A3B97-ACED-487E-AF18-F6E81627D0F6}.Release|Any CPU.Build.0 = Release|Any CPU + {0C38A6FC-7EF9-4B97-B767-6A2F40962D29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0C38A6FC-7EF9-4B97-B767-6A2F40962D29}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0C38A6FC-7EF9-4B97-B767-6A2F40962D29}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0C38A6FC-7EF9-4B97-B767-6A2F40962D29}.Release|Any CPU.Build.0 = Release|Any CPU + {8BC6E972-A4B6-4D79-9948-A3265FAAF2FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8BC6E972-A4B6-4D79-9948-A3265FAAF2FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8BC6E972-A4B6-4D79-9948-A3265FAAF2FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8BC6E972-A4B6-4D79-9948-A3265FAAF2FC}.Release|Any CPU.Build.0 = Release|Any CPU + {94A2F951-BA5B-4C51-9EC5-C909D97D30A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {94A2F951-BA5B-4C51-9EC5-C909D97D30A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {94A2F951-BA5B-4C51-9EC5-C909D97D30A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {94A2F951-BA5B-4C51-9EC5-C909D97D30A3}.Release|Any CPU.Build.0 = Release|Any CPU + {C5BEDCE5-382D-4963-B68F-F098A9EB3CA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C5BEDCE5-382D-4963-B68F-F098A9EB3CA8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C5BEDCE5-382D-4963-B68F-F098A9EB3CA8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C5BEDCE5-382D-4963-B68F-F098A9EB3CA8}.Release|Any CPU.Build.0 = Release|Any CPU + {BDCD4970-7BFF-4C69-B866-F022583D9E18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BDCD4970-7BFF-4C69-B866-F022583D9E18}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BDCD4970-7BFF-4C69-B866-F022583D9E18}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BDCD4970-7BFF-4C69-B866-F022583D9E18}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal