├── .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 | ![A box in blender, attached to a bone with no rotation](images/resized_rigging_not_rotated.png) 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 | ![The above but with the bone rotated 45°](images/resized_rigging_rotated.png) 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 | ![The bone is not rotated, but the mesh is](images/resized_rigging_broken.png) 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 | ![An image of a broken Dallas model](images/broken-in-game.png) 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 | --------------------------------------------------------------------------------