├── .gitattributes
├── Docs
├── images
│ ├── broken-in-game.png
│ ├── original
│ │ ├── rigging_broken.png
│ │ ├── rigging_rotated.png
│ │ └── rigging_not_rotated.png
│ ├── resized_rigging_broken.png
│ ├── resized_rigging_rotated.png
│ └── resized_rigging_not_rotated.png
└── RiggingPosesAnimations.md
├── .gitmodules
├── PD2ModelParser
├── StaticStorage.cs
├── Importers
│ ├── IOptionReceiver.cs
│ ├── AnimationImporter.cs
│ └── ModelReader.cs
├── Inspector
│ ├── Object3DReferenceConverter.cs
│ ├── HashNameConverter.cs
│ └── InspectionTree.cs
├── UI
│ ├── Form1.cs
│ ├── FileBrowserControl.Designer.cs
│ ├── ExportPanel.cs
│ ├── ExportPanel.Designer.cs
│ ├── FileBrowserControl.resx
│ ├── ExportPanel.resx
│ ├── ImportPanel.resx
│ ├── Form1.resx
│ ├── ObjectsPanel.cs
│ └── ObjectsPanel.resx
├── Sections
│ ├── IAnimationController.cs
│ ├── Unknown.cs
│ ├── TopologyIP.cs
│ ├── Camera.cs
│ ├── SectionHeader.cs
│ ├── CustomHashlist.cs
│ ├── LookAtConstrRotationController.cs
│ ├── PassthroughGP.cs
│ ├── Author.cs
│ ├── LinearFloatController.cs
│ ├── Animation.cs
│ ├── Material.cs
│ ├── MaterialGroup.cs
│ ├── Light.cs
│ ├── LinearVector3Controller.cs
│ ├── QuatLinearRotationController.cs
│ ├── SkinBones.cs
│ ├── Topology.cs
│ └── Bones.cs
├── obj_data.cs
├── InternationalisationUtils.cs
├── Settings.cs
├── Properties
│ ├── AssemblyInfo.cs.in
│ ├── Resources.Designer.cs
│ └── Resources.resx
├── App.config
├── Exporters
│ ├── AnimationExporter.cs
│ └── DieselExporter.cs
├── Misc
│ ├── ZLib
│ │ ├── Adler32.cs
│ │ └── ZLibHeader.cs
│ ├── BulkFunctions.cs
│ └── SerializeUtils.cs
├── PD2ModelParser.csproj
├── KnownIndex.cs
├── Logging.cs
├── FullModelData.cs
├── Filetype.cs
├── Tests
│ └── MultiplicationTest.cs
└── Modelscript
│ └── MergeCommand.cs
├── .gitignore
├── gen-version.sh
├── PD2ModelParser.sln
├── Backup
└── PD2ModelParser.sln
├── Research Notes
├── notes.txt
├── decompiled object matrix parsing.txt
├── decompiled matrix parsing.txt
├── decompiled matrix parsing 2.txt
└── format_documentation.md
├── appveyor.yml
└── README.md
/.gitattributes:
--------------------------------------------------------------------------------
1 | /gen-version.sh eol=lf
--------------------------------------------------------------------------------
/Docs/images/broken-in-game.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kythyria/payday2-model-tool/HEAD/Docs/images/broken-in-game.png
--------------------------------------------------------------------------------
/Docs/images/original/rigging_broken.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kythyria/payday2-model-tool/HEAD/Docs/images/original/rigging_broken.png
--------------------------------------------------------------------------------
/Docs/images/original/rigging_rotated.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kythyria/payday2-model-tool/HEAD/Docs/images/original/rigging_rotated.png
--------------------------------------------------------------------------------
/Docs/images/resized_rigging_broken.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kythyria/payday2-model-tool/HEAD/Docs/images/resized_rigging_broken.png
--------------------------------------------------------------------------------
/Docs/images/resized_rigging_rotated.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kythyria/payday2-model-tool/HEAD/Docs/images/resized_rigging_rotated.png
--------------------------------------------------------------------------------
/Docs/images/original/rigging_not_rotated.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kythyria/payday2-model-tool/HEAD/Docs/images/original/rigging_not_rotated.png
--------------------------------------------------------------------------------
/Docs/images/resized_rigging_not_rotated.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kythyria/payday2-model-tool/HEAD/Docs/images/resized_rigging_not_rotated.png
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "hashlist"]
2 | path = PD2ModelParser/hashlist
3 | url = https://github.com/Luffyyy/PAYDAY-2-Hashlist
4 | branch = master
5 | shallow = true
6 |
--------------------------------------------------------------------------------
/PD2ModelParser/StaticStorage.cs:
--------------------------------------------------------------------------------
1 | using PD2Bundle;
2 |
3 | namespace PD2ModelParser
4 | {
5 | ///
6 | /// The static storage.
7 | ///
8 | public static class StaticStorage
9 | {
10 | #region Static Fields
11 | ///
12 | /// The known index.
13 | ///
14 | public static KnownIndex hashindex = new KnownIndex();
15 |
16 | #endregion
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.vs
2 | /.vscode
3 | /packages
4 | /PD2ModelParser/bin
5 | /PD2ModelParser/obj
6 |
7 | # This is autogenerated by gen-version.sh before the build
8 | /PD2ModelParser/Properties/AssemblyInfo.cs
9 |
10 | # The FBX-related libraries
11 | /Libs/FbxNet.dll
12 | /Libs/FbxNetNative.dll
13 | /Libs/libFbxNet.so
14 | /Libs/libFbxNetNative.so
15 | /Libs/libfbxsdk.dll
16 | /Libs/libfbxsdk.so
17 |
18 | # VS options file
19 | # https://docs.microsoft.com/en-us/visualstudio/extensibility/internals/solution-user-options-dot-suo-file
20 | *.suo
21 |
22 | # VS per-user project settings
23 | *.csproj.user
24 |
--------------------------------------------------------------------------------
/PD2ModelParser/Importers/IOptionReceiver.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace PD2ModelParser.Importers
4 | {
5 | public interface IOptionReceiver
6 | {
7 | void AddOption(string name, string value);
8 |
9 | string GetOption(string name);
10 | }
11 |
12 | public class GenericOptionReceiver : IOptionReceiver
13 | {
14 | public Dictionary Options { get; private set; } = new Dictionary();
15 | public void AddOption(string name, string value) => Options.Add(name, value);
16 | public string GetOption(string name) => Options.GetValueOrDefault(name, null);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/PD2ModelParser/Inspector/Object3DReferenceConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using System.ComponentModel;
7 |
8 | namespace PD2ModelParser.Inspector
9 | {
10 | class Object3DReferenceConverter : TypeConverter
11 | {
12 | public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
13 | {
14 | if (destinationType != typeof(string) || !(value is Sections.Object3D section))
15 | return base.ConvertTo(context, culture, value, destinationType);
16 |
17 | return section.HashName.String;
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/gen-version.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | cd "`dirname "$0"`"
4 |
5 | cd PD2ModelParser/Properties
6 |
7 | cat << EOD > AssemblyInfo.cs
8 | // WARNING: This file is generated! Do not modify it, your
9 | // changes will be overwritten during the build process. Instead,
10 | // modify AssemblyInfo.cs.in, from which this file is generated.
11 | //
12 | EOD
13 |
14 | if [[ "$1" =~ "Release" ]]; then
15 | VER=`git describe --dirty=-modified`
16 | if [[ $VER =~ v([0-9]+\.[0-9]+\.[0-9]+) ]]; then
17 | VPRE=${BASH_REMATCH[1]}
18 | else
19 | echo "ERR: Describe ($VER) does not contain version in correct format!"
20 | rm -f AssemblyInfo.cs
21 | exit 1
22 | #VPRE="1.0.0"
23 | fi
24 | VN="$VPRE.` git rev-list --count HEAD `"
25 | sed "s/\"1.0.0.0\"/\"$VN\"/g;s/Debug Build/$VER/g" AssemblyInfo.cs.in >> AssemblyInfo.cs
26 | else
27 | cat "AssemblyInfo.cs.in" >> "AssemblyInfo.cs"
28 | fi
29 |
30 | dos2unix -q "AssemblyInfo.cs"
31 |
32 |
--------------------------------------------------------------------------------
/PD2ModelParser/UI/Form1.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Reflection;
3 | using System.Windows.Forms;
4 |
5 | namespace PD2ModelParser
6 | {
7 | public partial class Form1 : Form
8 | {
9 | public Form1()
10 | {
11 | this.InitializeComponent();
12 |
13 | Assembly assembly = Assembly.GetExecutingAssembly();
14 | var assemblyProduct = assembly.GetCustomAttribute() as AssemblyProductAttribute;
15 | var informationalVersion = assembly.GetCustomAttribute();
16 | var version = informationalVersion?.InformationalVersion ?? "BUG: AssemblyInformationalVersionAttribute missing!";
17 | Text = $"{assemblyProduct.Product} ({informationalVersion.InformationalVersion})";
18 | }
19 |
20 | private void Form1_Load(object sender, EventArgs e)
21 | {
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/PD2ModelParser/Sections/IAnimationController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 |
8 | namespace PD2ModelParser.Sections
9 | {
10 | public interface IAnimationController : ISection, IHashNamed
11 | {
12 | uint Flags { get; set; }
13 | float KeyframeLength { get; set; }
14 | }
15 |
16 | public interface IAnimationController : IAnimationController
17 | {
18 | IList> Keyframes { get; set; }
19 | }
20 |
21 | public class Keyframe
22 | {
23 | public float Timestamp { get; set; }
24 | public T Value { get; set; }
25 |
26 | public Keyframe(float ts, T v)
27 | {
28 | Timestamp = ts;
29 | Value = v;
30 | }
31 |
32 | public override string ToString() => $"Timestamp={Timestamp} Value={Value}";
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/PD2ModelParser/obj_data.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Numerics;
4 |
5 | using PD2ModelParser.Sections;
6 |
7 | namespace PD2ModelParser
8 | {
9 | class obj_data
10 | {
11 | public List verts { get; set; }
12 |
13 | public List uv { get; set; }
14 |
15 | public List normals { get; set; }
16 |
17 | public string object_name { get; set; }
18 |
19 | public List faces { get; set; }
20 |
21 | public string material_name { get; set; }
22 |
23 | public Dictionary> shading_groups { get; set; }
24 |
25 | public obj_data()
26 | {
27 | this.verts = new List();
28 | this.uv = new List();
29 | this.normals = new List();
30 | this.object_name = "";
31 | this.faces = new List();
32 | this.material_name = "";
33 | this.shading_groups = new Dictionary>();
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/PD2ModelParser/Sections/Unknown.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 |
4 | namespace PD2ModelParser.Sections
5 | {
6 | class Unknown : AbstractSection, ISection
7 | {
8 | UInt32 tag;
9 |
10 | public override uint TypeCode => this.tag;
11 |
12 | public UInt32 size;
13 | public byte[] data;
14 |
15 | public Unknown(BinaryReader instream, SectionHeader section)
16 | {
17 | this.SectionId = section.id;
18 | this.size = section.size;
19 |
20 | this.tag = instream.ReadUInt32();
21 |
22 | instream.BaseStream.Position = section.offset + 12;
23 |
24 | this.data = instream.ReadBytes((int)section.size);
25 | }
26 |
27 | public override void StreamWriteData(BinaryWriter outstream)
28 | {
29 | outstream.Write(this.data);
30 | }
31 |
32 | public override string ToString()
33 | {
34 | return base.ToString() + " size: " + this.size + " tag: " + this.tag + " Unknown_data: " + this.data.Length;
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/PD2ModelParser.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.9.34622.214
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PD2ModelParser", "PD2ModelParser\PD2ModelParser.csproj", "{C73A2D85-EFB5-43F3-9637-378A0C478A83}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {C73A2D85-EFB5-43F3-9637-378A0C478A83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {C73A2D85-EFB5-43F3-9637-378A0C478A83}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {C73A2D85-EFB5-43F3-9637-378A0C478A83}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {C73A2D85-EFB5-43F3-9637-378A0C478A83}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {74EE267C-4976-41E9-BFEC-478C736742C4}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/Backup/PD2ModelParser.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.30114.105
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PD2ModelParser", "PD2ModelParser\PD2ModelParser.csproj", "{C73A2D85-EFB5-43F3-9637-378A0C478A83}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {C73A2D85-EFB5-43F3-9637-378A0C478A83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {C73A2D85-EFB5-43F3-9637-378A0C478A83}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {C73A2D85-EFB5-43F3-9637-378A0C478A83}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {C73A2D85-EFB5-43F3-9637-378A0C478A83}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {74EE267C-4976-41E9-BFEC-478C736742C4}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/Research Notes/notes.txt:
--------------------------------------------------------------------------------
1 | First of all, a warning. The code in this program is messy to the extremes possible. Sorry for that.
2 |
3 | Second of all, this tool isn't complete and by no means is ready for public use.
4 |
5 | Names:
6 | Pretty much all proper names are hashed in these files. (Use hashlist.txt in 'bundle tools' folder to reverse find proper names)
7 |
8 |
9 | 3D Data:
10 | Verts, UVs, Normals, etc (every item in Geometry section) all have the same count. Also, they are all arranged by facelist. So, facelist is reused for all of those items.
11 |
12 |
13 | Object 3D section:
14 | I believe is for bones and such. Because you can create a skeleton from them. Example: https://dl.dropboxusercontent.com/u/30675690/Payday2/models/bones_bulldozer.jpeg (It seems to be a linked list)
15 |
16 |
17 | Topology/Geometry/Passthrough:
18 | Any section that uses these will link to their ID.
19 |
20 |
21 | Importing 3d verts/faces/normals/uvs:
22 | The process that is in place is probably incorrect. But it looks at each face, and tries to organize UVs and Normals to be arranged by the facelist. This way, the facelist can be used for all items in 3d data.
23 |
24 | Other sections like animations and some unknowns, I have not looked into.
--------------------------------------------------------------------------------
/PD2ModelParser/Inspector/HashNameConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using System.ComponentModel;
7 | using System.Globalization;
8 |
9 | namespace PD2ModelParser.Inspector
10 | {
11 | class HashNameConverter : ExpandableObjectConverter
12 | {
13 | public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
14 | {
15 | if(destinationType != typeof(string) || !(value is HashName hn))
16 | return base.ConvertTo(context, culture, value, destinationType);
17 |
18 | return hn.String;
19 | }
20 |
21 | public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
22 | {
23 | if (sourceType == typeof(string))
24 | return true;
25 | else
26 | return base.CanConvertFrom(context, sourceType);
27 | }
28 |
29 | public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
30 | {
31 | return new HashName(value as string);
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/PD2ModelParser/InternationalisationUtils.cs:
--------------------------------------------------------------------------------
1 | using System.Globalization;
2 |
3 | namespace PD2ModelParser
4 | {
5 | public static class InternationalisationUtils
6 | {
7 | public static float ParseFloat(this string str)
8 | {
9 | // Clean up the complete mess that was number formatting, where a modelscript
10 | // from one locale might not work in another.
11 | // Here we support ISO-8601 5.3.1.3 where numbers are to be formatted as follows:
12 | //
13 | // Numbers are separated at the thousands with spaces, and decimals are separated
14 | // either by full stops or commas (the latter being preferred). Full stops and
15 | // commas shall never be used as a thousands separator.
16 | //
17 | // We loosely convert these to a valid British formatting (by removing all the spaces
18 | // and converting the comma to a full stop) and then parse it with the invariant culture.
19 | //
20 | // This should put a stop to locale-related troubles.
21 | str = str.Trim();
22 | str = str.Replace(',', '.');
23 | str = str.Replace(" ", "");
24 | return float.Parse(str, CultureInfo.InvariantCulture);
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Research Notes/decompiled object matrix parsing.txt:
--------------------------------------------------------------------------------
1 | mat44 parent_mat = parent->mat; // v6-v7-v8-v9
2 |
3 | v10 = this->deserialized_mat[0];
4 | v22 = (
5 | ({v10[2], v10[2], v10[2], v10[2]} * parent_mat[2]) +
6 | ({v10[1], v10[1], v10[1], v10[1]} * parent_mat[1]) +
7 | ({v10[0], v10[0], v10[0], v10[0]} * parent_mat[0])
8 | );
9 | v11 = this->deserialized_mat.data[1];
10 | v21 = (
11 | ({v11[2], v11[2], v11[2], v11[2]} * parent_mat[2]) +
12 | ({v11[1], v11[1], v11[1], v11[1]} * parent_mat[1]) +
13 | ({v11[0], v11[0], v11[0], v11[0]} * parent_mat[0])
14 | );
15 | v12 = this->deserialized_mat.data[2];
16 | v20 = (
17 | ({v12[2], v12[2], v12[2], v12[2]} * parent_mat[2]) +
18 | ({v12[1], v12[1], v12[1], v12[1]} * parent_mat[1]) +
19 | ({v12[0], v12[0], v12[0], v12[0]} * parent_mat[0])
20 | );
21 | v13 = this->deserialized_mat.data[3];
22 | v19 = (
23 | ({v13[2], v13[2], v13[2], v13[2]} * parent_mat[2]) +
24 | ({v13[1], v13[1], v13[1], v13[1]} * parent_mat[1]) +
25 | ({v13[0], v13[0], v13[0], v13[0]} * parent_mat[0]) +
26 | parent_mat[3]
27 | );
28 |
29 | v22.m128_i32[3] = 0;
30 | v21.m128_i32[3] = 0;
31 | v20.m128_i32[3] = 0;
32 | v19.m128_i32[3] = 1065353216;
33 | v14 = v21;
34 | v15 = v20;
35 | v16 = v19;
36 | this->mat.data[0] = v22;
37 | this->mat.data[1] = v14;
38 | this->mat.data[2] = v15;
39 | this->mat.data[3] = v16;
40 |
--------------------------------------------------------------------------------
/PD2ModelParser/Settings.cs:
--------------------------------------------------------------------------------
1 | namespace PD2ModelParser.Properties
2 | {
3 | // This class allows you to handle specific events on the settings class:
4 | // The SettingChanging event is raised before a setting's value is changed.
5 | // The PropertyChanged event is raised after a setting's value is changed.
6 | // The SettingsLoaded event is raised after the setting values are loaded.
7 | // The SettingsSaving event is raised before the setting values are saved.
8 | internal sealed partial class Settings
9 | {
10 | public Settings()
11 | {
12 | // // To add event handlers for saving and changing settings, uncomment the lines below:
13 | //
14 | // this.SettingChanging += this.SettingChangingEventHandler;
15 | //
16 | // this.SettingsSaving += this.SettingsSavingEventHandler;
17 | //
18 | }
19 |
20 | private void SettingChangingEventHandler(object sender, System.Configuration.SettingChangingEventArgs e)
21 | {
22 | // Add code to handle the SettingChangingEvent event here.
23 | }
24 |
25 | private void SettingsSavingEventHandler(object sender, System.ComponentModel.CancelEventArgs e)
26 | {
27 | // Add code to handle the SettingsSaving event here.
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/PD2ModelParser/Sections/TopologyIP.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 |
4 | namespace PD2ModelParser.Sections
5 | {
6 | [ModelFileSection(Tags.topologyIP_tag)]
7 | class TopologyIP : AbstractSection, ISection, IPostLoadable
8 | {
9 | public UInt32 size = 0;
10 | public Topology Topology { get; set; }
11 | public byte[] remaining_data = null;
12 |
13 | public TopologyIP(uint sec_id, Topology top) : this(top)
14 | {
15 | this.SectionId = sec_id;
16 | }
17 | public TopologyIP(Topology top)
18 | {
19 | this.Topology = top;
20 | }
21 |
22 | public TopologyIP(BinaryReader br, SectionHeader sh)
23 | {
24 | this.SectionId = sh.id;
25 | this.size = sh.size;
26 | PostLoadRef(br.ReadUInt32(), i => Topology = i);
27 | this.remaining_data = null;
28 | if ((sh.offset + 12 + sh.size) > br.BaseStream.Position)
29 | this.remaining_data = br.ReadBytes((int)((sh.offset + 12 + sh.size) - br.BaseStream.Position));
30 | }
31 |
32 | public override void StreamWriteData(BinaryWriter outstream)
33 | {
34 | outstream.Write(this.Topology.SectionId);
35 |
36 | if (this.remaining_data != null)
37 | outstream.Write(this.remaining_data);
38 | }
39 |
40 | public override string ToString() => $"{base.ToString()} size: {size} Topology sectionID: {Topology.SectionId}" + (remaining_data != null ? $" REMAINING DATA! {remaining_data.Length} bytes" : "");
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/PD2ModelParser/Sections/Camera.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 |
8 | namespace PD2ModelParser.Sections
9 | {
10 | [ModelFileSection(Tags.camera_tag, ShowInInspectorRoot = false)]
11 | class Camera : Object3D, ISection
12 | {
13 | public float Unknown1 { get; set; }
14 | public float Unknown2 { get; set; }
15 | public float Unknown3 { get; set; }
16 | public float Unknown4 { get; set; }
17 | public float Unknown5 { get; set; }
18 | public float Unknown6 { get; set; }
19 |
20 | public Camera(string name, Object3D parent) : base(name, parent) { }
21 |
22 | public Camera(BinaryReader instream, SectionHeader section) : base(instream)
23 | {
24 | this.SectionId = section.id;
25 | this.size = section.size;
26 |
27 | Unknown1 = instream.ReadSingle();
28 | Unknown2 = instream.ReadSingle();
29 | Unknown3 = instream.ReadSingle();
30 | Unknown4 = instream.ReadSingle();
31 | Unknown5 = instream.ReadSingle();
32 | Unknown6 = instream.ReadSingle();
33 | }
34 |
35 | public override void StreamWriteData(BinaryWriter outstream)
36 | {
37 | base.StreamWriteData(outstream);
38 |
39 | outstream.Write(Unknown1);
40 | outstream.Write(Unknown2);
41 | outstream.Write(Unknown3);
42 | outstream.Write(Unknown4);
43 | outstream.Write(Unknown5);
44 | outstream.Write(Unknown6);
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/PD2ModelParser/Sections/SectionHeader.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 |
4 | namespace PD2ModelParser
5 | {
6 | public class SectionHeader
7 | {
8 | public UInt32 type;
9 | public UInt32 id;
10 | public UInt32 size;
11 | public long offset;
12 |
13 | ///
14 | /// Get the starting position for the contents of this section
15 | ///
16 | public long Start
17 | {
18 | get
19 | {
20 | return offset + 12; // 12 represents the three int32s that are part of the header
21 | }
22 | }
23 |
24 | ///
25 | /// Get the ending position for this section - this is one byte past the last end of this
26 | /// section, and is equal to the offset value of the next section header.
27 | ///
28 | public long End
29 | {
30 | get
31 | {
32 | return Start + size;
33 | }
34 | }
35 |
36 | public SectionHeader(uint sec_id)
37 | {
38 | this.id = sec_id;
39 | }
40 |
41 | public SectionHeader(BinaryReader instream)
42 | {
43 | this.offset = instream.BaseStream.Position;
44 | this.type = instream.ReadUInt32();
45 | this.id = instream.ReadUInt32();
46 | this.size = instream.ReadUInt32();
47 | }
48 |
49 | public override string ToString()
50 | {
51 | return "[SectionHeader] Type: " + this.type + " ID: " + this.id + " Size: " + this.size + " Offset: " + this.offset;
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/PD2ModelParser/Properties/AssemblyInfo.cs.in:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("PAYDAY 2 Model Tool")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("PAYDAY 2 Model Tool")]
13 | [assembly: AssemblyCopyright("Copyright © 2014-2018, GNU GPLv3")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // Setting ComVisible to false makes the types in this assembly not visible
18 | // to COM components. If you need to access a type in this assembly from
19 | // COM, set the ComVisible attribute to true on that type.
20 | [assembly: ComVisible(false)]
21 |
22 | // The following GUID is for the ID of the typelib if this project is exposed to COM
23 | [assembly: Guid("a47210e3-29a9-44d5-9ed7-7d15785875b3")]
24 |
25 | // Version information for an assembly consists of the following four values:
26 | //
27 | // Major Version
28 | // Minor Version
29 | // Build Number
30 | // Revision
31 | //
32 | // You can specify all the values or you can default the Build and Revision Numbers
33 | // by using the '*' as shown below:
34 | // [assembly: AssemblyVersion("1.0.*")]
35 | [assembly: AssemblyVersion("1.0.0.0")]
36 | [assembly: AssemblyFileVersion("1.0.0.0")]
37 | [assembly: AssemblyInformationalVersion("BUG: AssemblyInformationalVersion not set")]
38 |
--------------------------------------------------------------------------------
/PD2ModelParser/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/PD2ModelParser/Exporters/AnimationExporter.cs:
--------------------------------------------------------------------------------
1 | using PD2ModelParser.Misc;
2 | using PD2ModelParser.Sections;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Numerics;
7 | using System.Text;
8 | using System.Threading.Tasks;
9 |
10 | namespace PD2ModelParser.Exporters {
11 | class AnimationExporter {
12 | public static string ExportFile(FullModelData data, string path) {
13 | AnimationFile animationFile = new AnimationFile();
14 |
15 | foreach (Object3D object3D in data.SectionsOfType()) {
16 | if (object3D.Animations.Count > 0) {
17 | AnimationFileObject animationFileObject = new AnimationFileObject(object3D.HashName.String);
18 |
19 | foreach (IAnimationController animationController in object3D.Animations) {
20 | if (animationController is LinearVector3Controller) {
21 | LinearVector3Controller linearVector3Controller = (LinearVector3Controller)animationController;
22 | animationFileObject.PositionKeyframes = new List>(linearVector3Controller.Keyframes);
23 | } else if (animationController is QuatLinearRotationController) {
24 | QuatLinearRotationController quatLinearRotationController = (QuatLinearRotationController)animationController;
25 | animationFileObject.RotationKeyframes = new List>(quatLinearRotationController.Keyframes);
26 | }
27 | }
28 |
29 | animationFile.Objects.Add(animationFileObject);
30 | }
31 | }
32 |
33 | animationFile.Write(path);
34 |
35 | return path;
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/PD2ModelParser/Sections/CustomHashlist.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.IO;
3 | using System.Text;
4 |
5 | namespace PD2ModelParser.Sections
6 | {
7 | [ModelFileSection(Tags.custom_hashlist_tag)]
8 | public class CustomHashlist : AbstractSection, ISection
9 | {
10 | public HashSet Strings { get; } = new HashSet();
11 |
12 | public CustomHashlist()
13 | {
14 | }
15 |
16 | public CustomHashlist(BinaryReader br, SectionHeader sh)
17 | {
18 | ushort version = br.ReadUInt16();
19 |
20 | // The number of hash strings
21 | uint count = br.ReadUInt32();
22 |
23 | for (int i = 0; i < count; i++)
24 | {
25 | int length = br.ReadUInt16();
26 |
27 | if (br.BaseStream.Position + length > sh.End)
28 | {
29 | Log.Default.Warn("Malformed hashlist, too long");
30 | return;
31 | }
32 |
33 | byte[] bytes = br.ReadBytes(length);
34 | string str = Encoding.UTF8.GetString(bytes);
35 | StaticStorage.hashindex.Hint(str);
36 | }
37 | }
38 |
39 | public override void StreamWriteData(BinaryWriter output)
40 | {
41 | output.Write((ushort) 1);
42 | output.Write((uint) Strings.Count);
43 |
44 | foreach (string s in Strings)
45 | {
46 | byte[] bytes = Encoding.UTF8.GetBytes(s);
47 | output.Write((ushort) bytes.Length);
48 | output.Write(bytes);
49 | }
50 | }
51 |
52 | public void Hint(HashName hashname)
53 | {
54 | if (!hashname.Known)
55 | return;
56 |
57 | Strings.Add(hashname.String);
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | version: '{branch}.{build}'
2 | image: Visual Studio 2022
3 | configuration: Debug
4 | assembly_info:
5 | patch: true
6 | file: '**\AssemblyInfo.*'
7 | assembly_version: '{version}'
8 | assembly_file_version: '{version}'
9 | assembly_informational_version: $(INFORMATIONAL_VERSION)
10 | install:
11 | - ps: >-
12 | $gitdescribe = git describe
13 |
14 | $env:ASSEMBLY_VERSION_NUMBER = if ($gitdescribe -match "v(\d+(?:\.\d+){0,2})") {
15 | $Matches.1 + "." + $env:APPVEYOR_BUILD_NUMBER.ToString()
16 | }
17 |
18 | else {
19 | "0.0.0" + $env:APPVEYOR_BUILD_NUMBER.ToString()
20 | }
21 |
22 | Update-AppveyorBuild -Version $env:ASSEMBLY_VERSION_NUMBER
23 |
24 | Set-AppveyorBuildVariable -Name INFORMATIONAL_VERSION -Value ("{0}-{1}" -f $gitdescribe,$env:CONFIGURATION)
25 |
26 | git submodule -q init
27 |
28 | git submodule -q update --remote
29 |
30 |
31 | $env:OUTPUT_FILENAME = if ($env:APPVEYOR_REPO_TAG -eq "true") {
32 | "pd2modelparser-$($env:APPVEYOR_REPO_TAG_NAME)-$($env:CONFIGURATION)"
33 | }
34 |
35 | else {
36 | "pd2modelparser-{0}-{1}" -f $gitdescribe,$env:CONFIGURATION
37 | }
38 |
39 | $env:OUTPUT_FILENAME += ".zip"
40 | before_build:
41 | - cmd: >-
42 | nuget restore
43 |
44 | copy .\PD2ModelParser\Properties\AssemblyInfo.cs.in .\PD2ModelParser\Properties\AssemblyInfo.cs
45 | build:
46 | project: PD2ModelParser.sln
47 | verbosity: minimal
48 | after_build:
49 | - ps: >-
50 | 7z a "$env:OUTPUT_FILENAME" "$env:APPVEYOR_BUILD_FOLDER\PD2ModelParser\bin\$env:CONFIGURATION"
51 |
52 | Push-AppveyorArtifact "$env:OUTPUT_FILENAME" -Filename $env:OUTPUT_FILENAME
53 | deploy:
54 | - provider: GitHub
55 | auth_token:
56 | secure: L4MkfzcxXQWZBVDlHK3Y7G+FHUWDk0fcnVwDJ6n/cvCmRqDku818Ut4EVeWPmoZj
57 | artifact: $(OUTPUT_FILENAME)
58 | draft: true
59 | on:
60 | APPVEYOR_REPO_TAG: true
61 |
--------------------------------------------------------------------------------
/PD2ModelParser/Sections/LookAtConstrRotationController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 |
8 | namespace PD2ModelParser.Sections
9 | {
10 | [ModelFileSection(Tags.lookAtConstrRotationController)]
11 | class LookAtConstrRotationController : AbstractSection, ISection, IPostLoadable, IHashNamed
12 | {
13 | public HashName HashName { get; set; }
14 | public uint Unknown1 { get; set; }
15 |
16 | [System.ComponentModel.TypeConverter(typeof(Inspector.Object3DReferenceConverter))]
17 | public ISection Unknown2 { get; set; }
18 |
19 | [System.ComponentModel.TypeConverter(typeof(Inspector.Object3DReferenceConverter))]
20 | public ISection Unknown3 { get; set; }
21 |
22 | [System.ComponentModel.TypeConverter(typeof(Inspector.Object3DReferenceConverter))]
23 | public ISection Unknown4 { get; set; }
24 |
25 | public LookAtConstrRotationController() { }
26 |
27 | public LookAtConstrRotationController(BinaryReader br, SectionHeader sh)
28 | {
29 | this.SectionId = sh.id;
30 |
31 | HashName = new HashName(br.ReadUInt64());
32 | Unknown1 = br.ReadUInt32();
33 | PostLoadRef(br.ReadUInt32(), s => Unknown2 = s);
34 | PostLoadRef(br.ReadUInt32(), s => Unknown3 = s);
35 | PostLoadRef(br.ReadUInt32(), s => Unknown4 = s);
36 | }
37 |
38 | public override void StreamWriteData(BinaryWriter output)
39 | {
40 | output.Write(HashName.Hash);
41 | output.Write(Unknown1);
42 | output.Write(Unknown2?.SectionId ?? 0);
43 | output.Write(Unknown3?.SectionId ?? 0);
44 | output.Write(Unknown4?.SectionId ?? 0);
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/PD2ModelParser/Misc/ZLib/Adler32.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace PD2ModelParser.Misc.ZLib
8 | {
9 | public class Adler32
10 | {
11 | #region "Variables globales"
12 | private UInt32 a = 1;
13 | private UInt32 b = 0;
14 | private const int _base = 65521;
15 | private const int _nmax = 5550;
16 | private int pend = 0;
17 | #endregion
18 | #region "Metodos publicos"
19 | public void Update(byte data)
20 | {
21 | if (pend >= _nmax) updateModulus();
22 | a += data;
23 | b += a;
24 | pend++;
25 | }
26 | public void Update(byte[] data)
27 | {
28 | Update(data, 0, data.Length);
29 | }
30 | public void Update(byte[] data, int offset, int length)
31 | {
32 | int nextJToComputeModulus = _nmax - pend;
33 | for (int j = 0; j < length; j++)
34 | {
35 | if (j == nextJToComputeModulus)
36 | {
37 | updateModulus();
38 | nextJToComputeModulus = j + _nmax;
39 | }
40 | unchecked
41 | {
42 | a += data[j + offset];
43 | }
44 | b += a;
45 | pend++;
46 | }
47 | }
48 | public void Reset()
49 | {
50 | a = 1;
51 | b = 0;
52 | pend = 0;
53 | }
54 | private void updateModulus()
55 | {
56 | a %= _base;
57 | b %= _base;
58 | pend = 0;
59 | }
60 | public UInt32 GetValue()
61 | {
62 | if (pend > 0) updateModulus();
63 | return (b << 16) | a;
64 | }
65 | #endregion
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/PD2ModelParser/Sections/PassthroughGP.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.ComponentModel;
4 |
5 | namespace PD2ModelParser.Sections
6 | {
7 | [ModelFileSection(Tags.passthroughGP_tag)]
8 | [TypeConverter(typeof(ExpandableObjectConverter))]
9 | class PassthroughGP : AbstractSection, ISection, IPostLoadable
10 | {
11 | public UInt32 size = 8;
12 | [Category("PassthroughGP")]
13 | public Geometry Geometry { get; set; }
14 | [Category("PassthroughGP")]
15 | public Topology Topology { get; set; }
16 | public byte[] remaining_data = null;
17 |
18 | public PassthroughGP(Geometry geom, Topology topo)
19 | {
20 | this.Geometry = geom;
21 | this.Topology = topo;
22 | }
23 |
24 | public PassthroughGP(BinaryReader instream, SectionHeader section)
25 | {
26 | this.SectionId = section.id;
27 | this.size = section.size;
28 | PostLoadRef(instream.ReadUInt32(), i => this.Geometry = i);
29 | PostLoadRef(instream.ReadUInt32(), i => this.Topology = i);
30 | this.remaining_data = null;
31 | if ((section.offset + 12 + section.size) > instream.BaseStream.Position)
32 | this.remaining_data = instream.ReadBytes((int)((section.offset + 12 + section.size) - instream.BaseStream.Position));
33 | }
34 |
35 | public override void StreamWriteData(BinaryWriter outstream)
36 | {
37 | outstream.Write(this.Geometry.SectionId);
38 | outstream.Write(this.Topology.SectionId);
39 | }
40 |
41 | public override string ToString()
42 | {
43 | return $"{base.ToString()} size: {this.size} geometry_section: {this.Geometry.SectionId} topology_section: {this.Topology.SectionId}" + (this.remaining_data != null ? $" REMAINING DATA! {this.remaining_data.Length} bytes" : "");
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/PD2ModelParser/PD2ModelParser.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net6.0-windows
6 | false
7 | true
8 | true
9 | true
10 | true
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | CD /D $(MSBuildProjectDirectory)\..\ & bash.exe "gen-version.sh" "$(ConfigurationName)"
32 | "$(SolutionDir)gen-version.sh" "$(ConfigurationName)"
33 |
34 |
35 | x64
36 |
37 |
38 |
--------------------------------------------------------------------------------
/PD2ModelParser/Sections/Author.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 |
4 | namespace PD2ModelParser.Sections
5 | {
6 | [ModelFileSection(Tags.author_tag)]
7 | class Author : AbstractSection, ISection, IHashNamed
8 | {
9 | public UInt32 size;
10 |
11 | public HashName HashName { get; set; }
12 | public String email; //Author's email address
13 | public String source_file; //Source model file
14 | public UInt32 unknown2;
15 |
16 | public byte[] remaining_data = null;
17 |
18 | public Author(BinaryReader instream, SectionHeader section)
19 | {
20 | this.SectionId = section.id;
21 | this.size = section.size;
22 | this.HashName = new HashName(instream.ReadUInt64());
23 |
24 | this.email = instream.ReadCString();
25 | this.source_file = instream.ReadCString();
26 |
27 | this.unknown2 = instream.ReadUInt32();
28 |
29 | this.remaining_data = null;
30 | if ((section.offset + 12 + section.size) > instream.BaseStream.Position)
31 | this.remaining_data = instream.ReadBytes((int)((section.offset + 12 + section.size) - instream.BaseStream.Position));
32 | }
33 |
34 | public override void StreamWriteData(BinaryWriter outstream)
35 | {
36 | Byte zero = 0;
37 | outstream.Write(this.HashName.Hash);
38 | outstream.Write(this.email.ToCharArray());
39 | outstream.Write(zero);
40 | outstream.Write(this.source_file.ToCharArray());
41 | outstream.Write(zero);
42 | outstream.Write(this.unknown2);
43 |
44 | if (this.remaining_data != null)
45 | outstream.Write(this.remaining_data);
46 | }
47 |
48 | public override string ToString()
49 | {
50 | return $"{base.ToString()} size: {this.size} HashName: {this.HashName} email: {this.email} Source file: {this.source_file} unknown2: {this.unknown2}{(this.remaining_data != null ? " REMAINING DATA! " + this.remaining_data.Length + " bytes" : "")}";
51 | }
52 |
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/PD2ModelParser/Misc/BulkFunctions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 |
6 | namespace PD2ModelParser
7 | {
8 | static class BulkFunctions
9 | {
10 | public static IEnumerable WalkDirectoryTreeDepth(DirectoryInfo dir, string filepattern)
11 | {
12 | foreach (var i in dir.EnumerateFiles(filepattern))
13 | {
14 | if (i.Attributes.HasFlag(FileAttributes.Directory)) continue;
15 | yield return i;
16 | }
17 |
18 | foreach (var i in dir.EnumerateDirectories())
19 | {
20 | foreach (var f in WalkDirectoryTreeDepth(i, filepattern))
21 | {
22 | yield return f;
23 | }
24 | }
25 | }
26 |
27 | public static IEnumerable<(string fullpath, string relativepath, FullModelData data)> EveryModel(string root)
28 | {
29 | foreach (var i in WalkDirectoryTreeDepth(new DirectoryInfo(root), "*.model"))
30 | {
31 | FullModelData fmd = null;
32 | try
33 | {
34 | fmd = ModelReader.Open(i.FullName);
35 | }
36 | catch(Exception e)
37 | {
38 | Log.Default.Warn($"Unable to read {i.FullName}: {e}");
39 | }
40 | if (fmd != null)
41 | {
42 | yield return (i.FullName, i.FullName.Substring(root.Length), fmd);
43 | }
44 | }
45 | }
46 |
47 | public static void WriteSimpleCsvLike(TextWriter tw, IEnumerable items)
48 | {
49 | var fields = typeof(T).GetFields().OrderBy(i => i.MetadataToken).ToList();
50 | tw.WriteLine(string.Join(",", fields.Select(i => i.Name)));
51 | var count = 1000;
52 | var inc = 0;
53 | foreach (var i in items)
54 | {
55 | var values = fields.Select(field => field.GetValue(i));
56 | tw.WriteLine(string.Join(",", values));
57 | if (count-- == 0)
58 | {
59 | count = 1000;
60 | tw.Flush();
61 | Log.Default.Status($"{++inc}");
62 | }
63 | }
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/PD2ModelParser/Sections/LinearFloatController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Text;
6 |
7 | namespace PD2ModelParser.Sections
8 | {
9 | [ModelFileSection(Tags.linearFloatController_tag)]
10 | class LinearFloatController : AbstractSection, ISection, IHashNamed, IAnimationController
11 | {
12 | public HashName HashName { get; set; }
13 | public uint Flags { get; set; }
14 | public byte Flag0 { get => (byte)((Flags & 0x000000FF) >> 0); set => Flags = Flags & (uint)((value << 0) | 0xFFFFFF00); }
15 | public byte Flag1 { get => (byte)((Flags & 0x0000FF00) >> 8); set => Flags = Flags & (uint)((value << 8) | 0xFFFF00FF); }
16 | public byte Flag2 { get => (byte)((Flags & 0x00FF0000) >> 16); set => Flags = Flags & (uint)((value << 16) | 0xFF00FFFF); }
17 | public byte Flag3 { get => (byte)((Flags & 0xFF000000) >> 24); set => Flags = Flags & (uint)((value << 24) | 0x00FFFFFF); }
18 | public uint Unknown2 { get; set; }
19 | public float KeyframeLength { get; set; }
20 | public IList> Keyframes { get; set; } = new List>();
21 |
22 | public LinearFloatController(string name = null) => HashName = new HashName(name ?? "");
23 |
24 | public LinearFloatController(System.IO.BinaryReader instream, SectionHeader section)
25 | {
26 | SectionId = section.id;
27 | HashName = new HashName(instream.ReadUInt64());
28 | Flags = instream.ReadUInt32();
29 | Unknown2 = instream.ReadUInt32();
30 | KeyframeLength = instream.ReadSingle();
31 | var count = instream.ReadUInt32();
32 | for (var i = 0; i < count; i++) {
33 | Keyframes.Add(new Keyframe(instream.ReadSingle(), instream.ReadSingle()));
34 | }
35 | }
36 |
37 | public override void StreamWriteData(BinaryWriter output)
38 | {
39 | output.Write(HashName.Hash);
40 | output.Write(Flags);
41 | output.Write(Unknown2);
42 | output.Write(KeyframeLength);
43 | output.Write(Keyframes.Count);
44 | foreach(var i in Keyframes)
45 | {
46 | output.Write(i.Timestamp);
47 | output.Write(i.Value);
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/PD2ModelParser/Sections/Animation.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 |
5 | namespace PD2ModelParser.Sections
6 | {
7 | [ModelFileSection(Tags.animation_data_tag)]
8 | public class Animation : AbstractSection, ISection, IHashNamed
9 | {
10 | public UInt32 size;
11 |
12 | public HashName HashName { get; set; }
13 | public UInt32 unknown2 { get; set; }
14 | public float keyframe_length { get; set; }
15 | public UInt32 count { get; set; }
16 | public List items { get; set; } = new List();
17 |
18 | public byte[] remaining_data { get; set; } = null;
19 |
20 | public Animation(BinaryReader instream, SectionHeader section)
21 | {
22 | this.SectionId = section.id;
23 | this.size = section.size;
24 | this.HashName = new HashName(instream.ReadUInt64());
25 | this.unknown2 = instream.ReadUInt32();
26 | this.keyframe_length = instream.ReadSingle();
27 | this.count = instream.ReadUInt32();
28 |
29 | List items = new List();
30 | for (int x = 0; x < this.count; x++)
31 | this.items.Add(instream.ReadSingle());
32 |
33 | this.remaining_data = null;
34 | if ((section.offset + 12 + section.size) > instream.BaseStream.Position)
35 | this.remaining_data = instream.ReadBytes((int)((section.offset + 12 + section.size) - instream.BaseStream.Position));
36 | }
37 |
38 | public override void StreamWriteData(BinaryWriter outstream)
39 | {
40 | outstream.Write(this.HashName.Hash);
41 | outstream.Write(this.unknown2);
42 | outstream.Write(this.keyframe_length);
43 | outstream.Write(this.count);
44 | foreach (float item in this.items)
45 | {
46 | outstream.Write(item);
47 | }
48 |
49 | if (this.remaining_data != null)
50 | outstream.Write(this.remaining_data);
51 | }
52 |
53 | public override string ToString()
54 | {
55 | return $"{base.ToString()} size: {this.size} Name: {this.HashName} unknown2: {this.unknown2} keyframe_length: {this.keyframe_length} count: {this.count} items: (count={this.items.Count}){(remaining_data != null ? " REMAINING DATA! " + remaining_data.Length + " bytes" : "")}";
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Research Notes/decompiled matrix parsing.txt:
--------------------------------------------------------------------------------
1 | premul_items = this->bone_premul_mvec[i];
2 |
3 | object = objects_begin[i];
4 | obj_matrix = object->mat;
5 |
6 | float4 v14 = premul_items[0];
7 | float4 column0 = (
8 | {v14[2], v14[2], v14[2], v14[2]} * obj_matrix[2] +
9 | {v14[1], v14[1], v14[1], v14[1]} * obj_matrix[1] +
10 | {v14[0], v14[0], v14[0], v14[0]} * obj_matrix[0]
11 | );
12 |
13 | float4 v15 = premul_items[1];
14 | float4 column1 = (
15 | {v15[2], v15[2], v15[2], v15[2]} * obj_matrix[2] +
16 | {v15[1], v15[1], v15[1], v15[1]} * obj_matrix[1] +
17 | {v15[0], v15[0], v15[0], v15[0]} * obj_matrix[0]
18 | );
19 |
20 | float4 v16 = premul_items[2];
21 | float4 column2 = (
22 | ({v16[2], v16[2], v16[2], v16[2]} * obj_matrix[2]) +
23 | ({v16[1], v16[1], v16[1], v16[1]} * obj_matrix[1]) +
24 | ({v16[0], v16[0], v16[0], v16[0]} * obj_matrix[0])
25 | );
26 |
27 | float4 v17 = premul_items[3];
28 | float4 column3 = (
29 | ({v17[2], v17[2], v17[2], v17[2]} * obj_matrix[2]) +
30 | ({v17[2], v17[2], v17[2], v17[2]} * obj_matrix[1]) +
31 | ({v17[2], v17[2], v17[2], v17[2]} * obj_matrix[0]) +
32 | obj_matrix[3] /* Translation */
33 | );
34 |
35 | column0[3] = 0;
36 | column1[3] = 0;
37 | column2[3] = 0;
38 | column3[3] = 1065353216;
39 |
40 | float4 nextmult = {
41 | this->tail_matrix[0],
42 | this->tail_matrix[1],
43 | this->tail_matrix[2],
44 | {column3[2], column3[2], column3[2], column3[2]} * this->tail_matrix[2] + this->tail_matrix[3]
45 | };
46 |
47 | float4 result0 = (
48 | ({column0[2], column0[2], column0[2], column0[2]} * nextmult[2]) +
49 | ({column0[1], column0[1], column0[1], column0[1]} * nextmult[1]) +
50 | ({column0[0], column0[0], column0[0], column0[0]} * nextmult[0])
51 | );
52 | float4 result1 = (
53 | ({column1[2], column1[2], column1[2], column1[2]} * nextmult[2]) +
54 | ({column1[1], column1[1], column1[1], column1[1]} * nextmult[1]) +
55 | ({column1[0], column1[0], column1[0], column1[0]} * nextmult[0])
56 | );
57 | float4 result2 = (
58 | ({column2[2], column2[2], column2[2], column2[2]} * nextmult[2]) +
59 | ({column2[1], column2[1], column2[1], column2[1]} * nextmult[1]) +
60 | ({column2[0], column2[0], column2[0], column2[0]} * nextmult[0])
61 | );
62 | float4 result3 = (
63 | ({column3[1], column3[1], column3[1], column3[1]} * nextmult[1]) +
64 | ({column3[0], column3[0], column3[0], column3[0]} * nextmult[0]) +
65 | nextmult[3]
66 | );
67 |
68 | result0[3] = 0;
69 | result1[3] = 0;
70 | result2[3] = 0;
71 | result3[3] = 1065353216;
72 |
73 | this->matrix_vector[i] = {
74 | result0,
75 | result1,
76 | result2,
77 | result3
78 | };
79 |
80 | ++i;
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/PD2ModelParser/Properties/Resources.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace PD2ModelParser.Properties {
12 | using System;
13 |
14 |
15 | ///
16 | /// A strongly-typed resource class, for looking up localized strings, etc.
17 | ///
18 | // This class was auto-generated by the StronglyTypedResourceBuilder
19 | // class via a tool like ResGen or Visual Studio.
20 | // To add or remove a member, edit your .ResX file then rerun ResGen
21 | // with the /str option, or rebuild your VS project.
22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
25 | internal class Resources {
26 |
27 | private static global::System.Resources.ResourceManager resourceMan;
28 |
29 | private static global::System.Globalization.CultureInfo resourceCulture;
30 |
31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
32 | internal Resources() {
33 | }
34 |
35 | ///
36 | /// Returns the cached ResourceManager instance used by this class.
37 | ///
38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
39 | internal static global::System.Resources.ResourceManager ResourceManager {
40 | get {
41 | if (object.ReferenceEquals(resourceMan, null)) {
42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PD2ModelParser.Properties.Resources", typeof(Resources).Assembly);
43 | resourceMan = temp;
44 | }
45 | return resourceMan;
46 | }
47 | }
48 |
49 | ///
50 | /// Overrides the current thread's CurrentUICulture property for all
51 | /// resource lookups using this strongly typed resource class.
52 | ///
53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
54 | internal static global::System.Globalization.CultureInfo Culture {
55 | get {
56 | return resourceCulture;
57 | }
58 | set {
59 | resourceCulture = value;
60 | }
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/PD2ModelParser/KnownIndex.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Text.RegularExpressions;
6 | using PD2ModelParser;
7 |
8 | namespace PD2Bundle
9 | {
10 | public class KnownIndex
11 | {
12 | private Dictionary hashes = new Dictionary();
13 |
14 | public string GetString(ulong hash)
15 | {
16 | if (hashes.ContainsKey(hash))
17 | {
18 | return hashes[hash];
19 | }
20 | return Convert.ToString(hash);
21 | }
22 |
23 | public bool Contains(ulong hash)
24 | {
25 | return hashes.ContainsKey(hash);
26 | }
27 |
28 | private void CheckCollision(Dictionary item, ulong hash, string value)
29 | {
30 | if ( item.ContainsKey(hash) && (item[hash] != value) )
31 | {
32 | Log.Default.Warn("Hash collision: {0:x} : {1} == {2}", hash, item[hash], value);
33 | }
34 | }
35 |
36 | public void Clear()
37 | {
38 | this.hashes.Clear();
39 | loaded = false;
40 | }
41 |
42 | bool loaded = false;
43 |
44 | public bool Load()
45 | {
46 | if (loaded) return true;
47 |
48 | foreach(var name in GetHashfileNames())
49 | {
50 | loaded |= TryLoad(name);
51 | }
52 |
53 | return loaded;
54 | }
55 |
56 | public bool TryLoad(string filename)
57 | {
58 | try
59 | {
60 | using (var sr = new StreamReader(filename))
61 | {
62 | string line = sr.ReadLine();
63 | while (line != null)
64 | {
65 | Hint(line);
66 | line = sr.ReadLine();
67 | }
68 | }
69 | return true;
70 | }
71 | catch (Exception e)
72 | {
73 | Log.Default.Warn("Couldn't read hashlist file \"{0}\": {1}", filename, e.Message);
74 | return false;
75 | }
76 | }
77 |
78 | private IEnumerable GetHashfileNames()
79 | {
80 | var exepath = System.Reflection.Assembly.GetEntryAssembly().Location;
81 | var exedir = Path.GetDirectoryName(exepath);
82 | var cwd = Directory.GetCurrentDirectory();
83 |
84 | var hashregex = new Regex(@"hash(list|es)(-\d+)?(\.txt)?", RegexOptions.IgnoreCase);
85 | var names = Directory.GetFiles(cwd).Where(i=>hashregex.IsMatch(i));
86 | if(exedir != cwd)
87 | {
88 | names = names.Concat(Directory.GetFiles(exedir).Where(i => hashregex.IsMatch(i)));
89 | }
90 | return names;
91 | }
92 |
93 | public void Hint(string line)
94 | {
95 | ulong hash = Hash64.HashString(line);
96 | CheckCollision(hashes, hash, line);
97 | hashes[hash] = line;
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/Research Notes/decompiled matrix parsing 2.txt:
--------------------------------------------------------------------------------
1 | premul_items = this->bone_premul_mvec[i];
2 |
3 | object = objects_begin[i];
4 | obj_matrix = object->mat;
5 |
6 | float4 v14 = premul_items[0];
7 | float4 column0 = {
8 | v14[0] * obj_matrix[0] + v14[1] * obj_matrix[1] + v14[2] * obj_matrix[2],
9 | v14[0] * obj_matrix[0] + v14[1] * obj_matrix[1] + v14[2] * obj_matrix[2],
10 | v14[0] * obj_matrix[0] + v14[1] * obj_matrix[1] + v14[2] * obj_matrix[2],
11 | v14[0] * obj_matrix[0] + v14[1] * obj_matrix[1] + v14[2] * obj_matrix[2]
12 | };
13 |
14 | float4 v15 = premul_items[1];
15 | float4 column1 = {
16 | v15[0] * obj_matrix[0], v15[1] * obj_matrix[1], v15[2] * obj_matrix[2],
17 | v15[0] * obj_matrix[0], v15[1] * obj_matrix[1], v15[2] * obj_matrix[2],
18 | v15[0] * obj_matrix[0], v15[1] * obj_matrix[1], v15[2] * obj_matrix[2],
19 | v15[0] * obj_matrix[0], v15[1] * obj_matrix[1], v15[2] * obj_matrix[2]
20 | };
21 |
22 | float4 v16 = premul_items[2];
23 | float4 column2 = {
24 | v16[0] * obj_matrix[0] + v16[1] * obj_matrix[1] + v16[2] * obj_matrix[2],
25 | v16[0] * obj_matrix[0] + v16[1] * obj_matrix[1] + v16[2] * obj_matrix[2],
26 | v16[0] * obj_matrix[0] + v16[1] * obj_matrix[1] + v16[2] * obj_matrix[2],
27 | v16[0] * obj_matrix[0] + v16[1] * obj_matrix[1] + v16[2] * obj_matrix[2]
28 | };
29 |
30 | float4 v17 = premul_items[3];
31 | float4 column3 = {
32 | v17[2] * obj_matrix[0][0] + v17[2] * obj_matrix[1][0] + v17[2] * obj_matrix[2][0] + obj_matrix[3][0],
33 | v17[2] * obj_matrix[0][1] + v17[2] * obj_matrix[1][1] + v17[2] * obj_matrix[2][1] + obj_matrix[3][1],
34 | v17[2] * obj_matrix[0][2] + v17[2] * obj_matrix[1][2] + v17[2] * obj_matrix[2][2] + obj_matrix[3][2],
35 | v17[2] * obj_matrix[0][3] + v17[2] * obj_matrix[1][3] + v17[2] * obj_matrix[2][3] + obj_matrix[3][3]
36 | }
37 |
38 | column0[3] = 0;
39 | column1[3] = 0;
40 | column2[3] = 0;
41 | column3[3] = 1065353216;
42 |
43 | float4 nextmult = {
44 | this->tail_matrix[0],
45 | this->tail_matrix[1],
46 | this->tail_matrix[2],
47 | {column3[2], column3[2], column3[2], column3[2]} * this->tail_matrix[2] + this->tail_matrix[3]
48 | };
49 |
50 | float4 result0 = (
51 | ({column0[2], column0[2], column0[2], column0[2]} * nextmult[2]) +
52 | ({column0[1], column0[1], column0[1], column0[1]} * nextmult[1]) +
53 | ({column0[0], column0[0], column0[0], column0[0]} * nextmult[0])
54 | );
55 | float4 result1 = (
56 | ({column1[2], column1[2], column1[2], column1[2]} * nextmult[2]) +
57 | ({column1[1], column1[1], column1[1], column1[1]} * nextmult[1]) +
58 | ({column1[0], column1[0], column1[0], column1[0]} * nextmult[0])
59 | );
60 | float4 result2 = (
61 | ({column2[2], column2[2], column2[2], column2[2]} * nextmult[2]) +
62 | ({column2[1], column2[1], column2[1], column2[1]} * nextmult[1]) +
63 | ({column2[0], column2[0], column2[0], column2[0]} * nextmult[0])
64 | );
65 | float4 result3 = (
66 | ({column3[1], column3[1], column3[1], column3[1]} * nextmult[1]) +
67 | ({column3[0], column3[0], column3[0], column3[0]} * nextmult[0]) +
68 | nextmult[3]
69 | );
70 |
71 | result0[3] = 0;
72 | result1[3] = 0;
73 | result2[3] = 0;
74 | result3[3] = 1065353216;
75 |
76 | this->matrix_vector[i] = {
77 | result0,
78 | result1,
79 | result2,
80 | result3
81 | };
82 |
83 | ++i;
84 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/PD2ModelParser/Sections/Material.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 |
5 | namespace PD2ModelParser.Sections
6 | {
7 | public class MaterialItem
8 | {
9 | public UInt32 unknown1;
10 | public UInt32 unknown2;
11 |
12 | public override string ToString()
13 | {
14 | return "unknown1: " + this.unknown1 + " unknown2: " + this.unknown2;
15 | }
16 | }
17 |
18 | [ModelFileSection(Tags.material_tag)]
19 | class Material : AbstractSection, ISection, IHashNamed
20 | {
21 | public UInt32 size;
22 |
23 | public HashName HashName { get; set; } //Hashed material name (see hashlist.txt)
24 | public byte[] skipped;
25 | public uint count;
26 | public List items = new List();
27 |
28 | public byte[] remaining_data = null;
29 |
30 | public Material(string mat_name)
31 | {
32 |
33 | this.size = 0;
34 | this.HashName = new HashName(mat_name);
35 | this.skipped = new byte[48];
36 | this.count = 0;
37 | }
38 |
39 | public Material(BinaryReader instream, SectionHeader section)
40 | {
41 | this.SectionId = section.id;
42 | this.size = section.size;
43 |
44 | this.HashName = new HashName(instream.ReadUInt64());
45 | this.skipped = instream.ReadBytes(48);
46 | this.count = instream.ReadUInt32();
47 |
48 | for (int x = 0; x < this.count; x++)
49 | {
50 | MaterialItem item = new MaterialItem();
51 | item.unknown1 = instream.ReadUInt32();
52 | item.unknown2 = instream.ReadUInt32();
53 | this.items.Add(item);
54 | }
55 |
56 | this.remaining_data = null;
57 | if ((section.offset + 12 + section.size) > instream.BaseStream.Position)
58 | this.remaining_data =
59 | instream.ReadBytes((int) ((section.offset + 12 + section.size) - instream.BaseStream.Position));
60 | }
61 |
62 | public override void StreamWriteData(BinaryWriter outstream)
63 | {
64 | outstream.Write(this.HashName.Hash);
65 | outstream.Write(this.skipped);
66 | outstream.Write(this.count);
67 | foreach (MaterialItem item in this.items)
68 | {
69 | outstream.Write(item.unknown1);
70 | outstream.Write(item.unknown2);
71 | }
72 |
73 | if (this.remaining_data != null)
74 | outstream.Write(this.remaining_data);
75 | }
76 |
77 | public override string ToString()
78 | {
79 | string items_string = (this.items.Count == 0 ? "none" : "");
80 |
81 | foreach (MaterialItem item in this.items)
82 | {
83 | items_string += item + ", ";
84 | }
85 |
86 | return base.ToString() +
87 | $" size: {this.size} HashName: {this.HashName} count: {this.count} items: [ {items_string} ] " +
88 | (this.remaining_data != null ? " REMAINING DATA! " + this.remaining_data.Length + " bytes" : "");
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/PD2ModelParser/Sections/MaterialGroup.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 |
6 | namespace PD2ModelParser.Sections
7 | {
8 | [ModelFileSection(Tags.material_group_tag)]
9 | class MaterialGroup : AbstractSection, ISection, IPostLoadable
10 | {
11 | private UInt32 size = 0;
12 | private List itemIds = new List();
13 |
14 | public UInt32 Count => (uint)Items.Count;
15 | public List Items { get; set; } = new List();
16 | public byte[] remaining_data = null;
17 |
18 | public MaterialGroup(Material mat)
19 | {
20 | this.Items.Add(mat);
21 | }
22 |
23 | public MaterialGroup(IEnumerable mats)
24 | {
25 | this.Items = mats.ToList();
26 | }
27 |
28 | public MaterialGroup(BinaryReader instream, SectionHeader section)
29 | {
30 | this.SectionId = section.id;
31 | this.size = section.size;
32 |
33 | var count = instream.ReadUInt32();
34 | for (int x = 0; x < count; x++)
35 | {
36 | this.itemIds.Add(instream.ReadUInt32());
37 | }
38 | byte[] remaining_data = null;
39 | if ((section.offset + 12 + section.size) > instream.BaseStream.Position)
40 | {
41 | remaining_data = instream.ReadBytes((int)((section.offset + 12 + section.size) - instream.BaseStream.Position));
42 | Log.Default.Info($"Read a {nameof(MaterialGroup)} with remaining data of size {remaining_data.Length}");
43 | }
44 | }
45 |
46 | public override void PostLoad(uint id, Dictionary sections)
47 | {
48 | base.PostLoad(id, sections);
49 | foreach(var itemid in itemIds)
50 | {
51 | if(sections.TryGetValue(itemid, out var value)) {
52 | if(value is Material mat)
53 | {
54 | this.Items.Add(mat);
55 | }
56 | else { throw new Exception($"Couldn't load {nameof(MaterialGroup)} {this.SectionId}: Section {value.SectionId} is not a Material"); }
57 | }
58 | else { throw new Exception($"Couldn't load {nameof(MaterialGroup)} {this.SectionId}: Section {itemid} doesn't exist!"); }
59 | }
60 | itemIds = null;
61 | }
62 |
63 | public override void StreamWriteData(BinaryWriter outstream)
64 | {
65 | outstream.Write(this.Count);
66 | foreach (var item in this.Items)
67 | outstream.Write(item.SectionId);
68 |
69 | if (this.remaining_data != null)
70 | outstream.Write(this.remaining_data);
71 | }
72 |
73 | public override string ToString()
74 | {
75 | string items_string = (this.Items.Count == 0 ? "none" : "");
76 |
77 | items_string += string.Join(", ", this.Items.Select(i => i.SectionId));
78 |
79 | return base.ToString() + " size: " + this.size + " Count: " + this.Count + " Items: [ " + items_string + " ] " + (this.remaining_data != null ? " REMAINING DATA! " + this.remaining_data.Length + " bytes" : "");
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/PD2ModelParser/Misc/SerializeUtils.cs:
--------------------------------------------------------------------------------
1 | namespace PD2ModelParser
2 | {
3 | static class SerializeUtils
4 | {
5 | static public System.Numerics.Vector3 ReadVector3(this System.IO.BinaryReader self)
6 | => new System.Numerics.Vector3(self.ReadSingle(), self.ReadSingle(), self.ReadSingle());
7 |
8 | static public System.Numerics.Quaternion ReadQuaternion(this System.IO.BinaryReader self)
9 | => new System.Numerics.Quaternion(self.ReadSingle(), self.ReadSingle(), self.ReadSingle(), self.ReadSingle());
10 |
11 | static public void Write(this System.IO.BinaryWriter self, System.Numerics.Vector3 vec)
12 | {
13 | self.Write(vec.X);
14 | self.Write(vec.Y);
15 | self.Write(vec.Z);
16 | }
17 |
18 | //TODO: What encoding are these? This only actually works for ASCII-7bit
19 | static public string ReadCString(this System.IO.BinaryReader self)
20 | {
21 | var sb = new System.Text.StringBuilder();
22 | int buf;
23 | while ((buf = self.ReadByte()) != 0)
24 | sb.Append((char)buf);
25 | return sb.ToString();
26 | }
27 |
28 | public static System.Numerics.Matrix4x4 ReadMatrix(this System.IO.BinaryReader instream)
29 | {
30 | System.Numerics.Matrix4x4 m;
31 |
32 | // Yes, the matricies appear to be written top-down in colums, this isn't the field names being wrong
33 | // This is how a multidimensional array is layed out in memory.
34 |
35 | // First column
36 | m.M11 = instream.ReadSingle();
37 | m.M12 = instream.ReadSingle();
38 | m.M13 = instream.ReadSingle();
39 | m.M14 = instream.ReadSingle();
40 |
41 | // Second column
42 | m.M21 = instream.ReadSingle();
43 | m.M22 = instream.ReadSingle();
44 | m.M23 = instream.ReadSingle();
45 | m.M24 = instream.ReadSingle();
46 |
47 | // Third column
48 | m.M31 = instream.ReadSingle();
49 | m.M32 = instream.ReadSingle();
50 | m.M33 = instream.ReadSingle();
51 | m.M34 = instream.ReadSingle();
52 |
53 | // Fourth column
54 | m.M41 = instream.ReadSingle();
55 | m.M42 = instream.ReadSingle();
56 | m.M43 = instream.ReadSingle();
57 | m.M44 = instream.ReadSingle();
58 |
59 | return m;
60 | }
61 |
62 | public static void Write(this System.IO.BinaryWriter outstream, System.Numerics.Matrix4x4 matrix)
63 | {
64 | outstream.Write(matrix.M11);
65 | outstream.Write(matrix.M12);
66 | outstream.Write(matrix.M13);
67 | outstream.Write(matrix.M14);
68 | outstream.Write(matrix.M21);
69 | outstream.Write(matrix.M22);
70 | outstream.Write(matrix.M23);
71 | outstream.Write(matrix.M24);
72 | outstream.Write(matrix.M31);
73 | outstream.Write(matrix.M32);
74 | outstream.Write(matrix.M33);
75 | outstream.Write(matrix.M34);
76 | outstream.Write(matrix.M41);
77 | outstream.Write(matrix.M42);
78 | outstream.Write(matrix.M43);
79 | outstream.Write(matrix.M44);
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/PD2ModelParser/Sections/Light.cs:
--------------------------------------------------------------------------------
1 | using BinaryReader = System.IO.BinaryReader;
2 | using BinaryWriter = System.IO.BinaryWriter;
3 |
4 | namespace PD2ModelParser.Sections
5 | {
6 | class LightColour
7 | {
8 | public float R { get; set; } = 0;
9 | public float G { get; set; } = 0;
10 | public float B { get; set; } = 0;
11 | public float A { get; set; } = 0;
12 |
13 | public LightColour() { }
14 | public LightColour(BinaryReader instream)
15 | {
16 | R = instream.ReadSingle();
17 | G = instream.ReadSingle();
18 | B = instream.ReadSingle();
19 | A = instream.ReadSingle();
20 | }
21 |
22 | public void StreamWriteData(BinaryWriter outstream)
23 | {
24 | outstream.Write(R);
25 | outstream.Write(G);
26 | outstream.Write(B);
27 | outstream.Write(A);
28 | }
29 | }
30 |
31 | [ModelFileSection(Tags.light_tag,ShowInInspectorRoot=false)]
32 | class Light : Object3D, ISection
33 | {
34 | /* zdann says that
35 | * color - vector3
36 | * multiplier - float
37 | * far_range - float
38 | * spot_angle_end - float
39 | * enable - bool
40 | * falloff_exponent - float
41 | * properties - string
42 | * final_color - vector3
43 | * specular_multiplier - float
44 | * ambient_cube_side - vector3
45 | * are pertinent properties to lights */
46 |
47 | public byte unknown_1 { get; set; } // 1 in all known lights
48 | public int LightType { get; set; } // it's 1 in light_omni and 2 in light_spot
49 | public LightColour Colour { get; set; }
50 | public float NearRange { get; set; } // probably NearRange
51 | public float FarRange { get; set; } // probably FarRange
52 | public float unknown_6 { get; set; }
53 | public float unknown_7 { get; set; }
54 | public float unknown_8 { get; set; } // BitConverter.ToSingle(byte[4] { 4, 0, 0, 0 }, 0) in all known lights
55 |
56 | public Light(string name, Object3D parent) : base(name, parent) { }
57 |
58 | public Light(BinaryReader instream, SectionHeader section) : base(instream)
59 | {
60 | this.SectionId = section.id;
61 | this.size = section.size;
62 |
63 | unknown_1 = instream.ReadByte();
64 | LightType = instream.ReadInt32();
65 |
66 | Colour = new LightColour(instream);
67 |
68 | NearRange = instream.ReadSingle();
69 | FarRange = instream.ReadSingle();
70 | unknown_6 = instream.ReadSingle();
71 | unknown_7 = instream.ReadSingle();
72 | unknown_8 = instream.ReadSingle();
73 |
74 | if ((section.offset + 12 + section.size) > instream.BaseStream.Position)
75 | {
76 | remaining_data = instream.ReadBytes((int)((section.offset + 12 + section.size) - instream.BaseStream.Position));
77 | Log.Default.Info("Light {0}|{1} has {2} bytes of extraneous(?) data", section.id, HashName, remaining_data.Length);
78 | }
79 | }
80 |
81 | public override void StreamWriteData(BinaryWriter outstream)
82 | {
83 | base.StreamWriteData(outstream);
84 | outstream.Write(unknown_1);
85 | outstream.Write(LightType);
86 | Colour.StreamWriteData(outstream);
87 | outstream.Write(NearRange);
88 | outstream.Write(FarRange);
89 | outstream.Write(unknown_6);
90 | outstream.Write(unknown_7);
91 | outstream.Write(unknown_8);
92 | if (remaining_data != null)
93 | {
94 | outstream.Write(remaining_data, 0, remaining_data.Length);
95 | }
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/PD2ModelParser/Importers/AnimationImporter.cs:
--------------------------------------------------------------------------------
1 | using PD2ModelParser.Misc;
2 | using PD2ModelParser.Sections;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Numerics;
7 |
8 | namespace PD2ModelParser.Importers {
9 | class AnimationImporter {
10 | public static void Import(FullModelData fmd, string path) {
11 | AnimationFile animationFile = new AnimationFile();
12 | animationFile.Read(path);
13 |
14 | foreach(AnimationFileObject animationObject in animationFile.Objects) {
15 | Object3D object3D = fmd.GetObject3DByHash(new HashName(animationObject.Name));
16 |
17 | Log.Default.Info("Trying to add animation to " + animationObject.Name);
18 | if (object3D != null) {
19 | Log.Default.Info("Found " + animationObject.Name);
20 |
21 | object3D.Animations.Clear(); // Kill the old anims.
22 |
23 | if (animationObject.RotationKeyframes.Count > 0) {
24 | QuatLinearRotationController quatLinearRotationController = AddRotations(object3D, animationObject.RotationKeyframes);
25 | fmd.AddSection(quatLinearRotationController);
26 | }
27 |
28 | if (animationObject.PositionKeyframes.Count > 0) {
29 | LinearVector3Controller linearVector3Controller = AddPositions(object3D, animationObject.PositionKeyframes);
30 | fmd.AddSection(linearVector3Controller);
31 | }
32 | } else {
33 | Log.Default.Info("Not Found " + animationObject.Name);
34 | }
35 | }
36 | }
37 |
38 | public static QuatLinearRotationController AddRotations(Object3D targetObject, IList> keyframes) {
39 | var quatLinearRotationController = new QuatLinearRotationController();
40 | quatLinearRotationController.Keyframes = new List>(keyframes);
41 | quatLinearRotationController.KeyframeLength = quatLinearRotationController.Keyframes.Max(kf => kf.Timestamp);
42 |
43 | if (targetObject.Animations.Count == 0) {
44 | targetObject.Animations.Add(quatLinearRotationController);
45 | targetObject.Animations.Add(null);
46 | } else if (targetObject.Animations.Count == 1 && (targetObject.Animations[0].GetType() == typeof(LinearVector3Controller))) {
47 | targetObject.Animations.Insert(0, quatLinearRotationController);
48 | } else {
49 | throw new Exception($"Failed to insert animation in {targetObject.Name}: unrecognised controller list shape");
50 | }
51 |
52 | return quatLinearRotationController;
53 | }
54 |
55 | public static LinearVector3Controller AddPositions(Object3D targetObject, IList> keyframes) {
56 | LinearVector3Controller linearVector3Controller = new LinearVector3Controller();
57 | linearVector3Controller.Keyframes = new List>(keyframes);
58 | linearVector3Controller.KeyframeLength = linearVector3Controller.Keyframes.Max(kf => kf.Timestamp);
59 |
60 | if (targetObject.Animations.Count == 0) {
61 | targetObject.Animations.Add(linearVector3Controller);
62 | } else if (targetObject.Animations.Count == 2
63 | && targetObject.Animations[0].GetType() == typeof(QuatLinearRotationController)
64 | && targetObject.Animations[1] == null) {
65 | targetObject.Animations[1] = linearVector3Controller;
66 | } else {
67 | throw new Exception($"Failed to insert animation in {targetObject.Name}: unrecognised controller list shape");
68 | }
69 |
70 | return linearVector3Controller;
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Docs/RiggingPosesAnimations.md:
--------------------------------------------------------------------------------
1 | # Rigging and Animations
2 |
3 | Or: how to make and modify custom player and cop models.
4 |
5 | As of the version of the model tool that contains this documentation, the rigging
6 | support is basically finished, and is almost seamless to use.
7 |
8 | The TL;DR is that:
9 |
10 | ```xml
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | ```
22 |
23 | Is all you need to get a model exported properly.
24 |
25 | ## Animations and bone transforms
26 |
27 | When rigging a model, each bone actually has two transforms (a transform is
28 | the combination of translation, rotation and scaling). One transform gives
29 | the bone's position relative to it's parent, and the other transform (this
30 | is the 'object transform') and the other one is the bind transform - it's
31 | used to determine how the bone attaches to the skin. You don't set the bind
32 | transform yourself when modelling, Blender figures it out for you.
33 |
34 | Here's an example: In blender, create a small box and attach it to a bone:
35 |
36 | 
37 |
38 | In the armature's edit mode, we can roll (using the 'N' panel) the bone, and the
39 | mesh stays exactly the same:
40 |
41 | 
42 |
43 | At this point, we've changed the bone's object transform (by rotating it), but
44 | the mesh it's attached to hasn't also rotated. This is because Blender internally
45 | rotated the bind transform backwards 45° to compensate, so the skin stays in
46 | the same place.
47 |
48 | Now, let's pretend we're PAYDAY and we're loading an animation made for the original
49 | model. The animation says the bone shouldn't have any rotation. We can simulate this by
50 | going into pose mode, and setting the bone to have no rotation:
51 |
52 | 
53 |
54 | The bone is now in the same position and orientation as it was in the first screenshot,
55 | but the model it's attached to is now rotated 45°.
56 |
57 | This is what would happen if we used the following script, modified from above:
58 |
59 | ```xml
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | ```
70 |
71 | Unfortunately, when you import a GLTF model into blender, all the bones get
72 | rotated like in the 2nd screenshot.
73 |
74 | By default, when you import a GLTF/GLB file into the model tool, all the objects
75 | in the model (including the bones) will be moved and rotated to match what was
76 | in the GLTF file. When in game this looks fine if you disable animations, but
77 | with animations on it all horribly breaks. If you want to try this yourself, be
78 | sure to put a `` before the `` tag, otherwise it'll get broken
79 | differently. Here's what it looks like in game:
80 |
81 | 
82 |
83 | The red lines indicate where the bones are. You can see they're all in the
84 | right place, but the model isn't attached to them. This is what these broken
85 | transforms cause.
86 |
87 | Thus setting the `import-transforms` flag prevents you from making any changes
88 | to the rigging yourself in Blender, as it'll just get overwritten during
89 | import. However since it uses the original bones, all the original animations
90 | still work.
91 |
92 |
--------------------------------------------------------------------------------
/PD2ModelParser/UI/FileBrowserControl.Designer.cs:
--------------------------------------------------------------------------------
1 | namespace PD2ModelParser.UI
2 | {
3 | partial class FileBrowserControl
4 | {
5 | ///
6 | /// Required designer variable.
7 | ///
8 | private System.ComponentModel.IContainer components = null;
9 |
10 | ///
11 | /// Clean up any resources being used.
12 | ///
13 | /// true if managed resources should be disposed; otherwise, false.
14 | protected override void Dispose(bool disposing)
15 | {
16 | if (disposing && (components != null))
17 | {
18 | components.Dispose();
19 | }
20 | base.Dispose(disposing);
21 | }
22 |
23 | #region Component Designer generated code
24 |
25 | ///
26 | /// Required method for Designer support - do not modify
27 | /// the contents of this method with the code editor.
28 | ///
29 | private void InitializeComponent()
30 | {
31 | inputFileBox = new System.Windows.Forms.TextBox();
32 | browseBttn = new System.Windows.Forms.Button();
33 | clearBttn = new System.Windows.Forms.Button();
34 | SuspendLayout();
35 | //
36 | // inputFileBox
37 | //
38 | inputFileBox.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
39 | inputFileBox.Enabled = false;
40 | inputFileBox.Location = new System.Drawing.Point(0, 2);
41 | inputFileBox.Margin = new System.Windows.Forms.Padding(0);
42 | inputFileBox.Name = "inputFileBox";
43 | inputFileBox.Size = new System.Drawing.Size(251, 23);
44 | inputFileBox.TabIndex = 14;
45 | //
46 | // browseBttn
47 | //
48 | browseBttn.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
49 | browseBttn.Location = new System.Drawing.Point(251, 0);
50 | browseBttn.Margin = new System.Windows.Forms.Padding(0);
51 | browseBttn.Name = "browseBttn";
52 | browseBttn.Size = new System.Drawing.Size(88, 27);
53 | browseBttn.TabIndex = 15;
54 | browseBttn.Text = "Browse...";
55 | browseBttn.UseVisualStyleBackColor = true;
56 | browseBttn.Click += browseBttn_Click;
57 | //
58 | // clearBttn
59 | //
60 | clearBttn.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
61 | clearBttn.Location = new System.Drawing.Point(340, 0);
62 | clearBttn.Margin = new System.Windows.Forms.Padding(0);
63 | clearBttn.Name = "clearBttn";
64 | clearBttn.Size = new System.Drawing.Size(44, 27);
65 | clearBttn.TabIndex = 15;
66 | clearBttn.Text = "Clear";
67 | clearBttn.UseVisualStyleBackColor = true;
68 | clearBttn.Click += ClearFileSelected;
69 | //
70 | // FileBrowserControl
71 | //
72 | AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
73 | AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
74 | Controls.Add(inputFileBox);
75 | Controls.Add(browseBttn);
76 | Controls.Add(clearBttn);
77 | Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
78 | Name = "FileBrowserControl";
79 | Size = new System.Drawing.Size(384, 27);
80 | ResumeLayout(false);
81 | PerformLayout();
82 | }
83 |
84 | #endregion
85 |
86 | private System.Windows.Forms.TextBox inputFileBox;
87 | private System.Windows.Forms.Button browseBttn;
88 | private System.Windows.Forms.Button clearBttn;
89 |
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/PD2ModelParser/Sections/LinearVector3Controller.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Numerics;
6 |
7 | namespace PD2ModelParser.Sections
8 | {
9 | [ModelFileSection(Tags.linearVector3Controller_tag)]
10 | class LinearVector3Controller : AbstractSection, ISection, IHashNamed, IAnimationController
11 | {
12 | public UInt32 size;
13 |
14 | public HashName HashName { get; set; }
15 | public byte Flag0 { get; set; }
16 | public byte Flag1 { get; set; }
17 | public byte Flag2 { get; set; }
18 | public byte Flag3 { get; set; }
19 |
20 | public uint Flags
21 | {
22 | get => Flag0 | ((uint)Flag1 << 8) | ((uint)Flag2 << 16) | ((uint)Flag3 << 24);
23 | set
24 | {
25 | Flag0 = (byte)(value & 0x000000FF);
26 | Flag1 = (byte)((value >> 8) & 0x0000FF00);
27 | Flag2 = (byte)((value >> 16) & 0x00FF0000);
28 | Flag3 = (byte)((value >> 24) & 0xFF000000);
29 | }
30 | }
31 |
32 | public uint Unknown1 { get; set; }
33 | public float KeyframeLength { get; set; }
34 | public IList> Keyframes { get; set; } = new List>();
35 |
36 | public byte[] remaining_data = null;
37 |
38 | public LinearVector3Controller(string name = null) => HashName = new HashName(name ?? "");
39 |
40 | public LinearVector3Controller(BinaryReader instream, SectionHeader section)
41 | {
42 | this.SectionId = section.id;
43 | this.size = section.size;
44 |
45 | this.HashName = new HashName(instream.ReadUInt64());
46 | this.Flag0 = instream.ReadByte();
47 | this.Flag1 = instream.ReadByte();
48 | this.Flag2 = instream.ReadByte();
49 | this.Flag3 = instream.ReadByte();
50 | this.Unknown1 = instream.ReadUInt32();
51 | this.KeyframeLength = instream.ReadSingle();
52 | var keyframe_count = instream.ReadUInt32();
53 |
54 | for (int x = 0; x < keyframe_count; x++)
55 | {
56 | this.Keyframes.Add(new Keyframe(instream.ReadSingle(), instream.ReadVector3()));
57 | }
58 |
59 | this.remaining_data = null;
60 | if ((section.offset + 12 + section.size) > instream.BaseStream.Position)
61 | this.remaining_data = instream.ReadBytes((int)((section.offset + 12 + section.size) - instream.BaseStream.Position));
62 | }
63 |
64 | public override void StreamWriteData(BinaryWriter outstream)
65 | {
66 | outstream.Write(this.HashName.Hash);
67 | outstream.Write(this.Flag0);
68 | outstream.Write(this.Flag1);
69 | outstream.Write(this.Flag2);
70 | outstream.Write(this.Flag3);
71 | outstream.Write(this.Unknown1);
72 | outstream.Write(this.KeyframeLength);
73 | outstream.Write(this.Keyframes.Count);
74 |
75 | foreach (var kf in this.Keyframes)
76 | {
77 | outstream.Write(kf.Timestamp);
78 | outstream.Write(kf.Value);
79 | }
80 |
81 | if (this.remaining_data != null)
82 | outstream.Write(this.remaining_data);
83 | }
84 |
85 | public override string ToString()
86 | {
87 |
88 | string keyframes_string = (this.Keyframes.Count == 0 ? "none" : "");
89 | keyframes_string += string.Join(", ", this.Keyframes.Select(i => i.ToString()));
90 |
91 | return base.ToString() +
92 | $" size: {this.size} HashName: {this.HashName}" +
93 | $" flag0: {this.Flag0} flag1: {this.Flag1} flag2: {this.Flag2} flag3: {this.Flag3}" +
94 | $" unknown1: {this.Unknown1} keyframe_length: {this.KeyframeLength}" +
95 | $" count: {this.Keyframes.Count} items: [ {keyframes_string} ] " +
96 | (this.remaining_data != null ? " REMAINING DATA! " + this.remaining_data.Length + " bytes" : "");
97 | }
98 |
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/PD2ModelParser/Sections/QuatLinearRotationController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Numerics;
6 |
7 | namespace PD2ModelParser.Sections
8 | {
9 | [ModelFileSection(Tags.quatLinearRotationController_tag)]
10 | class QuatLinearRotationController : AbstractSection, ISection, IHashNamed, IAnimationController
11 | {
12 | public UInt32 size;
13 |
14 | public HashName HashName { get; set; }
15 | public Byte flag0; // 2 = Loop?
16 | public Byte flag1;
17 | public Byte flag2;
18 | public Byte flag3;
19 |
20 | public uint Flags
21 | {
22 | get => flag0 | ((uint)flag1 << 8) | ((uint)flag2 << 16) | ((uint)flag3 << 24);
23 | set
24 | {
25 | flag0 = (byte)(value & 0x000000FF);
26 | flag1 = (byte)((value >> 8) & 0x0000FF00);
27 | flag2 = (byte)((value >> 16) & 0x00FF0000);
28 | flag3 = (byte)((value >> 24) & 0xFF000000);
29 | }
30 | }
31 |
32 | public UInt32 unknown1;
33 | public float KeyframeLength { get; set; }
34 | public IList> Keyframes { get; set; } = new List>();
35 |
36 | public byte[] remaining_data = null;
37 |
38 | public QuatLinearRotationController(string name = null) => HashName = new HashName(name ?? "");
39 |
40 | public QuatLinearRotationController(BinaryReader instream, SectionHeader section)
41 | {
42 | this.SectionId = section.id;
43 | this.size = section.size;
44 |
45 | this.HashName = new HashName(instream.ReadUInt64());
46 | this.flag0 = instream.ReadByte();
47 | this.flag1 = instream.ReadByte();
48 | this.flag2 = instream.ReadByte();
49 | this.flag3 = instream.ReadByte();
50 | this.unknown1 = instream.ReadUInt32();
51 | this.KeyframeLength = instream.ReadSingle();
52 | var keyframe_count = instream.ReadUInt32();
53 |
54 | for(int x = 0; x < keyframe_count; x++)
55 | {
56 | this.Keyframes.Add(new Keyframe(instream.ReadSingle(), instream.ReadQuaternion()));
57 | }
58 |
59 | this.remaining_data = null;
60 | if ((section.offset + 12 + section.size) > instream.BaseStream.Position)
61 | this.remaining_data = instream.ReadBytes((int)((section.offset + 12 + section.size) - instream.BaseStream.Position));
62 | }
63 |
64 | public override void StreamWriteData(BinaryWriter outstream)
65 | {
66 | outstream.Write(this.HashName.Hash);
67 | outstream.Write(this.flag0);
68 | outstream.Write(this.flag1);
69 | outstream.Write(this.flag2);
70 | outstream.Write(this.flag3);
71 | outstream.Write(this.unknown1);
72 | outstream.Write(this.KeyframeLength);
73 | outstream.Write(this.Keyframes.Count);
74 |
75 | foreach (var item in this.Keyframes)
76 | {
77 | outstream.Write(item.Timestamp);
78 | outstream.Write(item.Value.X);
79 | outstream.Write(item.Value.Y);
80 | outstream.Write(item.Value.Z);
81 | outstream.Write(item.Value.W);
82 | }
83 |
84 | if (this.remaining_data != null)
85 | outstream.Write(this.remaining_data);
86 | }
87 |
88 | public override string ToString()
89 | {
90 |
91 | string keyframes_string = (this.Keyframes.Count == 0 ? "none" : "");
92 | keyframes_string += string.Join(", ", Keyframes.Select(i => i.ToString()));
93 |
94 | return base.ToString() +
95 | $" size: {this.size} HashName: {this.HashName}" +
96 | $" flag0: {this.flag0} flag1: {this.flag1} flag2: {this.flag2} flag3: {this.flag3}" +
97 | $" unknown1: {this.unknown1} keyframe_length: {this.KeyframeLength}" +
98 | $" count: {this.Keyframes.Count} items: [ {keyframes_string} ] " +
99 | (this.remaining_data != null ? " REMAINING DATA! " + this.remaining_data.Length + " bytes" : "");
100 | }
101 |
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/PD2ModelParser/Inspector/InspectionTree.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 |
5 | namespace PD2ModelParser.Inspector
6 | {
7 | public interface IInspectorNode
8 | {
9 | string Key { get; }
10 | string IconName { get; }
11 | string Label { get; }
12 | object PropertyItem { get; }
13 | IEnumerable GetChildren();
14 | }
15 |
16 | class ModelRootNode : IInspectorNode
17 | {
18 | FullModelData data;
19 | public ModelRootNode(FullModelData fmd) {
20 | data = fmd;
21 | }
22 |
23 | public string Key => "";
24 | public string IconName => null;
25 | public string Label => "";
26 | public object PropertyItem => data;
27 | public IEnumerable GetChildren()
28 | => Sections.SectionMetaInfo.All().Where(i => i.ShowInInspectorRoot).Select(i => i.GetRootInspector(data));
29 | }
30 |
31 |
32 | class AllSectionsNode : IInspectorNode
33 | where TSection : class, Sections.ISection
34 | {
35 | FullModelData data;
36 | Func labeller;
37 | public AllSectionsNode(FullModelData fmd)
38 | {
39 | data = fmd;
40 | Key = $">";
41 | Label = $"<{typeof(TSection).Name}>";
42 | if(typeof(TSection).GetInterfaces().Contains(typeof(Sections.IHashNamed)))
43 | {
44 | labeller = (sec) => $"{sec.SectionId} | {sec.GetType().Name} ({(sec as Sections.IHashNamed).HashName})";
45 | }
46 | else
47 | {
48 | labeller = (sec) => $"{sec.SectionId} | {sec.GetType().Name}";
49 | }
50 | }
51 |
52 | public string Key { get; private set; }
53 | public string IconName => null;
54 | public string Label { get; private set; }
55 | public object PropertyItem => null;
56 | public IEnumerable GetChildren()
57 | {
58 | var cname = typeof(TSection).Name;
59 | return data.SectionsOfType().Select(i => new GenericNode
60 | {
61 | Key = $"{cname}_{i.SectionId}",
62 | IconName = null,
63 | Label = labeller(i),
64 | PropertyItem = i
65 | });
66 | }
67 | }
68 |
69 | class GenericNode : IInspectorNode
70 | {
71 | public string Key { get; set; }
72 | public string IconName { get; set; }
73 | public string Label { get; set; }
74 | public object PropertyItem { get; set; }
75 | public IEnumerable GetChildren() => Enumerable.Empty();
76 | }
77 |
78 | class ObjectsRootNode : IInspectorNode
79 | {
80 | FullModelData data;
81 | public ObjectsRootNode(FullModelData fmd)
82 | {
83 | data = fmd;
84 | }
85 |
86 | public string Key => "";
87 | public string IconName => null;
88 | public string Label => "";
89 | public object PropertyItem => null;
90 | public IEnumerable GetChildren()
91 | {
92 | return data.SectionsOfType().Where(i => i.Parent == null).Select(i => new ObjectNode(data, i));
93 | }
94 | }
95 |
96 | class ObjectNode : IInspectorNode
97 | {
98 | FullModelData data;
99 | Sections.Object3D obj;
100 | public ObjectNode(FullModelData fmd, Sections.Object3D obj)
101 | {
102 | data = fmd;
103 | this.obj = obj;
104 |
105 | Label = $"{obj.SectionId} | {obj.Name}";
106 | if (obj.GetType() != typeof(Sections.Object3D))
107 | {
108 | Label += $" ({obj.GetType().Name})";
109 | }
110 | }
111 |
112 | public string Key => $"obj_{obj.SectionId}";
113 | public string IconName => null;
114 | public string Label { get; private set; }
115 | public object PropertyItem => obj;
116 | public IEnumerable GetChildren()
117 | {
118 | return data.SectionsOfType().Where(i => i.Parent == obj).Select(i => new ObjectNode(data, i));
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/PD2ModelParser/Sections/SkinBones.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.ComponentModel;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Numerics;
6 |
7 | namespace PD2ModelParser.Sections
8 | {
9 | [ModelFileSection(Tags.skinbones_tag, ShowInInspectorRoot=false)]
10 | class SkinBones : Bones, ISection, IPostLoadable
11 | {
12 | private List objects { get; set; } = new List(); // of Object3D by SectionID
13 |
14 | [TypeConverter(typeof(Inspector.Object3DReferenceConverter))]
15 | public Object3D ProbablyRootBone { get; set; }
16 | public int count => Objects.Count;
17 | public List Objects { get; private set; } = new List();
18 | public List rotations { get; private set; } = new List();
19 | public Matrix4x4 global_skin_transform { get; set; }
20 |
21 | // Post-loaded
22 | public List SkinPositions { get; private set; }
23 |
24 | public SkinBones() : base() { }
25 |
26 | public SkinBones(BinaryReader instream, SectionHeader section) : base(instream)
27 | {
28 | this.SectionId = section.id;
29 | this.size = section.size;
30 |
31 | PostLoadRef(instream.ReadUInt32(), i => ProbablyRootBone = i);
32 | uint count = instream.ReadUInt32();
33 | for (int x = 0; x < count; x++)
34 | this.objects.Add(instream.ReadUInt32());
35 | for (int x = 0; x < count; x++)
36 | {
37 | this.rotations.Add(instream.ReadMatrix());
38 | }
39 |
40 | this.global_skin_transform = instream.ReadMatrix();
41 |
42 | this.remaining_data = null;
43 |
44 | long end_pos = section.offset + 12 + section.size;
45 | if (end_pos > instream.BaseStream.Position)
46 | {
47 | // If exists, this contains hashed name for this geometry (see hashlist.txt)
48 | remaining_data = instream.ReadBytes((int) (end_pos - instream.BaseStream.Position));
49 | }
50 | }
51 |
52 | public override void StreamWriteData(BinaryWriter outstream)
53 | {
54 | base.StreamWriteData(outstream);
55 | outstream.Write(this.ProbablyRootBone.SectionId);
56 | outstream.Write(this.count);
57 |
58 | SectionUtils.CheckLength(count, Objects);
59 | SectionUtils.CheckLength(count, rotations);
60 |
61 | foreach (var item in this.Objects)
62 | outstream.Write(item.SectionId);
63 | foreach (Matrix4x4 matrix in this.rotations)
64 | {
65 | outstream.Write(matrix);
66 | }
67 |
68 | outstream.Write(global_skin_transform);
69 |
70 | if (this.remaining_data != null)
71 | outstream.Write(this.remaining_data);
72 | }
73 |
74 | public override string ToString()
75 | {
76 | string objects_string = (this.Objects.Count == 0 ? "none" : "");
77 |
78 | objects_string += string.Join(", ", this.Objects.Select(i => i.SectionId));
79 |
80 | string rotations_string = (this.rotations.Count == 0 ? "none" : "");
81 |
82 | foreach (Matrix4x4 rotation in this.rotations)
83 | {
84 | rotations_string += rotation + ", ";
85 | }
86 |
87 | return base.ToString() +
88 | " object3D_section_id: " + this.ProbablyRootBone.SectionId +
89 | " count: " + this.Objects.Count + " objects:[ " + objects_string + " ]" +
90 | " rotations count: " + this.rotations.Count + " rotations:[ " + rotations_string + " ]" +
91 | " global_skin_transform: " + this.global_skin_transform +
92 | (this.remaining_data != null ? " REMAINING DATA! " + this.remaining_data.Length + " bytes" : "");
93 | }
94 |
95 | public override void PostLoad(uint id, Dictionary parsed_sections)
96 | {
97 | base.PostLoad(id, parsed_sections);
98 | SkinPositions = new List(count);
99 |
100 | for (int i = 0; i < objects.Count; i++)
101 | {
102 | Object3D obj = (Object3D) parsed_sections[objects[i]];
103 | Objects.Add(obj);
104 |
105 | Matrix4x4 inter = rotations[i].MultDiesel(obj.WorldTransform);
106 | Matrix4x4 skin_node = inter.MultDiesel(global_skin_transform);
107 |
108 | SkinPositions.Add(skin_node);
109 | }
110 | objects = null;
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/PD2ModelParser/Sections/Topology.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 |
6 | namespace PD2ModelParser.Sections
7 | {
8 | /** A triangular face */
9 | public struct Face
10 | {
11 | /** The index of the first vertex in this face */
12 | public readonly ushort a;
13 |
14 | /** The index of the second vertex in this face */
15 | public readonly ushort b;
16 |
17 | /** The index of the third (last) vertex in this face */
18 | public readonly ushort c;
19 |
20 | public Face(ushort a, ushort b, ushort c)
21 | {
22 | this.a = a;
23 | this.b = b;
24 | this.c = c;
25 | }
26 |
27 | public Face OffsetBy(int offset)
28 | {
29 | return new Face(
30 | (ushort) (a + offset),
31 | (ushort) (b + offset),
32 | (ushort) (c + offset)
33 | );
34 | }
35 |
36 | public bool BoundsCheck(int vertlen)
37 | {
38 | return a >= 0 && b >= 0 && c >= 0 && a < vertlen && b < vertlen && c < vertlen;
39 | }
40 |
41 | public override string ToString()
42 | {
43 | return $"{a}, {b}, {c}";
44 | }
45 | }
46 |
47 | [ModelFileSection(Tags.topology_tag)]
48 | class Topology : AbstractSection, ISection, IHashNamed
49 | {
50 | public UInt32 unknown1 { get; set; }
51 | public List facelist = new List();
52 | public UInt32 count2;
53 | public byte[] items2;
54 | public HashName HashName { get; set; }
55 |
56 | public byte[] remaining_data = null;
57 |
58 | public Topology Clone(string newName)
59 | {
60 | var dst = new Topology(newName);
61 | dst.unknown1 = this.unknown1;
62 | dst.facelist.Capacity = this.facelist.Count;
63 | dst.facelist.AddRange(this.facelist.Select(f => new Face(f.a, f.b, f.c )));
64 | dst.count2 = this.count2;
65 | dst.items2 = (byte[])(this.items2.Clone());
66 | return dst;
67 | }
68 |
69 | public Topology(string objectName)
70 | {
71 | this.unknown1 = 0;
72 |
73 | this.count2 = 0;
74 | this.items2 = new byte[0];
75 | this.HashName = new HashName(objectName + ".Topology");
76 | }
77 |
78 | public Topology(obj_data obj) : this(obj.object_name)
79 | {
80 | this.facelist = obj.faces;
81 | }
82 |
83 | public Topology(BinaryReader instream, SectionHeader section)
84 | {
85 | SectionId = section.id;
86 | this.unknown1 = instream.ReadUInt32();
87 | uint count1 = instream.ReadUInt32();
88 | for (int x = 0; x < count1 / 3; x++)
89 | {
90 | var a = instream.ReadUInt16();
91 | var b = instream.ReadUInt16();
92 | var c = instream.ReadUInt16();
93 | this.facelist.Add(new Face(a,b,c));
94 | }
95 |
96 | this.count2 = instream.ReadUInt32();
97 | this.items2 = instream.ReadBytes((int) this.count2);
98 | this.HashName = new HashName(instream.ReadUInt64());
99 |
100 | this.remaining_data = null;
101 | if ((section.offset + 12 + section.size) > instream.BaseStream.Position)
102 | remaining_data = instream.ReadBytes((int)((section.offset + 12 + section.size) - instream.BaseStream.Position));
103 | }
104 |
105 | public override void StreamWriteData(BinaryWriter outstream)
106 | {
107 | outstream.Write(this.unknown1);
108 | outstream.Write(facelist.Count * 3);
109 | foreach (Face face in facelist)
110 | {
111 | outstream.Write(face.a);
112 | outstream.Write(face.b);
113 | outstream.Write(face.c);
114 | }
115 |
116 | outstream.Write(this.count2);
117 | outstream.Write(this.items2);
118 | outstream.Write(this.HashName.Hash);
119 |
120 | if (this.remaining_data != null)
121 | outstream.Write(this.remaining_data);
122 | }
123 |
124 | public override string ToString()
125 | {
126 | return base.ToString() +
127 | $" unknown1: {unknown1} facelist: {facelist.Count} count2: {count2}" +
128 | $" items2: {items2.Length} HashName: {HashName}" +
129 | (this.remaining_data != null ? " REMAINING DATA! " + this.remaining_data.Length + " bytes" : "");
130 | }
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PAYDAY 2 Model Tool - Calcium Edition
2 |
3 | This is a copy of IAmNotASpy and PoueT's model tool, with a bunch of new features:
4 |
5 | * Greatly improved UI, with different functions cleanly separated
6 | * The ability to use an XML-based script to modify the object/bone structure of models, and create entirely
7 | new models without deriving them from an existing model, and set rootpoints for different objects
8 | * Experimental Collada (DAE) export support, with bones.
9 | * glTF export support, with vertex colours, all eight UV channels, and material slots (multiUV).
10 | There's also preliminary support for exporting rigged models.
11 | * glTF import support, also with vertex colours, all eight UV channels, and material slots.
12 | * And a bunch of miscellaneous features and bugfixes
13 |
14 | # glTF export/import
15 |
16 | Both the importer and exporter treat the material name `Material: Default Material` specially: it becomes no
17 | material on export, and a lack of material on import is replaced with that. Otherwise, the exporter creates
18 | a dummy material for each material name in the Diesel model. The importer doesn't care about the precise
19 | definition of materials, only their names.
20 |
21 | Exporting preserves the object hierarchy, and includes partial rigging support: bones and weights should be
22 | exported, but validating glTF parsers may complain about non-normalised weights, and whether or not meshes
23 | stay attached to their skeletons is a bit iffy.
24 |
25 | Importing is designed so you don't need a modelscript so much:
26 | * If an object has the same name as one already in the .model, the latter's rotation and parentage are overwritten.
27 | * Models with the same name as an existing object delete that object and adopt its child objects.
28 | (this may break animations).
29 | * Models with the same name as an existing *model* have their model data replaced.
30 | * Objects with a parent in the glTF file always keep that parent on import.
31 | * Objects with no parent in the glTF file are parented according to the modelscript, except that not specifying a
32 | rootpoint at all isn't an error.
33 |
34 | Because GLTF dictates a 1m scale, and Payday 2 uses a 1cm scale, the exporter accounts for this (this does have
35 | the downside that if you're importing into Blender bones and empties are drawn much too big).
36 |
37 | # Feature Matrix
38 |
39 | | Format | Import | Export |
40 | |--------|--------|--------|
41 | | OBJ | ✓ | ✓ |
42 | | DAE | | ✓ |
43 | | GLTF | ✓ | ✓ |
44 |
45 | | Data | DAE | GLTF In | GLTF Out |
46 | |------------------|-----|---------|----------|
47 | | Triangles | ✓ | ✓ | ✓ |
48 | | UV channels | One | ✓ | ✓ |
49 | | Vertex colours | ✗ | ✓ | ✓ |
50 | | Vertex weights | ✗ | ✓ | ✓ |
51 | | Material slots | ✗ | ✓ | ✓ |
52 | | Object hierarchy | ✓ | ✓ | ✓ |
53 | | Bones | As objects | As objects | Partial |
54 | | Skinning | ✗ | Ignored | Partial |
55 |
56 | Partial bone/skinning support refers to the result not being read sensibly in all implementations.
57 |
58 | The GLTF importer completely ignores skinning data, so the results will be odd as well as effectively unrigged.
59 |
60 | # Hashlists
61 | Diesel very rarely stores actual names of things if it can store a hash of the name instead, so a list of
62 | names is needed in order to present something readable names instead of just large numbers. On export anything
63 | not in the list will be written as a number, while the GLTF importer will assume any name that's a valid
64 | `unsigned long` is the result of that process.
65 |
66 | A copy of [Luffyyy's version of the hashlist](https://github.com/Luffyyy/PAYDAY-2-Hashlist) is included; the
67 | tool looks for files whose names include, case insensitively, `hashlist` or `hashes`, in the current directory
68 | and next to the executable. Any it finds are interpreted as lists
69 | of unhashed names, one per line. If you change hashlists you will need to restart the tool in order to pick
70 | up the changes.
71 |
72 | # Licence:
73 |
74 | This program is Free Software under the terms of the GNU General Public Licence, version 3. A copy of
75 | this licence is distributed with the program's source files.
76 |
77 | As an exception to the GPLv3, you may use Autodesk's FBX SDK as part of this program, and you are not
78 | required to provide that under the GPL (since it is impossible to do so, and would prevent binary redistribution).
79 |
80 | If you are using this exception, you must ship the FBX SDK dynamically linked (not statically linked), and
81 | you must provide the entire rest of the program under the GPLv3 with this exception.
82 |
83 | If you wish, you may also delete this exception from modified versions of the software and use the plain
84 | GPLv3.
85 |
--------------------------------------------------------------------------------
/PD2ModelParser/Logging.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.Reflection;
4 |
5 | namespace PD2ModelParser
6 | {
7 | public static class Log
8 | {
9 | private static ILogger _default;
10 |
11 | public static ILogger Default
12 | {
13 | get
14 | {
15 | if (_default == null)
16 | throw new InvalidOperationException("Default logger not set!");
17 | return _default;
18 | }
19 |
20 | set
21 | {
22 | if (_default != null)
23 | throw new InvalidOperationException("Default logger already set!");
24 | _default = value ?? throw new InvalidOperationException("Cannot set default logger to null");
25 | }
26 | }
27 | }
28 |
29 | public enum LoggerLevel
30 | {
31 | Debug,
32 | Info,
33 | Status,
34 | Warn,
35 | Error
36 | }
37 |
38 | public interface ILogger
39 | {
40 | ///
41 | /// Prints or stores a log entry in whatever way is appropriate
42 | /// for the current logger.
43 | ///
44 | /// The severity of the message
45 | /// The format string to print
46 | /// The parameters for the format string
47 | void Log(LoggerLevel level, string message, params object[] value);
48 |
49 | ///
50 | /// Logs a very verbose and normally useless string, that's generally
51 | /// only useful for debugging.
52 | ///
53 | /// The format string to print
54 | /// The parameters for the format string
55 | void Debug(string message, params object[] value);
56 |
57 | ///
58 | /// Logs a piece of information that is potentially useful for the user,
59 | /// but is generally not needed.
60 | ///
61 | /// The format string to print
62 | /// The parameters for the format string
63 | void Info(string message, params object[] value);
64 |
65 | ///
66 | /// Logs the current status of the tool. This may be displayed on progress
67 | /// bars and the like, so it should not be called multiple times to display a
68 | /// single message.
69 | ///
70 | /// The format string to print
71 | /// The parameters for the format string
72 | void Status(string message, params object[] value);
73 |
74 | ///
75 | /// Logs a warning. This is a very important message to warn the user an error
76 | /// has occured, but the program has recovered (however, invalid output may result).
77 | ///
78 | /// The format string to print
79 | /// The parameters for the format string
80 | void Warn(string message, params object[] value);
81 |
82 | ///
83 | /// This operation has hit an unrecoverable error, and cannot continue.
84 | ///
85 | /// The format string to print
86 | /// The parameters for the format string
87 | void Error(string message, params object[] value);
88 | }
89 |
90 | public abstract class BaseLogger : ILogger
91 | {
92 | protected string GetCallerName(int level)
93 | {
94 | StackFrame frame = new StackFrame(level);
95 | MethodBase method = frame.GetMethod();
96 | return $"{method.DeclaringType?.Name}.{method.Name}";
97 | }
98 |
99 | public abstract void Log(LoggerLevel level, string message, params object[] value);
100 |
101 | public void Debug(string message, params object[] value)
102 | {
103 | Log(LoggerLevel.Debug, message, value);
104 | }
105 |
106 | public void Info(string message, params object[] value)
107 | {
108 | Log(LoggerLevel.Info, message, value);
109 | }
110 |
111 | public void Status(string message, params object[] value)
112 | {
113 | Log(LoggerLevel.Status, message, value);
114 | }
115 |
116 | public void Warn(string message, params object[] value)
117 | {
118 | Log(LoggerLevel.Warn, message, value);
119 | }
120 |
121 | public void Error(string message, params object[] value)
122 | {
123 | Log(LoggerLevel.Error, message, value);
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/PD2ModelParser/Sections/Bones.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 |
5 | namespace PD2ModelParser.Sections
6 | {
7 | ///
8 | /// Represents an entry in dsl::BoneMapping, storing indexed mappings to SkinBones.SkinPositions
9 | ///
10 | ///
11 | /// Inside PD2, there is the dsl::BoneMapping class. This is used for some unknown purpose,
12 | /// however what is known is that it builds a list of matrices. These are referred to by
13 | /// indexes into a runtime table built by SkinBones (Bones::matrices).
14 | ///
15 | /// This runtime table is built by multiplying together the world transform and global skin
16 | /// transform onto each SkinBones matrix. This is done in C#, loaded into the SkinPositions
17 | /// list in SkinBones.
18 | ///
19 | /// Each bone mapping corresponds to a RenderAtom.
20 | ///
21 | /// Note to self: Setting this directly after the invocation of BoneMapping::setup_matrix_sets
22 | /// will null out the first matrix in the first set.
23 | /// set *(void**)( **(void***)((char*)($rbx + 0x20) + 24) ) = 0
24 | /// And that didn't cause any crashes for me unfortunately, which would give a stacktrace to
25 | /// where it's used.
26 | ///
27 | class BoneMappingItem
28 | {
29 | public readonly List bones = new List();
30 |
31 | public override string ToString()
32 | {
33 | string verts_string = (bones.Count == 0 ? "none" : "");
34 |
35 | foreach (UInt32 vert in bones)
36 | {
37 | verts_string += vert + ", ";
38 | }
39 |
40 | return "count: " + bones.Count + " verts: [" + verts_string + "]";
41 | }
42 | }
43 |
44 | ///
45 | /// Represents dsl::Bones, an abstract base class inside PD2.
46 | ///
47 | ///
48 | ///
49 | /// See the dumped vtables:
50 | /// https://raw.githubusercontent.com/blt4linux/blt4l/master/doc/payday2_vtables
51 | /// Note it has pure virtual methods.
52 | ///
53 | /// As an abstract class, it can never be found by itself, only embedded within
54 | /// SkinBones (it's only known - and likely only - subclass).
55 | ///
56 | [ModelFileSection(Tags.bones_tag)]
57 | class Bones : AbstractSection, ISection
58 | {
59 | public UInt32 size;
60 |
61 | public List bone_mappings { get; private set; } = new List();
62 |
63 | public byte[] remaining_data = null;
64 |
65 | internal Bones()
66 | {
67 | }
68 |
69 | public Bones(BinaryReader instream, SectionHeader section) : this(instream)
70 | {
71 | Log.Default.Warn("Model contains a Bones that isn't a SkinBones!");
72 | this.SectionId = section.id;
73 | this.size = section.size;
74 |
75 | if ((section.offset + 12 + section.size) > instream.BaseStream.Position)
76 | remaining_data =
77 | instream.ReadBytes((int) ((section.offset + 12 + section.size) - instream.BaseStream.Position));
78 | }
79 |
80 | public Bones(BinaryReader instream)
81 | {
82 | uint count = instream.ReadUInt32();
83 |
84 | for (int x = 0; x < count; x++)
85 | {
86 | BoneMappingItem bone_mapping_item = new BoneMappingItem();
87 | uint bone_count = instream.ReadUInt32();
88 | for (int y = 0; y < bone_count; y++)
89 | bone_mapping_item.bones.Add(instream.ReadUInt32());
90 | bone_mappings.Add(bone_mapping_item);
91 | }
92 |
93 | this.remaining_data = null;
94 | }
95 |
96 | public override void StreamWriteData(BinaryWriter outstream)
97 | {
98 | outstream.Write(bone_mappings.Count);
99 | foreach (BoneMappingItem bone in this.bone_mappings)
100 | {
101 | outstream.Write(bone.bones.Count);
102 | foreach (UInt32 vert in bone.bones)
103 | outstream.Write(vert);
104 | }
105 |
106 | if (this.remaining_data != null)
107 | outstream.Write(this.remaining_data);
108 | }
109 |
110 | public override string ToString()
111 | {
112 | string bones_string = (bone_mappings.Count == 0 ? "none" : "");
113 |
114 | foreach (BoneMappingItem bone in bone_mappings)
115 | {
116 | bones_string += bone + ", ";
117 | }
118 |
119 | return base.ToString() + " size: " + this.size + " bones:[ " +
120 | bones_string + " ]" + (this.remaining_data != null
121 | ? " REMAINING DATA! " + this.remaining_data.Length + " bytes"
122 | : "");
123 | }
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/PD2ModelParser/FullModelData.cs:
--------------------------------------------------------------------------------
1 | using PD2ModelParser.Sections;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Text.RegularExpressions;
6 |
7 | namespace PD2ModelParser
8 | {
9 | public class FullModelData
10 | {
11 | public List sections = new List();
12 | public Dictionary parsed_sections = new Dictionary();
13 | public byte[] leftover_data = null;
14 |
15 | ///
16 | /// Adds a section to the model data.
17 | ///
18 | ///
19 | /// This sets the Section ID of the passed object.
20 | ///
21 | /// The section to add
22 | public void AddSection(ISection obj)
23 | {
24 | // Shouldn't add twice
25 | if(parsed_sections.ContainsValue(obj)) { return; }
26 |
27 | // Objects with no ID already start at 10001. There's no real reason for this
28 | // but we have to start somewhere and this is smaller than what overkill uses.
29 | uint id = obj.SectionId != 0 ? obj.SectionId : 10001;
30 |
31 | // Find the first unused ID
32 | while (parsed_sections.ContainsKey(id))
33 | id++;
34 |
35 | // Set the object's ID
36 | obj.SectionId = id;
37 |
38 | // Load the new section into the parsed_sections dictionary
39 | parsed_sections[id] = obj;
40 |
41 | // And create a header for it
42 | SectionHeader header = new SectionHeader(id) {type = obj.TypeCode};
43 | sections.Add(header);
44 | }
45 |
46 | public void RemoveSection(uint id)
47 | {
48 | SectionHeader header = sections.Find(s => s.id == id);
49 | if (header == null)
50 | throw new ArgumentException("Cannot remove missing header", nameof(id));
51 |
52 | if (!parsed_sections.ContainsKey(id))
53 | throw new ArgumentException("Cannot remove unparsed header", nameof(id));
54 |
55 | parsed_sections.Remove(id);
56 | sections.Remove(header);
57 | }
58 |
59 | public void RemoveSection(ISection section) => RemoveSection(section.SectionId);
60 |
61 | public Object3D GetObject3DByHash(HashName hashName) {
62 | foreach (Object3D object3D in SectionsOfType()) {
63 | if (hashName.Hash == object3D.HashName.Hash) {
64 | return object3D;
65 | }
66 | }
67 |
68 | return null;
69 | }
70 |
71 | public IEnumerable SectionsOfType() where T : class
72 | {
73 | return parsed_sections.Where(i => i.Value is T).Select(i => i.Value as T);
74 | }
75 |
76 | ///
77 | /// Enforces that sections with a have unique names.
78 | ///
79 | public void UniquifyNames()
80 | {
81 | // The trick we use here is that since AbstractSection is abstract, we can group objects
82 | // by their highest non-abstract base class and be pretty much right. The worst that can
83 | // happen is a too-wide namespace anyway, and that won't hurt much.
84 |
85 | var seenNamesOverall = new Dictionary>();
86 |
87 | foreach(var sec in SectionsOfType())
88 | {
89 | var st = sec.GetType();
90 | while (!st.BaseType.IsAbstract) { st = st.BaseType; }
91 |
92 | if(!seenNamesOverall.TryGetValue(st, out var seenNames))
93 | {
94 | seenNames = seenNamesOverall[st] = new HashSet();
95 | }
96 |
97 | var candidateName = sec.HashName;
98 |
99 | while (seenNames.Contains(candidateName.Hash))
100 | {
101 | if(!candidateName.Known)
102 | {
103 | candidateName = new HashName(candidateName.Hash++);
104 | }
105 | else
106 | {
107 | var m = Regex.Match(candidateName.String, @"\.(\d+)$");
108 | if(m == null)
109 | {
110 | candidateName = new HashName(candidateName + ".001");
111 | }
112 | else
113 | {
114 | var count = int.Parse(m.Groups[1].Value);
115 | count++;
116 | var baseName = candidateName.String.Substring(0, candidateName.String.Length - m.Length);
117 | candidateName = new HashName(string.Format("{0}.{1:D3}", baseName, count));
118 | }
119 | }
120 | }
121 | }
122 | }
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/PD2ModelParser/UI/ExportPanel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Data;
4 | using System.Drawing;
5 | using System.Linq;
6 | using System.Windows.Forms;
7 | using System.Windows.Forms.Layout;
8 | using PD2ModelParser.Modelscript;
9 |
10 | namespace PD2ModelParser.UI
11 | {
12 | public partial class ExportPanel : UserControl
13 | {
14 | private FullModelData model;
15 | private ExportLayoutEngine layout = new ExportLayoutEngine();
16 | public override LayoutEngine LayoutEngine => layout;
17 |
18 | public ExportPanel()
19 | {
20 | InitializeComponent();
21 |
22 | formatBox.BeginUpdate();
23 |
24 | // Fill in the actual list of exporters
25 | formatBox.Items.Clear();
26 | formatBox.Items.AddRange(FileTypeInfo.Types.Where(i => i.CanExport).ToArray());
27 | formatBox.DisplayMember = nameof(FileTypeInfo.FormatName);
28 | // Select the default item, since for whatever reason we can't
29 | // do that in the designer.
30 | formatBox.SelectedIndex = 0;
31 |
32 | formatBox.EndUpdate();
33 |
34 | }
35 |
36 | private void inputFileBox_FileSelected(object sender, EventArgs e)
37 | {
38 | if (inputFileBox.Selected == null)
39 | {
40 | exportBttn.Enabled = false;
41 | return;
42 | }
43 | exportBttn.Enabled = true;
44 | }
45 |
46 | private void exportBttn_Click(object sender, EventArgs e)
47 | {
48 | var script = new List();
49 |
50 | script.Add(new LoadModel() { File = inputFileBox.Selected });
51 | model = ModelReader.Open(inputFileBox.Selected);
52 |
53 | var exportCmd = new Export();
54 |
55 | var type = formatBox.SelectedItem as FileTypeInfo;
56 | if(type == null)
57 | {
58 | MessageBox.Show("Unknown format '{format}'");
59 | return;
60 | }
61 | var outName = System.IO.Path.ChangeExtension(inputFileBox.Selected, type.Extension);
62 | exportCmd.File = outName;
63 | script.Add(exportCmd);
64 | Script.ExecuteItems(script, System.IO.Directory.GetCurrentDirectory());
65 |
66 | MessageBox.Show($"Successfully exported model {inputFileBox.Selected.Split('\\').Last()} (placed in the input model folder)");
67 | }
68 |
69 | class ExportLayoutEngine : LayoutEngine
70 | {
71 |
72 | public override bool Layout(object sender, LayoutEventArgs e)
73 | {
74 | var panel = (ExportPanel)sender;
75 |
76 | var labels = new Label[] {
77 | panel.label1,
78 | panel.label2
79 | };
80 |
81 | var fields = new Control[]
82 | {
83 | panel.inputFileBox,
84 | panel.formatBox
85 | };
86 |
87 | var maxLabelWidth = labels
88 | .Where(i => i != null)
89 | .Select(i => i.PreferredSize.Width + i.Margin.Horizontal).Max();
90 | var currY = 0;
91 |
92 | for (var i = 0; i < fields.Length; i++)
93 | {
94 | var label = labels[i];
95 | var labelSize = label?.GetPreferredSize(new Size(1, 1)) ?? new Size(0, 0);
96 | var field = fields[i];
97 | var fieldSize = field.GetPreferredSize(new Size(1, 1));
98 |
99 | var rowHeight = Math.Max(labelSize.Height + (label?.Margin.Vertical ?? 0), fieldSize.Height + field.Margin.Vertical);
100 |
101 | if (label != null)
102 | {
103 | var labelOffsY = (rowHeight - labelSize.Height) / 2;
104 | label.SetBounds(maxLabelWidth - (labelSize.Width + label.Margin.Right), currY + labelOffsY, labelSize.Width, labelSize.Height);
105 | }
106 |
107 | var fieldX = maxLabelWidth + field.Margin.Left;
108 | var fieldOffsY = currY + (rowHeight - fieldSize.Height) / 2;
109 | var fieldWidth = panel.Width - (maxLabelWidth + field.Margin.Horizontal);
110 | field.SetBounds(fieldX, fieldOffsY, fieldWidth, fieldSize.Height);
111 | currY += rowHeight;
112 | }
113 |
114 | var buttonSize = panel.exportBttn.GetPreferredSize(new Size(1, 1));
115 | panel.exportBttn.SetBounds(panel.exportBttn.Margin.Left, currY + panel.exportBttn.Margin.Top, panel.Width - panel.exportBttn.Margin.Horizontal, buttonSize.Height);
116 | currY += panel.exportBttn.Bounds.Bottom + panel.exportBttn.Margin.Bottom;
117 |
118 | //Console.WriteLine("End Layout");
119 |
120 | panel.MinimumSize = new Size(panel.MinimumSize.Width, currY);
121 | return true;
122 | }
123 | }
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/PD2ModelParser/UI/ExportPanel.Designer.cs:
--------------------------------------------------------------------------------
1 | namespace PD2ModelParser.UI
2 | {
3 | partial class ExportPanel
4 | {
5 | ///
6 | /// Required designer variable.
7 | ///
8 | private System.ComponentModel.IContainer components = null;
9 |
10 | ///
11 | /// Clean up any resources being used.
12 | ///
13 | /// true if managed resources should be disposed; otherwise, false.
14 | protected override void Dispose(bool disposing)
15 | {
16 | if (disposing && (components != null))
17 | {
18 | components.Dispose();
19 | }
20 | base.Dispose(disposing);
21 | }
22 |
23 | #region Component Designer generated code
24 |
25 | ///
26 | /// Required method for Designer support - do not modify
27 | /// the contents of this method with the code editor.
28 | ///
29 | private void InitializeComponent()
30 | {
31 | this.exportBttn = new System.Windows.Forms.Button();
32 | this.label1 = new System.Windows.Forms.Label();
33 | this.label2 = new System.Windows.Forms.Label();
34 | this.formatBox = new System.Windows.Forms.ComboBox();
35 | this.inputFileBox = new PD2ModelParser.UI.FileBrowserControl();
36 | this.SuspendLayout();
37 | //
38 | // exportBttn
39 | //
40 | this.exportBttn.Enabled = false;
41 | this.exportBttn.Location = new System.Drawing.Point(6, 63);
42 | this.exportBttn.Name = "exportBttn";
43 | this.exportBttn.Size = new System.Drawing.Size(274, 23);
44 | this.exportBttn.TabIndex = 17;
45 | this.exportBttn.Text = "Convert";
46 | this.exportBttn.UseVisualStyleBackColor = true;
47 | this.exportBttn.Click += new System.EventHandler(this.exportBttn_Click);
48 | //
49 | // label1
50 | //
51 | this.label1.AutoSize = true;
52 | this.label1.Location = new System.Drawing.Point(3, 12);
53 | this.label1.Name = "label1";
54 | this.label1.Size = new System.Drawing.Size(53, 13);
55 | this.label1.TabIndex = 14;
56 | this.label1.Text = "Input File:";
57 | //
58 | // label2
59 | //
60 | this.label2.AutoSize = true;
61 | this.label2.Location = new System.Drawing.Point(5, 39);
62 | this.label2.Name = "label2";
63 | this.label2.Size = new System.Drawing.Size(42, 13);
64 | this.label2.TabIndex = 18;
65 | this.label2.Text = "Format:";
66 | //
67 | // formatBox
68 | //
69 | this.formatBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
70 | | System.Windows.Forms.AnchorStyles.Right)));
71 | this.formatBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
72 | this.formatBox.FormattingEnabled = true;
73 | this.formatBox.Items.AddRange(new object[] {
74 | "If you can see this at runtime, something strange has happened."});
75 | this.formatBox.Location = new System.Drawing.Point(70, 36);
76 | this.formatBox.Name = "formatBox";
77 | this.formatBox.Size = new System.Drawing.Size(353, 21);
78 | this.formatBox.TabIndex = 19;
79 | //
80 | // inputFileBox
81 | //
82 | this.inputFileBox.AllowDrop = true;
83 | this.inputFileBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
84 | | System.Windows.Forms.AnchorStyles.Right)));
85 | this.inputFileBox.Filter = "Diesel Model(*.model)|*.model";
86 | this.inputFileBox.Location = new System.Drawing.Point(70, 7);
87 | this.inputFileBox.Name = "inputFileBox";
88 | this.inputFileBox.SaveMode = false;
89 | this.inputFileBox.Size = new System.Drawing.Size(353, 23);
90 | this.inputFileBox.TabIndex = 20;
91 | this.inputFileBox.FileSelected += new System.EventHandler(this.inputFileBox_FileSelected);
92 | //
93 | // ExportPanel
94 | //
95 | this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
96 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
97 | this.Controls.Add(this.inputFileBox);
98 | this.Controls.Add(this.label2);
99 | this.Controls.Add(this.formatBox);
100 | this.Controls.Add(this.label1);
101 | this.Controls.Add(this.exportBttn);
102 | this.Name = "ExportPanel";
103 | this.Size = new System.Drawing.Size(426, 189);
104 | this.ResumeLayout(false);
105 | this.PerformLayout();
106 |
107 | }
108 |
109 | #endregion
110 | private System.Windows.Forms.Button exportBttn;
111 | private System.Windows.Forms.Label label1;
112 | private System.Windows.Forms.Label label2;
113 | private System.Windows.Forms.ComboBox formatBox;
114 | private FileBrowserControl inputFileBox;
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/PD2ModelParser/Filetype.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using PD2ModelParser.Importers;
5 |
6 | namespace PD2ModelParser
7 | {
8 | public abstract class FileTypeInfo
9 | {
10 | public abstract string Extension { get; }
11 | public abstract string Name { get; }
12 | public virtual string FormatName => $"{Name} (.{Extension})";
13 | public abstract bool CanExport { get; }
14 | public abstract bool CanImport { get; }
15 | public abstract string Export(FullModelData data, string path);
16 | public abstract void Import(FullModelData data, string path, bool createModels, Func parentFinder, IOptionReceiver options);
17 | public virtual IOptionReceiver CreateOptionReceiver() => new GenericOptionReceiver();
18 | public override string ToString() => Extension.ToUpper();
19 |
20 | public static bool TryParseName(string name, out FileTypeInfo result)
21 | {
22 | if(name == null) { result = null; return false; }
23 | var ident = name.ToLower().TrimStart('.');
24 | result = Types.FirstOrDefault(i => i.Extension == ident || i.Name == ident);
25 | return result != null;
26 | }
27 |
28 | public static FileTypeInfo ParseName(string name)
29 | {
30 | if (TryParseName(name, out var result)) { return result; }
31 | else throw new Exception($"'{name}' is not a recognised filetype");
32 | }
33 |
34 | public static bool TryParseFromExtension(string path, out FileTypeInfo result)
35 | {
36 | return TryParseName(System.IO.Path.GetExtension(path), out result);
37 | }
38 |
39 | class ObjType : FileTypeInfo
40 | {
41 | public override string Extension => "obj";
42 | public override string Name => "Object";
43 | public override bool CanExport => true;
44 | public override bool CanImport => true;
45 | public override void Import(FullModelData data, string path, bool createModels, Func parentFinder, IOptionReceiver options)
46 | => NewObjImporter.ImportNewObj(data, path, createModels, parentFinder, options);
47 | public override string Export(FullModelData data, string path) => Exporters.ObjWriter.ExportFile(data, path);
48 | }
49 | public static readonly FileTypeInfo Obj = new ObjType();
50 |
51 | class DaeType : FileTypeInfo
52 | {
53 | public override string Extension => "dae";
54 | public override string Name => "Collada";
55 | public override bool CanExport => true;
56 | public override bool CanImport => false;
57 | public override void Import(FullModelData data, string path, bool createModels, Func parentFinder, IOptionReceiver options)
58 | => throw new Exception("Importing DAE files is not supported.");
59 | public override string Export(FullModelData data, string path) => ColladaExporter.ExportFile(data, path);
60 | }
61 | public static readonly FileTypeInfo Dae = new DaeType();
62 |
63 | class GltfType : FileTypeInfo
64 | {
65 | public override string Extension => "gltf";
66 | public override string Name => "glTF Separate Files";
67 | public override bool CanExport => true;
68 | public override bool CanImport => true;
69 | public override void Import(FullModelData data, string path, bool createModels, Func parentFinder, IOptionReceiver options)
70 | => GltfImporter.Import(data, path, createModels, parentFinder, options);
71 | public override string Export(FullModelData data, string path)
72 | => Exporters.GltfExporter.ExportFile(data, path, false);
73 | }
74 | public static readonly FileTypeInfo Gltf = new GltfType();
75 |
76 | class GlbType : GltfType
77 | {
78 | public override string Extension => "glb";
79 | public override string Name => "glTF Binary";
80 | public override string Export(FullModelData data, string path)
81 | => Exporters.GltfExporter.ExportFile(data, path, true);
82 | }
83 | public static readonly FileTypeInfo Glb = new GlbType();
84 |
85 | class AnimationType : FileTypeInfo
86 | {
87 | public override string Extension => "animation";
88 | public override string Name => "Animation";
89 | public override bool CanExport => true;
90 | public override bool CanImport => false;
91 | public override void Import(FullModelData data, string path, bool createModels, Func parentFinder, IOptionReceiver options)
92 | => throw new Exception("Please import animation files in the animation section.");
93 | public override string Export(FullModelData data, string path) => Exporters.AnimationExporter.ExportFile(data, path);
94 | }
95 | public static readonly FileTypeInfo Animation = new AnimationType();
96 |
97 | public static IReadOnlyList Types { get; } = new List() {
98 | FileTypeInfo.Dae,
99 | FileTypeInfo.Obj,
100 | FileTypeInfo.Gltf,
101 | FileTypeInfo.Glb,
102 | FileTypeInfo.Animation
103 | };
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/PD2ModelParser/Misc/ZLib/ZLibHeader.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace PD2ModelParser.Misc.ZLib
8 | {
9 | public enum FLevel
10 | {
11 | Faster = 0,
12 | Fast = 1,
13 | Default = 2,
14 | Optimal = 3,
15 | }
16 | public sealed class ZLibHeader
17 | {
18 | #region "Variables globales"
19 | private bool mIsSupportedZLibStream;
20 | private byte mCompressionMethod; //CMF 0-3
21 | private byte mCompressionInfo; //CMF 4-7
22 | private byte mFCheck; //Flag 0-4 (Check bits for CMF and FLG)
23 | private bool mFDict; //Flag 5 (Preset dictionary)
24 | private FLevel mFLevel; //Flag 6-7 (Compression level)
25 | #endregion
26 | #region "Propiedades"
27 | public bool IsSupportedZLibStream
28 | {
29 | get
30 | {
31 | return this.mIsSupportedZLibStream;
32 | }
33 | set
34 | {
35 | this.mIsSupportedZLibStream = value;
36 | }
37 | }
38 | public byte CompressionMethod
39 | {
40 | get
41 | {
42 | return this.mCompressionMethod;
43 | }
44 | set
45 | {
46 | if (value > 15)
47 | {
48 | throw new ArgumentOutOfRangeException("Argument cannot be greater than 15");
49 | }
50 | this.mCompressionMethod = value;
51 | }
52 | }
53 | public byte CompressionInfo
54 | {
55 | get
56 | {
57 | return this.mCompressionInfo;
58 | }
59 | set
60 | {
61 | if (value > 15)
62 | {
63 | throw new ArgumentOutOfRangeException("Argument cannot be greater than 15");
64 | }
65 | this.mCompressionInfo = value;
66 | }
67 | }
68 | public byte FCheck
69 | {
70 | get
71 | {
72 | return this.mFCheck;
73 | }
74 | set
75 | {
76 | if (value > 31)
77 | {
78 | throw new ArgumentOutOfRangeException("Argument cannot be greater than 31");
79 | }
80 | this.mFCheck = value;
81 | }
82 | }
83 | public bool FDict
84 | {
85 | get
86 | {
87 | return this.mFDict;
88 | }
89 | set
90 | {
91 | this.mFDict = value;
92 | }
93 | }
94 | public FLevel FLevel
95 | {
96 | get
97 | {
98 | return this.mFLevel;
99 | }
100 | set
101 | {
102 | this.mFLevel = value;
103 | }
104 | }
105 | #endregion
106 | #region "Constructor"
107 | public ZLibHeader()
108 | {
109 |
110 | }
111 | #endregion
112 | #region "Metodos privados"
113 | private void RefreshFCheck()
114 | {
115 | byte byteFLG = 0x00;
116 |
117 | byteFLG = (byte)(Convert.ToByte(this.FLevel) << 1);
118 | byteFLG |= Convert.ToByte(this.FDict);
119 |
120 | this.FCheck = Convert.ToByte(31 - Convert.ToByte((this.GetCMF() * 256 + byteFLG) % 31));
121 | }
122 | private byte GetCMF()
123 | {
124 | byte byteCMF = 0x00;
125 |
126 | byteCMF = (byte)(this.CompressionInfo << 4);
127 | byteCMF |= (byte)(this.CompressionMethod);
128 |
129 | return byteCMF;
130 | }
131 | private byte GetFLG()
132 | {
133 | byte byteFLG = 0x00;
134 |
135 | byteFLG = (byte)(Convert.ToByte(this.FLevel) << 6);
136 | byteFLG |= (byte)(Convert.ToByte(this.FDict) << 5);
137 | byteFLG |= this.FCheck;
138 |
139 | return byteFLG;
140 | }
141 | #endregion
142 | #region "Metodos publicos"
143 | public byte[] EncodeZlibHeader()
144 | {
145 | byte[] result = new byte[2];
146 |
147 | this.RefreshFCheck();
148 |
149 | result[0] = this.GetCMF();
150 | result[1] = this.GetFLG();
151 |
152 | return result;
153 | }
154 | #endregion
155 | #region "Metodos estáticos"
156 | public static ZLibHeader DecodeHeader(int pCMF, int pFlag)
157 | {
158 | ZLibHeader result = new ZLibHeader();
159 |
160 | //Ensure that parameters are bytes
161 | pCMF = pCMF & 0x0FF;
162 | pFlag = pFlag & 0x0FF;
163 |
164 | //Decode bytes
165 | result.CompressionInfo = Convert.ToByte((pCMF & 0xF0) >> 4);
166 | result.CompressionMethod = Convert.ToByte(pCMF & 0x0F);
167 |
168 | result.FCheck = Convert.ToByte(pFlag & 0x1F);
169 | result.FDict = Convert.ToBoolean(Convert.ToByte((pFlag & 0x20) >> 5));
170 | result.FLevel = (FLevel)Convert.ToByte((pFlag & 0xC0) >> 6);
171 |
172 | result.IsSupportedZLibStream = (result.CompressionMethod == 8) && (result.CompressionInfo == 7) && (((pCMF * 256 + pFlag) % 31 == 0)) && (result.FDict == false);
173 |
174 | return result;
175 | }
176 | #endregion
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/PD2ModelParser/Exporters/DieselExporter.cs:
--------------------------------------------------------------------------------
1 | using PD2ModelParser.Sections;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.IO;
5 | using System.Linq;
6 |
7 | namespace PD2ModelParser.Exporters
8 | {
9 | static class DieselExporter
10 | {
11 | public static void ExportFile(FullModelData data, string path)
12 | {
13 | //you remove items from the parsed_sections
14 | //you edit items in the parsed_sections, they will get read and exported
15 |
16 | //Sort the sections
17 | List animation_sections = new List();
18 | List author_sections = new List();
19 | List material_sections = new List();
20 | List object3D_sections = new List();
21 | List model_sections = new List();
22 | List other_sections = new List();
23 |
24 | // Discard the old hashlist
25 | // Note that we use ToArray, which allows us to mutate the list without breaking anything
26 | foreach (SectionHeader header in data.sections.ToArray())
27 | if (header.type == Tags.custom_hashlist_tag)
28 | data.RemoveSection(header.id);
29 |
30 | CustomHashlist hashlist = new CustomHashlist();
31 | data.AddSection(hashlist);
32 |
33 | foreach (SectionHeader sectionheader in data.sections)
34 | {
35 | if (!data.parsed_sections.Keys.Contains(sectionheader.id))
36 | {
37 | Log.Default.Warn($"BUG: SectionHeader with id {sectionheader.id} has no counterpart in parsed_sections");
38 | continue;
39 | }
40 |
41 | var section = data.parsed_sections[sectionheader.id];
42 |
43 | if (section is Animation)
44 | {
45 | animation_sections.Add(section as Animation);
46 | }
47 | else if (section is Author)
48 | {
49 | author_sections.Add(section as Author);
50 | }
51 | else if (section is MaterialGroup)
52 | {
53 | foreach(var matsec in (section as MaterialGroup).Items)
54 | {
55 | if (!data.parsed_sections.ContainsKey(matsec.SectionId))
56 | throw new Exception($"BUG: In MaterialGroup {section.SectionId}, Material {matsec.SectionId} isn't registered as part of the .model we're saving");
57 | if(!material_sections.Contains(matsec))
58 | {
59 | material_sections.Add(matsec);
60 | }
61 | }
62 | material_sections.Add(section);
63 | }
64 | else if (section is Model) // Has to be before Object3D, since it's a subclass.
65 | {
66 | model_sections.Add(section as Model);
67 | }
68 | else if (section is Object3D)
69 | {
70 | object3D_sections.Add(section as Object3D);
71 | }
72 | else if (section != null )
73 | {
74 | other_sections.Add(section as ISection);
75 | }
76 | else
77 | {
78 | Log.Default.Warn("BUG: Somehow a null or non-section found its way into the list of sections.");
79 | }
80 |
81 | if (section is IHashContainer container)
82 | {
83 | container.CollectHashes(hashlist);
84 | }
85 | }
86 |
87 | var sections_to_write = Enumerable.Empty()
88 | .Concat(animation_sections)
89 | .Concat(author_sections)
90 | .Concat(material_sections)
91 | .Concat(object3D_sections)
92 | .Concat(model_sections)
93 | .Concat(other_sections)
94 | .OrderedDistinct()
95 | .ToList();
96 |
97 | //after each section, you go back and enter it's new size
98 | using (FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write))
99 | {
100 | using (BinaryWriter bw = new BinaryWriter(fs))
101 | {
102 |
103 | bw.Write(-1); //the - (yyyy)
104 | bw.Write((UInt32)100); //Filesize (GO BACK AT END AND CHANGE!!!)
105 | int sectionCount = data.sections.Count;
106 | bw.Write(sectionCount); //Sections count
107 |
108 | foreach (var sec in sections_to_write)
109 | {
110 | sec.StreamWrite(bw);
111 | }
112 |
113 | if(sections_to_write.Count != sectionCount)
114 | {
115 | Log.Default.Warn($"BUG : There were {sectionCount} sections to write but {sections_to_write.Count} were written");
116 | }
117 |
118 | if (data.leftover_data != null)
119 | bw.Write(data.leftover_data);
120 |
121 | fs.Position = 4;
122 | bw.Write((UInt32)fs.Length);
123 |
124 | }
125 | }
126 | }
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/PD2ModelParser/Importers/ModelReader.cs:
--------------------------------------------------------------------------------
1 |
2 | using System;
3 | using System.Collections.Generic;
4 | using System.IO;
5 |
6 | using PD2ModelParser.Sections;
7 |
8 | namespace PD2ModelParser
9 | {
10 | static class ModelReader
11 | {
12 | public static FullModelData Open(string filepath)
13 | {
14 | FullModelData data = new FullModelData();
15 |
16 | StaticStorage.hashindex.Load();
17 |
18 | Log.Default.Info("Opening Model: {0}", filepath);
19 |
20 | Read(data, filepath);
21 |
22 | return data;
23 | }
24 |
25 | public delegate void SectionVisitor(BinaryReader file, SectionHeader section);
26 |
27 | ///
28 | /// Iterate over each part of the model file, and letting the caller handle them.
29 | ///
30 | /// This allows much faster reading of a file if you're only interested in one
31 | /// specific part of it.
32 | ///
33 | /// The name of the file to open
34 | public static void VisitModel(string filepath, SectionVisitor visitor)
35 | {
36 | using (FileStream fs = new FileStream(filepath, FileMode.Open, FileAccess.Read))
37 | {
38 | using (BinaryReader br = new BinaryReader(fs))
39 | {
40 | List headers = ReadHeaders(br);
41 |
42 | foreach (SectionHeader header in headers)
43 | {
44 | fs.Position = header.Start;
45 | visitor(br, header);
46 | }
47 | }
48 | }
49 | }
50 |
51 | ///
52 | /// Reads all the section headers from a model file.
53 | ///
54 | /// Note this leaves the file's position just after the end of the last section.
55 | ///
56 | /// The input source
57 | /// The list of section headers
58 | public static List ReadHeaders(BinaryReader br)
59 | {
60 | int random = br.ReadInt32();
61 | int filesize = br.ReadInt32();
62 | int sectionCount;
63 | if (random == -1)
64 | {
65 | sectionCount = br.ReadInt32();
66 | }
67 | else
68 | sectionCount = random;
69 |
70 | Log.Default.Debug("Size: {0} bytes, Sections: {1},{2}", filesize, sectionCount, br.BaseStream.Position);
71 |
72 | List sections = new List();
73 |
74 | for (int x = 0; x < sectionCount; x++)
75 | {
76 | SectionHeader sectionHead = new SectionHeader(br);
77 | sections.Add(sectionHead);
78 | Log.Default.Debug("Section: {0}", sectionHead);
79 |
80 | Log.Default.Debug("Next offset: {0}", sectionHead.End);
81 | br.BaseStream.Position = sectionHead.End;
82 | }
83 |
84 | return sections;
85 | }
86 |
87 | private static void Read(FullModelData data, string filepath)
88 | {
89 | List sections = data.sections;
90 | Dictionary parsed_sections = data.parsed_sections;
91 |
92 | byte[] bytes;
93 |
94 | using (FileStream fs = new FileStream(filepath, FileMode.Open, FileAccess.Read))
95 | {
96 | bytes = new byte[fs.Length];
97 | int res = fs.Read(bytes, 0, (int)fs.Length);
98 | if (res != fs.Length)
99 | throw new Exception($"Failed to read {filepath} all in one go!");
100 | }
101 |
102 | using (var ms = new MemoryStream(bytes, 0, bytes.Length, false, true))
103 | using (var br = new BinaryReader(ms))
104 | {
105 | sections.Clear();
106 | sections.AddRange(ReadHeaders(br));
107 |
108 | foreach (SectionHeader sh in sections)
109 | {
110 | ISection section;
111 |
112 | ms.Position = sh.Start;
113 |
114 | if (SectionMetaInfo.TryGetForTag(sh.type, out var mi))
115 | {
116 | section = mi.Deserialise(br, sh);
117 | }
118 | else
119 | {
120 | Log.Default.Warn("UNKNOWN Tag {2} at {0} Size: {1}", sh.offset, sh.size, sh.type);
121 | ms.Position = sh.offset;
122 |
123 | section = new Unknown(br, sh);
124 | }
125 |
126 | if (ms.Position != sh.End)
127 | {
128 | //throw new Exception(string.Format("Section of type {2} {0} read more than its length of {1} ", sh.id, sh.size, sh.type));
129 | Log.Default.Warn("Section {0} (type {2:X}) was too short ({1} bytes read)", sh.id, sh.size, sh.type);
130 | }
131 |
132 | Log.Default.Debug("Section {0} at {1} length {2}",
133 | section.GetType().Name, sh.offset, sh.size);
134 |
135 | parsed_sections.Add(sh.id, section);
136 | }
137 |
138 | foreach (var i in parsed_sections)
139 | {
140 | if (i.Value is IPostLoadable pl)
141 | {
142 | pl.PostLoad(i.Key, parsed_sections);
143 | }
144 | }
145 |
146 | if (ms.Position < ms.Length)
147 | data.leftover_data = br.ReadBytes((int)(ms.Length - ms.Position));
148 | }
149 | }
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/PD2ModelParser/UI/FileBrowserControl.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
--------------------------------------------------------------------------------
/PD2ModelParser/UI/ExportPanel.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
--------------------------------------------------------------------------------
/PD2ModelParser/UI/ImportPanel.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
--------------------------------------------------------------------------------
/PD2ModelParser/Properties/Resources.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
--------------------------------------------------------------------------------
/PD2ModelParser/UI/Form1.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
121 | 17, 17
122 |
123 |
--------------------------------------------------------------------------------
/PD2ModelParser/Tests/MultiplicationTest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Numerics;
4 | using System.Reflection;
5 | using System.Text.RegularExpressions;
6 | using NUnit.Framework;
7 |
8 | namespace PD2ModelParser.Tests
9 | {
10 | [TestFixture]
11 | public class MultiplicationTest
12 | {
13 | private static Matrix4x4 MatrixDecode(string str)
14 | {
15 | // str is a byte-for-byte dump of the Matrix3D structure, a C float[4][4] struct
16 | // a float is four bytes, so that's 64 bytes. With 2 characters per byte, that's 128 chars.
17 | Assert.AreEqual((4 * 4) * 4 * 2, str.Length);
18 |
19 | Matrix4x4 mat = new Matrix4x4();
20 |
21 | byte[] data = new byte[64];
22 | for (int i = 0; i < data.Length; i++)
23 | {
24 | data[i] = Convert.ToByte(str.Substring(i * 2, 2), 16);
25 | }
26 |
27 | for (int i = 0; i < 16; i++)
28 | {
29 | mat.Index(i) = BitConverter.ToSingle(data, i * 4);
30 | }
31 |
32 | return mat;
33 | }
34 |
35 | private static void TestRoundedEquals(Matrix4x4 expect, Matrix4x4 test, float err)
36 | {
37 | for (int i = 0; i < 16; i++)
38 | {
39 | float diff = Math.Abs(expect.Index(i) - test.Index(i));
40 | if (diff > err)
41 | Assert.Fail("Matrix value mismatch: {0} vs {1}", expect.Index(i), test.Index(i));
42 | }
43 | }
44 |
45 | [Test]
46 | public void TestIdentityMultiplication()
47 | {
48 | Matrix4x4 base_ = new Matrix4x4(
49 | -1, 1.22465e-16f, 0, 0,
50 | -6.16298e-32f, -4.44089e-16f, 1, 0,
51 | 1.22465e-16f, 1, 4.44089e-16f, 0,
52 | 1.31163e-14f, 5.32948f, 101.777f, 1
53 | );
54 |
55 | Matrix4x4 result = base_.MultDiesel(Matrix4x4.Identity);
56 |
57 | Assert.AreEqual(base_, result);
58 | }
59 |
60 | [Test]
61 | public void TestSimpleMultiplication()
62 | {
63 | // vec11
64 | Matrix4x4 base_ = new Matrix4x4(
65 | 0.644716f, -0.205625f, 0.736247f, 0,
66 | 0.135465f, 0.978631f, 0.154697f, 0,
67 | -0.752323f, 4.72831e-09f, 0.658794f, 0,
68 | 4.06338f, -1.85149f, 2.32434f, 1
69 | );
70 |
71 | Matrix4x4 arg = new Matrix4x4(
72 | 0.602691f, 0.747197f, -0.280109f, 0,
73 | -0.538034f, 0.639738f, 0.548867f, 0,
74 | 0.589308f, -0.180089f, 0.787581f, 0,
75 | -21.0668f, 36.5858f, 120.295f, 1
76 | );
77 |
78 | Matrix4x4 target = new Matrix4x4(
79 | 0.933074f, 0.217594f, 0.286403f, 0,
80 | -0.353729f, 0.699427f, 0.621029f, 0,
81 | -0.0651858f, -0.680775f, 0.729586f, 0,
82 | -16.2519f, 38.0189f, 119.972f, 1
83 | );
84 |
85 | Matrix4x4 result = base_.MultDiesel(arg);
86 |
87 | // Check if the values are within reason. Note we need to be quite sloppy (1mm), as copy+pasted
88 | // values from C printf don't have all the digits, and the errors add up.
89 | TestRoundedEquals(target, result, 0.001f);
90 | }
91 |
92 | [Test]
93 | public void TestDecoder()
94 | {
95 | // vec11 in1
96 | Matrix4x4 base_ = new Matrix4x4(
97 | 0.644716f, -0.205625f, 0.736247f, 0,
98 | 0.135465f, 0.978631f, 0.154697f, 0,
99 | -0.752323f, 4.72831e-09f, 0.658794f, 0,
100 | 4.06338f, -1.85149f, 2.32434f, 1
101 | );
102 |
103 | TestRoundedEquals(base_, MatrixDecode(
104 | "1C0C253F6D8F52BEAE7A3C3F0000000040B70A3E8C877A3FD0681E3E00000000" +
105 | "459840BFB076A231B7A6283F000000003107824090FDECBF07C214400000803F"
106 | ), 0.0001f);
107 | }
108 |
109 | public void TestInputFile(string filename)
110 | {
111 | Regex inpat1 = new Regex(@"in1 vec([\d ]\d): Matrix4{ ([\dABCDEF]{128}) }", RegexOptions.IgnoreCase);
112 | Regex inpat2 = new Regex(@"in2 vec([\d ]\d): Matrix4{ ([\dABCDEF]{128}) }", RegexOptions.IgnoreCase);
113 | Regex outpat = new Regex(@"out vec([\d ]\d): Matrix4{ ([\dABCDEF]{128}) }", RegexOptions.IgnoreCase);
114 |
115 | using (StreamReader file = new StreamReader(filename))
116 | {
117 | string line;
118 | while ((line = file.ReadLine()) != null)
119 | {
120 | // Leave room for comments
121 | if (!line.Contains("Matrix4")) continue;
122 |
123 | Match match1 = inpat1.Match(line);
124 | Assert.True(match1.Success);
125 | Match match2 = inpat2.Match(file.ReadLine());
126 | Assert.True(match2.Success);
127 | Match matchout = outpat.Match(file.ReadLine());
128 | Assert.True(matchout.Success);
129 |
130 | // Concat the strings, otherwise it fails for some strange reason
131 | Assert.AreEqual(match1.Groups[1].Value, match2.Groups[1].Value);
132 | Assert.AreEqual(match1.Groups[1].Value, matchout.Groups[1].Value);
133 |
134 | Matrix4x4 in1 = MatrixDecode(match1.Groups[2].Value);
135 | Matrix4x4 in2 = MatrixDecode(match2.Groups[2].Value);
136 | Matrix4x4 outm = MatrixDecode(matchout.Groups[2].Value);
137 |
138 | // Values aren't going to come out exactly the same unfortunately, since we're in C# the
139 | // float calculations will be ever so slightly off, but not nearly enough to be a problem.
140 | TestRoundedEquals(outm, in1.MultDiesel(in2), 0.0001f);
141 | }
142 | }
143 | }
144 |
145 | [Test]
146 | public void TestCopInputFile()
147 | {
148 | var dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
149 | TestInputFile(dir + "/../../../testdata/cop matrix dump.td");
150 | }
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/PD2ModelParser/Modelscript/MergeCommand.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Xml.Linq;
5 | using XmlAttributeAttribute = System.Xml.Serialization.XmlAttributeAttribute;
6 |
7 | using D = PD2ModelParser.Sections;
8 |
9 | namespace PD2ModelParser.Modelscript
10 | {
11 | [Flags]
12 | enum PropertyMergeFlags
13 | {
14 | None = 0,
15 | NewObjects = 0x1,
16 | Parents = 0x2,
17 | Position = 0x4,
18 | Rotation = 0x8,
19 | Scale = 0x10,
20 | Materials = 0x20,
21 | Animations = 0x40,
22 | Transform = Position|Rotation|Scale,
23 | Everything = NewObjects|Parents|Materials|Transform|Animations,
24 | }
25 |
26 | enum ModelDataMergeMode
27 | {
28 | None,
29 | Recreate,
30 | Overwrite,
31 | VertexEdit
32 | }
33 |
34 | [Flags]
35 | enum ModelAttributesMergeFlags
36 | {
37 | None = 0,
38 | Indices = 0x01,
39 | Positions = 0x02,
40 | Normals = 0x04,
41 | Colors = 0x08,
42 | Colours = 0x08,
43 | Weights = 0x10,
44 | UV0 = 0x20,
45 | UV1 = 0x40,
46 | UV2 = 0x80,
47 | UV3 = 0x100,
48 | UV4 = 0x200,
49 | UV5 = 0x400,
50 | UV6 = 0x800,
51 | UV7 = 0x1000,
52 |
53 | UVs = UV0 | UV1 | UV2 | UV3 | UV4 | UV5 | UV6 | UV7,
54 | Vertices = Positions | Normals | Colors | Weights | UVs
55 | }
56 |
57 | class Merge : ScriptItem, IScriptItem
58 | {
59 | [XmlAttribute("property-merge")] public PropertyMergeFlags PropertyMerge { get; set; } = PropertyMergeFlags.Everything;
60 | [XmlAttribute("model-merge")] public ModelDataMergeMode ModelMergeMode { get; set; } = ModelDataMergeMode.Overwrite;
61 | [XmlAttribute("model-attributes")] public ModelAttributesMergeFlags AttributeMergeMode { get; set; } = ModelAttributesMergeFlags.Vertices;
62 | [XmlAttribute("remap-uv")] public int[] RemapUV { get; set; } = new int[0];
63 | [NotAttribute] public IList Script { get; set; } = new List();
64 |
65 | public override void ParseXml(XElement elem)
66 | {
67 | base.ParseXml(elem);
68 | Script = Modelscript.Script.ParseXml(elem.Elements("modelscript").First());
69 | }
70 |
71 | public override void Execute(ScriptState state)
72 | {
73 | var childData = Modelscript.Script.ExecuteItems(this.Script, state.WorkDir);
74 |
75 | var rootObjects = childData.SectionsOfType().Where(i => i.Parent == null);
76 |
77 | foreach(var ro in rootObjects)
78 | {
79 | MergeObject(state.Data, ro);
80 | }
81 | }
82 |
83 | private void MergeObject(FullModelData targetData, D.Object3D sourceObject)
84 | {
85 |
86 | }
87 | }
88 |
89 | class TransplantAttributes : ScriptItem, IScriptItem
90 | {
91 | [XmlAttribute("models")] public string[] Models { get; set; } = new string[0];
92 | [NotAttribute] public IList Script { get; set; } = new List();
93 |
94 | public override void ParseXml(XElement elem)
95 | {
96 | base.ParseXml(elem);
97 | Script = Modelscript.Script.ParseXml(elem.Elements("modelscript").First());
98 | }
99 |
100 | public override void Execute(ScriptState state)
101 | {
102 | state.Log.Status("Run donor script");
103 | var donor = Modelscript.Script.ExecuteItems(Script, state.WorkDir);
104 |
105 | foreach(var name in Models)
106 | {
107 | state.Log.Status("Transfer attributes for {0}", name);
108 | var src_obj = GetModel(state, donor, name, "Source");
109 | var dst_obj = GetModel(state, state.Data, name, "Destination");
110 |
111 | var src_geo = src_obj.PassthroughGP.Geometry;
112 | var dst_geo = dst_obj.PassthroughGP.Geometry;
113 |
114 | dst_geo.Headers.Clear();
115 | dst_geo.Headers.AddRange(src_geo.Headers);
116 |
117 | TransplantAttribute(src_geo.verts, dst_geo.verts);
118 | TransplantAttribute(src_geo.normals, dst_geo.normals);
119 | TransplantAttribute(src_geo.vertex_colors, dst_geo.vertex_colors);
120 | TransplantAttribute(src_geo.weight_groups, dst_geo.weight_groups);
121 | TransplantAttribute(src_geo.weights, dst_geo.weights);
122 | TransplantAttribute(src_geo.binormals, dst_geo.binormals);
123 | TransplantAttribute(src_geo.tangents, dst_geo.tangents);
124 | for(var i = 0; i < src_geo.UVs.Length; i++)
125 | {
126 | TransplantAttribute(src_geo.UVs[i], dst_geo.UVs[i]);
127 | }
128 |
129 | var src_topo = src_obj.PassthroughGP.Topology;
130 | var dst_topo = dst_obj.PassthroughGP.Topology;
131 |
132 | TransplantAttribute(src_topo.facelist, dst_topo.facelist);
133 | TransplantAttribute(src_obj.RenderAtoms, dst_obj.RenderAtoms);
134 | dst_geo.vert_count = src_geo.vert_count;
135 | }
136 | }
137 |
138 | private void TransplantAttribute(List src, List dest)
139 | {
140 | dest.Clear();
141 | dest.Capacity = src.Capacity;
142 | dest.AddRange(src);
143 | }
144 |
145 | private D.Model GetModel(ScriptState state, FullModelData fmd, string name, string reponame)
146 | {
147 | var mod = fmd.GetObject3DByHash(HashName.FromNumberOrString(name)) as D.Model;
148 | if(mod == null)
149 | {
150 | string message = string.Format("{1} object {0} is nonexistent or not a model", name, reponame);
151 | state.Log.Error(message);
152 | throw new Exception(message);
153 | }
154 |
155 | if (mod.PassthroughGP == null)
156 | {
157 | string message = string.Format("{1} model {0} has no geometry provider", name, reponame);
158 | state.Log.Error(message);
159 | throw new Exception(message);
160 | }
161 |
162 | return mod;
163 | }
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/PD2ModelParser/UI/ObjectsPanel.cs:
--------------------------------------------------------------------------------
1 | using PD2ModelParser.Sections;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Data;
5 | using System.Linq;
6 | using System.Windows.Forms;
7 |
8 | using Directory = System.IO.Directory;
9 |
10 | namespace PD2ModelParser.UI
11 | {
12 | public partial class ObjectsPanel : UserControl
13 | {
14 | private readonly Dictionary nodes = new Dictionary();
15 | private readonly ContextMenuStrip nodeRightclickMenu;
16 | private TreeNode menuTarget;
17 | private FullModelData data;
18 |
19 | public ObjectsPanel()
20 | {
21 | InitializeComponent();
22 |
23 | treeView.Nodes.Clear();
24 |
25 | nodeRightclickMenu = new ContextMenuStrip();
26 |
27 | ToolStripButton properties = new ToolStripButton("Properties");
28 | properties.Click += optProperties_Click;
29 | nodeRightclickMenu.Items.Add(properties);
30 | }
31 |
32 | ///
33 | /// Reload the tree view to reflect any new settings
34 | ///
35 | ///
36 | /// This method reloads the model file each time it is
37 | /// called. While this may sound slow, particularly if you've
38 | /// seen how long it takes the tool to load a model when you
39 | /// first drag it in, it is much quicker on subsequent
40 | /// runs.
41 | ///
42 | private void Reload()
43 | {
44 | data = null;
45 | var script = new List();
46 | if(modelFile.Selected != null)
47 | {
48 | script.Add(new Modelscript.LoadModel() { File = modelFile.Selected });
49 | }
50 | else
51 | {
52 | script.Add(new Modelscript.NewModel());
53 | }
54 | btnSave.Enabled = !showScriptChanges.Checked;
55 |
56 | if(scriptFile.Selected != null && showScriptChanges.Checked)
57 | {
58 | script.Add(new Modelscript.RunScript() { File = scriptFile.Selected });
59 | }
60 |
61 | // TODO: There must be a better way to deal with errors.
62 | bool success = Modelscript.Script.ExecuteWithMsgBox(script, Directory.GetCurrentDirectory(), ref data);
63 | if (!success)
64 | return;
65 |
66 | var rootinspector = new Inspector.ModelRootNode(data);
67 | ReconcileChildNodes(rootinspector, treeView.Nodes);
68 | }
69 |
70 | private void showScriptChanges_CheckedChanged(object sender, EventArgs e)
71 | {
72 | Reload();
73 | }
74 |
75 | private void btnReload_Click(object sender, EventArgs e)
76 | {
77 | Reload();
78 | }
79 |
80 | private void fileBrowserControl2_FileSelected(object sender, EventArgs e)
81 | {
82 | Reload();
83 | }
84 |
85 | private void fileBrowserControl1_FileSelected(object sender, EventArgs e)
86 | {
87 | Reload();
88 | }
89 |
90 | private void treeView_NodeMouseClick(object sender, TreeNodeMouseClickEventArgs e)
91 | {
92 | propertyGrid1.SelectedObject = e.Node.Tag;
93 | // Only process right clicks
94 | if (e.Button != MouseButtons.Right)
95 | return;
96 |
97 | // No properties for the root node
98 | if (e.Node.Tag == null)
99 | return;
100 |
101 | menuTarget = e.Node;
102 | nodeRightclickMenu.Show(treeView, e.Location);
103 | }
104 |
105 | private void optProperties_Click(object sender, EventArgs e)
106 | {
107 | var obj = menuTarget.Tag;
108 |
109 | propertyGrid1.SelectedObject = obj;
110 | }
111 |
112 | ///
113 | /// Merges the treeview nodes implied by an IInspectorNode into an existing tree.
114 | ///
115 | ///
116 | ///
117 | /// We use a TreeNodeCollection here to avoid the roots of the tree being special.
118 | /// The root of the inspector node tree corresponds to the treeview as a whole and
119 | /// is never rendered. But that's really a consideration for the caller.
120 | ///
121 | /// Anyway, this method preserves the existing nodes if they match according to the
122 | /// Key member of the inspector node and the name of the treeview node. Keys only
123 | /// actually NEED to be
124 | ///
125 | ///
126 | private void ReconcileChildNodes(Inspector.IInspectorNode modelNode, TreeNodeCollection viewNodes)
127 | {
128 | var newModels = modelNode.GetChildren().ToList();
129 |
130 | var existingKeys = new HashSet(viewNodes.OfType().Select(i=>i.Name));
131 | var newKeys = new HashSet(newModels.Select(i => i.Key));
132 |
133 | var toRemove = new HashSet(existingKeys);
134 | toRemove.ExceptWith(newKeys);
135 | foreach(var i in toRemove)
136 | {
137 | viewNodes.RemoveByKey(i);
138 | }
139 |
140 | List toAdd = new List(newKeys.Count);
141 | foreach(var i in newModels)
142 | {
143 | TreeNode[] mn = viewNodes.Find(i.Key, false);
144 | TreeNode n;
145 | if(mn.Length == 0) { n = new TreeNode(); toAdd.Add(n); }
146 | else { n = mn[0]; }
147 | n.Name = i.Key;
148 | n.Tag = i.PropertyItem;
149 | n.Text = i.Label;
150 | ReconcileChildNodes(i, n.Nodes);
151 | }
152 | viewNodes.AddRange(toAdd.ToArray());
153 | }
154 |
155 | private void btnSave_Click(object sender, EventArgs e)
156 | {
157 | var script = new List()
158 | {
159 | new Modelscript.SaveModel() { File = modelFile.Selected }
160 | };
161 | // TODO: There must be a better way to deal with errors.
162 | bool success = Modelscript.Script.ExecuteWithMsgBox(script, Directory.GetCurrentDirectory(), ref data);
163 | if (!success)
164 | return;
165 | }
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/PD2ModelParser/UI/ObjectsPanel.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
121 | False
122 |
123 |
124 | False
125 |
126 |
--------------------------------------------------------------------------------
/Research Notes/format_documentation.md:
--------------------------------------------------------------------------------
1 | Model format notes.
2 | 0x0 DW If -1 then additional header, else, number of sections.
3 | Additional headers:
4 | 0x4 DW appears to be total file size.
5 | 0x8 DW number of sections
6 |
7 | Sections:
8 | uint32 section_type // Uses one of the below tags. Tags are assigned to serializable objects within the Diesel engine.
9 | uint32 section_id // Appears to be a random, but unique value assigned to the section. Unknown if these have any requirements or meanings.
10 | uint32 size
11 | char[size] data
12 |
13 | Tag:
14 | TopologyIP:
15 | uint32 topology_section_id
16 |
17 | PassthroughGP:
18 | uint32 geometry_section_id
19 | uint32 topology_section_id
20 |
21 | Animation Data:
22 | uint64 unique_id
23 | uint32 unknown
24 | uint32 unknown
25 | uint32 count
26 | item[count]:
27 | uint32 unknown
28 |
29 | Topology:
30 | uint32 unknown
31 | int32 indice_count
32 | short[count1] indices
33 | int32 count2
34 | char[count2]
35 | uint64 unknown
36 |
37 | Geometry:
38 | int32 item_count
39 | int32 type_count
40 | types[type_count]:
41 | uint32 type_size
42 | uint32 type
43 | char[count1*calculated_size] vertex_buffer
44 | uint64 unknown
45 | calculated_size = sum(size_index[type])
46 | size_index = {0, 4, 8, 12, 16, 4, 4, 8, 12}
47 | types: 1 = Vertex, 2 = Normal, 7 = UV, 8 = Unknown, 15 = Unknown, 20 = Tangent/Binormal, 21 = Tangent/Binormal
48 |
49 | cur_data_offset = 0
50 | for type in types:
51 | type_size = size_index[type.type_size]
52 | data_for_type = data[cur_data_offset:cur_data_offset+type_size*item_count]
53 | for x in xrange(item_count):
54 | item = data_for_type[x*type_size:x*type_size+type_size]
55 |
56 |
57 | Material:
58 | uint64 material_id
59 | uint32 zero //48 bytes of skipped data when reading.
60 | uint32 zero
61 | char[16] zero
62 | char[16] zero
63 | uint32 zero
64 | uint32 zero //end 48 bytes of skipped data.
65 | uint32 count
66 | item[count]:
67 | uint32 unknown
68 | uint32 unknown
69 |
70 | Material Group:
71 | uint32 material_count
72 | uint32[count] material_section_ids
73 |
74 | Author:
75 | uint64 unknown
76 | cstring email
77 | cstring source_path
78 | uint32 unknown
79 |
80 | Object3D:
81 | uint64 unique_id
82 | uint32 count
83 | item[count]:
84 | uint32 unknown
85 | uint32 unknown
86 | uint32 unknown
87 | float[4][4] rotation_matrix // Custom orientation matrix for submeshes
88 | float[3] position // Used to position submeshes within object space.
89 | unit32 parent/child_object_section_id
90 |
91 | Model Data:
92 | Object3D 3d_object
93 | uint32 version
94 | if version == 6:
95 | float[3] bounds_min
96 | float[3] bounds_max
97 | uint32 unknown
98 | uint32 unknown
99 | else:
100 | uint32 passthroughgp_section_id
101 | uint32 topologyip_section_id
102 | uint32 count
103 | item[count]:
104 | uint32 unknown
105 | uint32 unknown
106 | uint32 unknown
107 | uint32 unknown
108 | uint32 unknown
109 | uint32 material_group_section_id
110 | uint32 unknown
111 | float[3] bounds_min
112 | float[3] bounds_max
113 | uint32 unknown
114 | uint32 unknown
115 | uint32 unknown
116 |
117 | Light:
118 | Object3D object3d
119 | byte unknown
120 | int32 unknown
121 | float[4] unknown //color?
122 | float unknown //intensity?
123 | float unknown //falloff
124 | float unknown //cone inner?
125 | float unknown //cone outer?
126 | float unknown
127 |
128 | LinearFloatController:
129 | uint64 unique_id //hash of animation name?
130 | uint32 unknown //flags? Appears to use bytes 1 and 2 as flags.
131 | uint32 unknown
132 | uint32 unknown //Appears linked to above value
133 | uint32 keyframe_count
134 | keyframe[keyframe_count]:
135 | float unknown //Timestamp?
136 | float value
137 |
138 | LookAtConstrRotationController:
139 | uint64 unique_id //hash of animation name?
140 | uint32 unknown
141 | uint32 unknown //Reference to another section
142 | uint32 unknown //Reference to another section
143 | uint32 unknown //Reference to another section
144 |
145 | LinearVector3Controller:
146 | uint64 unique_id //hash of animation name?
147 | uint32 unknown //flags? Appears to use bytes 1 and 2 as flags.
148 | uint32 unknown
149 | uint32 unknown //Appears linked to above value
150 | uint32 keyframe_count
151 | keyframe[keyframe_count]:
152 | float unknown //Timestamp?
153 | float[3] position
154 |
155 | QuatLinearRotationController:
156 | uint64 unique_id //hash of animation name?
157 | uint32 unknown //flags? Appears to use bytes 1 and 2 as flags.
158 | uint32 unknown
159 | uint32 unknown //Appears linked to above value
160 | uint32 keyframe_count
161 | keyframe[keyframe_count]:
162 | float unknown //Timestamp?
163 | float[4] rotation //Quaternion
164 |
165 | QuatBezRotationcontroller:
166 | uint64 unique_id //hash of animation name?
167 | uint32 unknown //flags? Appears to use bytes 1 and 2 as flags.
168 | uint32 unknown
169 | uint32 unknown //Appears linked to above value
170 | uint32 keyframe_count
171 | keyframe[keyframe_count]:
172 | float unknown //Timestamp?
173 | float[4] unknown
174 | float[4] unknown
175 | float[4] unknown
176 |
177 | LightSet:
178 | uint64 unique_id //hash of light set name?
179 | uint32 light_count
180 | light[light_count]
181 | uint32 light_section_id
182 |
183 | Bones:
184 | uint32 count
185 | bone[count]:
186 | uint32 vertex_count?
187 | bone_vertex[count]:
188 | uint32 vertex_id?
189 |
190 |
191 | Skin Bones:
192 | Bones bones
193 | uint32 object3d_section_id
194 | uint32 count
195 | objects[count]:
196 | uint32 object3d_section_id
197 | rotations[count]:
198 | float[4][4] orientation_matrix
199 | float[4][4] unknown_matrix
200 |
201 | Camera:
202 | Object3D object
203 | float unknown
204 | float unknown
205 | float unknown
206 | float unknown
207 | float unknown
208 | float unknown
209 |
210 | Tags:
211 | 0x5DC011B8 == Load routine at 0x0073E930 //Animation data
212 | 0x7623C465 == Load routine at 0x006FA100 //Author tag
213 | 0x29276B1D == Load routine at 0x0073E340 //Material Group
214 | 0x3C54609C == Load routine at 0x0073E270 //Material
215 | 0x0FFCD100 == Load routine at 0x00742F80 //Object3D
216 | 0x62212D88 == Load routine at 0x00749750 //Model data
217 | 0x7AB072D3 == Load routine at 0x0071FEA0 //Geometry
218 | 0x4C507A13 == Load routine at 0x0071FFF0 //Topology
219 | 0xE3A3B1CA == Load routine at 0x0073DD10 //PassthroughGP
220 | 0x03B634BD == Load routine at 0x0073DDC0 //TopologyIP
221 | 0x648A206C == Load routine at 0x0071F680 //QuatLinearRotationController
222 | 0x197345A5 == Load routine at 0x0071F6B0 //QuatBezRotationController
223 | 0x65CC1825 == Load routine at 0x007440D0 //SkinBones
224 | 0x2EB43C77 == Load routine at 0x00743FD0 //Bones
225 | 0xFFA13B80 == Load routine at 0x00745A40 //Light
226 | 0x33552583 == Load routine at 0x0073DDF0 //LightSet
227 | 0x26A5128C == Load routine at 0x0071F620 //LinearVector3Controller
228 | 0x76BF5B66 == Load routine at 0x0071F570 //LinearFloatController
229 | 0x679D695B == Load routine at 0x0073DA00 //LookAtConstrRotationController
230 | 0x46BF31A7 == Load routine at 0x00745970 //Camera
231 |
232 |
--------------------------------------------------------------------------------