From bc1efebe5c5dfa292a4b1fd84608c109ffe6380f Mon Sep 17 00:00:00 2001 From: dexy Date: Wed, 20 Mar 2019 21:21:47 +1100 Subject: [PATCH] FBX importer v1, fix for YNDs DLC loading, fix for renamed NG encrypted RPFs, RelFile MetaNames addition --- CodeWalker.Core/CodeWalker.Core.csproj | 2 + .../GameFiles/FileTypes/RelFile.cs | 2 +- CodeWalker.Core/GameFiles/GameFileCache.cs | 152 +- .../GameFiles/MetaTypes/MetaNames.cs | 1 + CodeWalker.Core/GameFiles/Resources/Bounds.cs | 3 +- .../GameFiles/Resources/Drawable.cs | 99 +- .../GameFiles/Resources/ResourceBaseTypes.cs | 2 +- CodeWalker.Core/GameFiles/RpfManager.cs | 7 +- CodeWalker.Core/Utils/Fbx.cs | 1877 +++++++++++++++++ CodeWalker.Core/Utils/FbxConverter.cs | 1010 +++++++++ CodeWalker.Core/World/Space.cs | 1 + CodeWalker.csproj | 9 + ExploreForm.Designer.cs | 43 +- ExploreForm.cs | 113 +- ExploreForm.resx | 2 +- Notice.txt | 19 +- Rendering/Renderable.cs | 16 +- Rendering/Renderer.cs | 2 +- Rendering/ShaderManager.cs | 2 +- Shaders/Common.hlsli | 10 +- Tools/ImportFbxForm.Designer.cs | 145 ++ Tools/ImportFbxForm.cs | 133 ++ Tools/ImportFbxForm.resx | 409 ++++ 23 files changed, 3876 insertions(+), 183 deletions(-) create mode 100644 CodeWalker.Core/Utils/Fbx.cs create mode 100644 CodeWalker.Core/Utils/FbxConverter.cs create mode 100644 Tools/ImportFbxForm.Designer.cs create mode 100644 Tools/ImportFbxForm.cs create mode 100644 Tools/ImportFbxForm.resx diff --git a/CodeWalker.Core/CodeWalker.Core.csproj b/CodeWalker.Core/CodeWalker.Core.csproj index 85c1a62..949c009 100644 --- a/CodeWalker.Core/CodeWalker.Core.csproj +++ b/CodeWalker.Core/CodeWalker.Core.csproj @@ -123,6 +123,8 @@ + + diff --git a/CodeWalker.Core/GameFiles/FileTypes/RelFile.cs b/CodeWalker.Core/GameFiles/FileTypes/RelFile.cs index 37a79c3..92752cc 100644 --- a/CodeWalker.Core/GameFiles/FileTypes/RelFile.cs +++ b/CodeWalker.Core/GameFiles/FileTypes/RelFile.cs @@ -4932,7 +4932,7 @@ namespace CodeWalker.GameFiles VehicleEngine = 4, Entity = 5, //not sure about this StaticEmitter = 6,//radio emitter? - Prop = 7,//prop? eg. fan, radar + Prop = 7,//prop? entity emitter? eg. fan, radar Helicopter = 8, Unk9 = 9, Unk11 = 11, //contains reference to Unk12 diff --git a/CodeWalker.Core/GameFiles/GameFileCache.cs b/CodeWalker.Core/GameFiles/GameFileCache.cs index cfbaa9e..2c92b80 100644 --- a/CodeWalker.Core/GameFiles/GameFileCache.cs +++ b/CodeWalker.Core/GameFiles/GameFileCache.cs @@ -1014,33 +1014,41 @@ namespace CodeWalker.GameFiles //} if (entry.Name.EndsWith(".ymf"))// || entry.Name.EndsWith(".ymt")) { - UpdateStatus(string.Format(entry.Path)); - YmfFile ymffile = RpfMan.GetFile(entry); - if (ymffile != null) + try { - AllManifests.Add(ymffile); - - if (ymffile.Pso != null) - { } - else if (ymffile.Rbf != null) - { } - else if (ymffile.Meta != null) - { } - else - { } - - - if (ymffile.HDTxdAssetBindings != null) + UpdateStatus(string.Format(entry.Path)); + YmfFile ymffile = RpfMan.GetFile(entry); + if (ymffile != null) { - for (int i = 0; i < ymffile.HDTxdAssetBindings.Length; i++) - { - var b = ymffile.HDTxdAssetBindings[i]; - var targetasset = JenkHash.GenHash(b.targetAsset.ToString().ToLowerInvariant()); - var hdtxd = JenkHash.GenHash(b.HDTxd.ToString().ToLowerInvariant()); - hdtexturelookup[targetasset] = hdtxd; - } - } + AllManifests.Add(ymffile); + if (ymffile.Pso != null) + { } + else if (ymffile.Rbf != null) + { } + else if (ymffile.Meta != null) + { } + else + { } + + + if (ymffile.HDTxdAssetBindings != null) + { + for (int i = 0; i < ymffile.HDTxdAssetBindings.Length; i++) + { + var b = ymffile.HDTxdAssetBindings[i]; + var targetasset = JenkHash.GenHash(b.targetAsset.ToString().ToLowerInvariant()); + var hdtxd = JenkHash.GenHash(b.HDTxd.ToString().ToLowerInvariant()); + hdtexturelookup[targetasset] = hdtxd; + } + } + + } + } + catch (Exception ex) + { + string errstr = entry.Path + "\n" + ex.ToString(); + ErrorLog(errstr); } } @@ -2363,9 +2371,6 @@ namespace CodeWalker.GameFiles { UpdateStatus("Testing Audio REL files"); - StringBuilder sb = new StringBuilder(); - StringBuilder sbh = new StringBuilder(); - StringBuilder sbi = new StringBuilder(); bool savetest = true; bool xmltest = true; @@ -2386,82 +2391,6 @@ namespace CodeWalker.GameFiles RpfMan.LoadFile(rel, rfe); - #region string building - - if (rel.NameTable == null) - { - sb.AppendLine(rfe.Path + ": no strings found"); - } - else - { - sb.AppendLine(rfe.Path + ": " + rel.NameTable.Length.ToString() + " strings found:"); - foreach (string str in rel.NameTable) - { - sb.AppendLine(str); - } - } - if (rel.IndexStrings != null) - { - sb.AppendLine("Config-specific:"); - foreach (var unk in rel.IndexStrings) - { - sb.AppendLine(unk.ToString()); - } - } - if (rel.IndexHashes != null) - { - sbh.AppendLine(rfe.Path + ": " + rel.IndexHashes.Length.ToString() + " entries:"); - foreach (var unk in rel.IndexHashes) - { - sbh.Append(unk.Name.Hash.ToString("X8")); - string strval; - if (JenkIndex.Index.TryGetValue(unk.Name, out strval)) - { - sbh.Append(" - "); - sbh.Append(strval); - } - sbh.AppendLine(); - //sbh.AppendLine(unk.ToString()); - } - sbh.AppendLine(); - } - if (rel.HashTable != null) - { - sbh.AppendLine(rfe.Path + ": " + rel.HashTable.Length.ToString() + " Hashes1:"); - foreach (var unk in rel.HashTable) - { - sbh.Append(unk.Hash.ToString("X8")); - string strval; - if (JenkIndex.Index.TryGetValue(unk, out strval)) - { - sbh.Append(" - "); - sbh.Append(strval); - } - sbh.AppendLine(); - } - sbh.AppendLine(); - } - if (rel.PackTable != null) - { - sbh.AppendLine(rfe.Path + ": " + rel.PackTable.Length.ToString() + " Hashes2:"); - foreach (var unk in rel.PackTable) - { - sbh.Append(unk.Hash.ToString("X8")); - string strval; - if (JenkIndex.Index.TryGetValue(unk, out strval)) - { - sbh.Append(" - "); - sbh.Append(strval); - } - sbh.AppendLine(); - } - sbh.AppendLine(); - } - - sb.AppendLine(); - - #endregion - byte[] data; @@ -2540,29 +2469,12 @@ namespace CodeWalker.GameFiles } - //sbi.Clear(); - //foreach (var rd in rel.RelDatas) - //{ - // sbi.AppendLine(new FlagsUint(rd.NameHash).Bin); - //} - //string indexbinstr = sbi.ToString(); - } } } - //int ctot = Dat151RelData.TotCount; - //StringBuilder sbp = new StringBuilder(); - //foreach (string s in Dat151RelData.FoundCoords) - //{ - // sbp.AppendLine(s); - //} - //string posz = sbp.ToString(); - - string relstrs = sb.ToString(); - string hashstrs = sbh.ToString(); var hashmap = RelFile.HashesMap; diff --git a/CodeWalker.Core/GameFiles/MetaTypes/MetaNames.cs b/CodeWalker.Core/GameFiles/MetaTypes/MetaNames.cs index 068f467..7f5c4d1 100644 --- a/CodeWalker.Core/GameFiles/MetaTypes/MetaNames.cs +++ b/CodeWalker.Core/GameFiles/MetaTypes/MetaNames.cs @@ -3475,6 +3475,7 @@ namespace CodeWalker.GameFiles silence = 3503773450, //used in game.dat151.rel null_sound = 3817852694, //used in game.dat151.rel + constant_one = 3454258691, //used in dat .rels run = 285848937, //used in game.dat151.rel cop_dispatch_interaction_settings = 778268174, //used in game.dat151.rel diff --git a/CodeWalker.Core/GameFiles/Resources/Bounds.cs b/CodeWalker.Core/GameFiles/Resources/Bounds.cs index 4bf87ca..ed50602 100644 --- a/CodeWalker.Core/GameFiles/Resources/Bounds.cs +++ b/CodeWalker.Core/GameFiles/Resources/Bounds.cs @@ -775,8 +775,7 @@ namespace CodeWalker.GameFiles } } - [TC(typeof(EXP))] - public struct BoundMaterial_s + [TC(typeof(EXP))] public struct BoundMaterial_s { public uint Data1; diff --git a/CodeWalker.Core/GameFiles/Resources/Drawable.cs b/CodeWalker.Core/GameFiles/Resources/Drawable.cs index 49d9848..5555030 100644 --- a/CodeWalker.Core/GameFiles/Resources/Drawable.cs +++ b/CodeWalker.Core/GameFiles/Resources/Drawable.cs @@ -122,13 +122,14 @@ namespace CodeWalker.GameFiles public MetaHash Name { get; set; } //530103687, 2401522793, 1912906641 public uint Unknown_Ch { get; set; } // 0x00000000 public byte ParameterCount { get; set; } - public byte Unknown_11h { get; set; } // 2, 0, - public ushort Unknown_12h { get; set; } // 32768 - public uint Unknown_14h { get; set; } //10485872, 17826000, 26214720 + public byte RenderBucket { get; set; } // 2, 0, + public ushort Unknown_12h { get; set; } // 32768 HasComment? + public ushort ParameterSize { get; set; } //112, 208, 320 (with 16h) 10485872, 17826000, 26214720 + public ushort ParameterDataSize { get; set; } //160, 272, 400 public MetaHash FileName { get; set; } //2918136469, 2635608835, 2247429097 public uint Unknown_1Ch { get; set; } // 0x00000000 - public uint Unknown_20h { get; set; } //65284, 65281 - public ushort Unknown_24h { get; set; } //0 + public uint RenderBucketMask { get; set; } //65284, 65281 DrawBucketMask? (1< Geometries { get; set; } @@ -1116,8 +1146,10 @@ namespace CodeWalker.GameFiles this.Unknown_14h = reader.ReadUInt32(); this.BoundsPointer = reader.ReadUInt64(); this.ShaderMappingPointer = reader.ReadUInt64(); - this.Unknown_28h = reader.ReadUInt32(); - this.Unknown_2Ch = reader.ReadUInt32(); + this.SkeletonBinding = reader.ReadUInt32(); + this.RenderMaskFlags = reader.ReadUInt16(); + this.GeometriesCount3 = reader.ReadUInt16(); + // read reference data this.Geometries = reader.ReadBlockAt>( @@ -1138,6 +1170,7 @@ namespace CodeWalker.GameFiles this.GeometriesPointer = (ulong)(this.Geometries != null ? this.Geometries.FilePosition : 0); this.GeometriesCount1 = (ushort)(this.Geometries != null ? this.Geometries.Count : 0); this.GeometriesCount2 = this.GeometriesCount1;//is this correct? + this.GeometriesCount3 = this.GeometriesCount1;//is this correct? this.BoundsPointer = (ulong)(this.BoundsDataBlock != null ? this.BoundsDataBlock.FilePosition : 0); this.ShaderMappingPointer = (ulong)(this.ShaderMappingBlock != null ? this.ShaderMappingBlock.FilePosition : 0); @@ -1151,8 +1184,9 @@ namespace CodeWalker.GameFiles writer.Write(this.Unknown_14h); writer.Write(this.BoundsPointer); writer.Write(this.ShaderMappingPointer); - writer.Write(this.Unknown_28h); - writer.Write(this.Unknown_2Ch); + writer.Write(this.SkeletonBinding); + writer.Write(this.RenderMaskFlags); + writer.Write(this.GeometriesCount3); } /// @@ -2137,16 +2171,17 @@ namespace CodeWalker.GameFiles public ulong DrawableModelsMediumPointer { get; set; } public ulong DrawableModelsLowPointer { get; set; } public ulong DrawableModelsVeryLowPointer { get; set; } - public float LodGroupHigh { get; set; } - public float LodGroupMed { get; set; } - public float LodGroupLow { get; set; } - public float LodGroupVlow { get; set; } + public float LodDistHigh { get; set; } + public float LodDistMed { get; set; } + public float LodDistLow { get; set; } + public float LodDistVlow { get; set; } public uint Unknown_80h { get; set; } public uint Unknown_84h { get; set; } public uint Unknown_88h { get; set; } public uint Unknown_8Ch { get; set; } public ulong JointsPointer { get; set; } - public uint Unknown_98h { get; set; } + public ushort Unknown_98h { get; set; } + public ushort Unknown_9Ah { get; set; } public uint Unknown_9Ch { get; set; } // 0x00000000 public ulong DrawableModelsXPointer { get; set; } @@ -2207,16 +2242,17 @@ namespace CodeWalker.GameFiles this.DrawableModelsMediumPointer = reader.ReadUInt64(); this.DrawableModelsLowPointer = reader.ReadUInt64(); this.DrawableModelsVeryLowPointer = reader.ReadUInt64(); - this.LodGroupHigh = reader.ReadSingle(); - this.LodGroupMed = reader.ReadSingle(); - this.LodGroupLow = reader.ReadSingle(); - this.LodGroupVlow = reader.ReadSingle(); + this.LodDistHigh = reader.ReadSingle(); + this.LodDistMed = reader.ReadSingle(); + this.LodDistLow = reader.ReadSingle(); + this.LodDistVlow = reader.ReadSingle(); this.Unknown_80h = reader.ReadUInt32(); this.Unknown_84h = reader.ReadUInt32(); this.Unknown_88h = reader.ReadUInt32(); this.Unknown_8Ch = reader.ReadUInt32(); this.JointsPointer = reader.ReadUInt64(); - this.Unknown_98h = reader.ReadUInt32(); + this.Unknown_98h = reader.ReadUInt16(); + this.Unknown_9Ah = reader.ReadUInt16(); this.Unknown_9Ch = reader.ReadUInt32(); this.DrawableModelsXPointer = reader.ReadUInt64(); @@ -2345,16 +2381,17 @@ namespace CodeWalker.GameFiles writer.Write(this.DrawableModelsMediumPointer); writer.Write(this.DrawableModelsLowPointer); writer.Write(this.DrawableModelsVeryLowPointer); - writer.Write(this.LodGroupHigh); - writer.Write(this.LodGroupMed); - writer.Write(this.LodGroupLow); - writer.Write(this.LodGroupVlow); + writer.Write(this.LodDistHigh); + writer.Write(this.LodDistMed); + writer.Write(this.LodDistLow); + writer.Write(this.LodDistVlow); writer.Write(this.Unknown_80h); writer.Write(this.Unknown_84h); writer.Write(this.Unknown_88h); writer.Write(this.Unknown_8Ch); writer.Write(this.JointsPointer); writer.Write(this.Unknown_98h); + writer.Write(this.Unknown_9Ah); writer.Write(this.Unknown_9Ch); writer.Write(this.DrawableModelsXPointer); } diff --git a/CodeWalker.Core/GameFiles/Resources/ResourceBaseTypes.cs b/CodeWalker.Core/GameFiles/Resources/ResourceBaseTypes.cs index 7ccb4d9..f433fc9 100644 --- a/CodeWalker.Core/GameFiles/Resources/ResourceBaseTypes.cs +++ b/CodeWalker.Core/GameFiles/Resources/ResourceBaseTypes.cs @@ -1644,7 +1644,7 @@ namespace CodeWalker.GameFiles //public ResourcePointerArray64 Entries; public ulong[] data_pointers { get; private set; } - public T[] data_items { get; private set; } + public T[] data_items { get; set; } private ResourcePointerArray64 data_block;//used for saving. diff --git a/CodeWalker.Core/GameFiles/RpfManager.cs b/CodeWalker.Core/GameFiles/RpfManager.cs index be8686f..c437013 100644 --- a/CodeWalker.Core/GameFiles/RpfManager.cs +++ b/CodeWalker.Core/GameFiles/RpfManager.cs @@ -74,6 +74,11 @@ namespace CodeWalker.GameFiles rf.ScanStructure(updateStatus, errorLog); + if (rf.LastException != null) //incase of corrupted rpf (or renamed NG encrypted RPF) + { + continue; + } + AddRpfFile(rf, false, false); } catch (Exception ex) @@ -384,7 +389,7 @@ namespace CodeWalker.GameFiles } if (BuildExtendedJenkIndex) { - if (nlow.EndsWith(".ydr") || nlow.EndsWith(".yft")) + if (nlow.EndsWith(".ydr"))// || nlow.EndsWith(".yft")) //do yft's get lods? { var sname = nlow.Substring(0, nlow.Length - 4); JenkIndex.Ensure(sname + "_lod"); diff --git a/CodeWalker.Core/Utils/Fbx.cs b/CodeWalker.Core/Utils/Fbx.cs new file mode 100644 index 0000000..75bacb4 --- /dev/null +++ b/CodeWalker.Core/Utils/Fbx.cs @@ -0,0 +1,1877 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.RegularExpressions; + +/* + Shamelessly stolen and mangled from: + https://github.com/hamish-milne/FbxWriter + Under GPL license, for full terms see the above link. + + Copyright (c) 2015 Hamish Milne + + "An FBX library for .NET" +*/ + + +namespace CodeWalker +{ + + /// + /// Static read and write methods + /// + public static class FbxIO + { + + /// + /// Read binary or ASCII FBX from memory. Decides which based on the header. + /// (This method added by dexyfex) + /// + /// FBX byte array. + /// + public static FbxDocument Read(byte[] data) + { + using (var stream = new MemoryStream(data)) + { + var isbinary = FbxBinary.IsBinary(stream); + if (isbinary) + { + var reader = new FbxBinaryReader(stream); + return reader.Read(); + } + else //try ASCII + { + var reader = new FbxAsciiReader(stream); + return reader.Read(); + } + } + } + + + /// + /// Reads a binary FBX file + /// + /// + /// The top level document node + public static FbxDocument ReadBinary(string path) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + using (var stream = new FileStream(path, FileMode.Open)) + { + var reader = new FbxBinaryReader(stream); + return reader.Read(); + } + } + + /// + /// Reads an ASCII FBX file + /// + /// + /// The top level document node + public static FbxDocument ReadAscii(string path) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + using (var stream = new FileStream(path, FileMode.Open)) + { + var reader = new FbxAsciiReader(stream); + return reader.Read(); + } + } + + /// + /// Writes an FBX document + /// + /// The top level document node + /// + public static void WriteBinary(FbxDocument document, string path) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + using (var stream = new FileStream(path, FileMode.Create)) + { + var writer = new FbxBinaryWriter(stream); + writer.Write(document); + } + } + + /// + /// Writes an FBX document + /// + /// The top level document node + /// + public static void WriteAscii(FbxDocument document, string path) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + using (var stream = new FileStream(path, FileMode.Create)) + { + var writer = new FbxAsciiWriter(stream); + writer.Write(document); + } + } + } + + + + + /// + /// Reads FBX nodes from a text stream + /// + public class FbxAsciiReader + { + private readonly Stream stream; + private readonly FbxErrorLevel errorLevel; + + private int line = 1; + private int column = 1; + + /// + /// Creates a new reader + /// + /// + /// + public FbxAsciiReader(Stream stream, FbxErrorLevel errorLevel = FbxErrorLevel.Checked) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + this.stream = stream; + this.errorLevel = errorLevel; + } + + /// + /// The maximum array size that will be allocated + /// + /// + /// If you trust the source, you can expand this value as necessary. + /// Malformed files could cause large amounts of memory to be allocated + /// and slow or crash the system as a result. + /// + public int MaxArrayLength { get; set; } = (1 << 24); + + // We read bytes a lot, so we should make a more efficient method here + // (The normal one makes a new byte array each time) + + readonly byte[] singleChar = new byte[1]; + private char? prevChar; + private bool endStream; + private bool wasCr; + + // Reads a char, allows peeking and checks for end of stream + char ReadChar() + { + if (prevChar != null) + { + var c = prevChar.Value; + prevChar = null; + return c; + } + if (stream.Read(singleChar, 0, 1) < 1) + { + endStream = true; + return '\0'; + } + var ch = (char)singleChar[0]; + // Handle line and column numbers here; + // This isn't terribly accurate, but good enough for diagnostics + if (ch == '\r') + { + wasCr = true; + line++; + column = 0; + } + else + { + if (ch == '\n' && !wasCr) + { + line++; + column = 0; + } + wasCr = false; + } + column++; + return ch; + } + + // Checks if a character is valid in a real number + static bool IsDigit(char c, bool first) + { + if (char.IsDigit(c)) + return true; + switch (c) + { + case '-': + case '+': + return true; + case '.': + case 'e': + case 'E': + case 'X': + case 'x': + return !first; + } + return false; + } + + static bool IsLineEnd(char c) + { + return c == '\r' || c == '\n'; + } + + // Token to mark the end of the stream + class EndOfStream + { + public override string ToString() + { + return "end of stream"; + } + } + + // Wrapper around a string to mark it as an identifier + // (as opposed to a string literal) + class Identifier + { + public readonly string String; + + public override bool Equals(object obj) + { + var id = obj as Identifier; + if (id != null) + return String == id.String; + return false; + } + + public override int GetHashCode() + { + return String?.GetHashCode() ?? 0; + } + + public Identifier(string str) + { + String = str; + } + + public override string ToString() + { + return String + ":"; + } + } + + private object prevTokenSingle; + + // Reads a single token, allows peeking + // Can return 'null' for a comment or whitespace + object ReadTokenSingle() + { + if (prevTokenSingle != null) + { + var ret = prevTokenSingle; + prevTokenSingle = null; + return ret; + } + var c = ReadChar(); + if (endStream) + return new EndOfStream(); + switch (c) + { + case ';': // Comments + while (!IsLineEnd(ReadChar()) && !endStream) { } // Skip a line + return null; + case '{': // Operators + case '}': + case '*': + case ':': + case ',': + return c; + case '"': // String literal + var sb1 = new StringBuilder(); + while ((c = ReadChar()) != '"') + { + if (endStream) + throw new FbxException(line, column, + "Unexpected end of stream; expecting end quote"); + sb1.Append(c); + } + return sb1.ToString(); + default: + if (char.IsWhiteSpace(c)) + { + // Merge whitespace + while (char.IsWhiteSpace(c = ReadChar()) && !endStream) { } + if (!endStream) + prevChar = c; + return null; + } + if (IsDigit(c, true)) // Number + { + var sb2 = new StringBuilder(); + do + { + sb2.Append(c); + c = ReadChar(); + } while (IsDigit(c, false) && !endStream); + if (!endStream) + prevChar = c; + var str = sb2.ToString(); + if (str.Contains(".")) + { + if (str.Split('.', 'e', 'E')[1].Length > 6) + { + double d; + if (!double.TryParse(str, out d)) + throw new FbxException(line, column, + "Invalid number"); + return d; + } + else + { + float f; + if (!float.TryParse(str, out f)) + throw new FbxException(line, column, + "Invalid number"); + return f; + } + } + long l; + if (!long.TryParse(str, out l)) + throw new FbxException(line, column, + "Invalid integer"); + // Check size and return the smallest possible + if (l >= byte.MinValue && l <= byte.MaxValue) + return (byte)l; + if (l >= int.MinValue && l <= int.MaxValue) + return (int)l; + return l; + } + if (char.IsLetter(c) || c == '_') // Identifier + { + var sb3 = new StringBuilder(); + do + { + sb3.Append(c); + c = ReadChar(); + } while ((char.IsLetterOrDigit(c) || c == '_') && !endStream); + if (!endStream) + prevChar = c; + return new Identifier(sb3.ToString()); + } + break; + } + throw new FbxException(line, column, + "Unknown character " + c); + } + + private object prevToken; + + // Use a loop rather than recursion to prevent stack overflow + // Here we can also merge string+colon into an identifier, + // returning single-character bare strings (for C-type properties) + object ReadToken() + { + object ret; + if (prevToken != null) + { + ret = prevToken; + prevToken = null; + return ret; + } + do + { + ret = ReadTokenSingle(); + } while (ret == null); + var id = ret as Identifier; + if (id != null) + { + object colon; + do + { + colon = ReadTokenSingle(); + } while (colon == null); + if (!':'.Equals(colon)) + { + if (id.String.Length > 1) + throw new FbxException(line, column, + "Unexpected '" + colon + "', expected ':' or a single-char literal"); + ret = id.String[0]; + prevTokenSingle = colon; + } + } + return ret; + } + + void ExpectToken(object token) + { + var t = ReadToken(); + if (!token.Equals(t)) + throw new FbxException(line, column, + "Unexpected '" + t + "', expected " + token); + } + + private enum ArrayType + { + Byte = 0, + Int = 1, + Long = 2, + Float = 3, + Double = 4, + }; + + Array ReadArray() + { + // Read array length and header + var len = ReadToken(); + long l; + if (len is long) + l = (long)len; + else if (len is int) + l = (int)len; + else if (len is byte) + l = (byte)len; + else + throw new FbxException(line, column, + "Unexpected '" + len + "', expected an integer"); + if (l < 0) + throw new FbxException(line, column, + "Invalid array length " + l); + if (l > MaxArrayLength) + throw new FbxException(line, column, + "Array length " + l + " higher than permitted maximum " + MaxArrayLength); + ExpectToken('{'); + ExpectToken(new Identifier("a")); + var array = new double[l]; + + // Read array elements + bool expectComma = false; + object token; + var arrayType = ArrayType.Byte; + long pos = 0; + while (!'}'.Equals(token = ReadToken())) + { + if (expectComma) + { + if (!','.Equals(token)) + throw new FbxException(line, column, + "Unexpected '" + token + "', expected ','"); + expectComma = false; + continue; + } + if (pos >= array.Length) + { + if (errorLevel >= FbxErrorLevel.Checked) + throw new FbxException(line, column, + "Too many elements in array"); + continue; + } + + // Add element to the array, checking for the maximum + // size of any one element. + // (I'm not sure if this is the 'correct' way to do it, but it's the only + // logical one given the nature of the ASCII format) + double d; + if (token is byte) + { + d = (byte)token; + } + else if (token is int) + { + d = (int)token; + if (arrayType < ArrayType.Int) + arrayType = ArrayType.Int; + } + else if (token is long) + { + d = (long)token; + if (arrayType < ArrayType.Long) + arrayType = ArrayType.Long; + } + else if (token is float) + { + d = (float)token; + // A long can't be accurately represented by a float + arrayType = arrayType < ArrayType.Long + ? ArrayType.Float : ArrayType.Double; + } + else if (token is double) + { + d = (double)token; + if (arrayType < ArrayType.Double) + arrayType = ArrayType.Double; + } + else + throw new FbxException(line, column, + "Unexpected '" + token + "', expected a number"); + array[pos++] = d; + expectComma = true; + } + if (pos < array.Length && errorLevel >= FbxErrorLevel.Checked) + throw new FbxException(line, column, + "Too few elements in array - expected " + (array.Length - pos) + " more"); + + // Convert the array to the smallest type we can see + Array ret; + switch (arrayType) + { + case ArrayType.Byte: + var bArray = new byte[array.Length]; + for (int i = 0; i < bArray.Length; i++) + bArray[i] = (byte)array[i]; + ret = bArray; + break; + case ArrayType.Int: + var iArray = new int[array.Length]; + for (int i = 0; i < iArray.Length; i++) + iArray[i] = (int)array[i]; + ret = iArray; + break; + case ArrayType.Long: + var lArray = new long[array.Length]; + for (int i = 0; i < lArray.Length; i++) + lArray[i] = (long)array[i]; + ret = lArray; + break; + case ArrayType.Float: + var fArray = new float[array.Length]; + for (int i = 0; i < fArray.Length; i++) + fArray[i] = (long)array[i]; + ret = fArray; + break; + default: + ret = array; + break; + } + return ret; + } + + /// + /// Reads the next node from the stream + /// + /// The read node, or null + public FbxNode ReadNode() + { + var first = ReadToken(); + var id = first as Identifier; + if (id == null) + { + if (first is EndOfStream) + return null; + throw new FbxException(line, column, + "Unexpected '" + first + "', expected an identifier"); + } + var node = new FbxNode { Name = id.String }; + + // Read properties + object token; + bool expectComma = false; + while (!'{'.Equals(token = ReadToken()) && !(token is Identifier) && !'}'.Equals(token)) + { + if (expectComma) + { + if (!','.Equals(token)) + throw new FbxException(line, column, + "Unexpected '" + token + "', expected a ','"); + expectComma = false; + continue; + } + if (token is char) + { + var c = (char)token; + switch (c) + { + case '*': + token = ReadArray(); + break; + case '}': + case ':': + case ',': + throw new FbxException(line, column, + "Unexpected '" + c + "' in property list"); + } + } + node.Properties.Add(token); + expectComma = true; // The final comma before the open brace isn't required + } + // TODO: Merge property list into an array as necessary + // Now we're either at an open brace, close brace or a new node + if (token is Identifier || '}'.Equals(token)) + { + prevToken = token; + return node; + } + // The while loop can't end unless we're at an open brace, so we can continue right on + object endBrace; + while (!'}'.Equals(endBrace = ReadToken())) + { + prevToken = endBrace; // If it's not an end brace, the next node will need it + node.Nodes.Add(ReadNode()); + } + if (node.Nodes.Count < 1) // If there's an open brace, we want that to be preserved + node.Nodes.Add(null); + return node; + } + + /// + /// Reads a full document from the stream + /// + /// The complete document object + public FbxDocument Read() + { + var ret = new FbxDocument(); + + // Read version string + const string versionString = @"; FBX (\d)\.(\d)\.(\d) project file"; + char c; + while (char.IsWhiteSpace(c = ReadChar()) && !endStream) { } // Skip whitespace + bool hasVersionString = false; + if (c == ';') + { + var sb = new StringBuilder(); + do + { + sb.Append(c); + } while (!IsLineEnd(c = ReadChar()) && !endStream); + var match = Regex.Match(sb.ToString(), versionString); + hasVersionString = match.Success; + if (hasVersionString) + ret.Version = (FbxVersion)( + int.Parse(match.Groups[1].Value) * 1000 + + int.Parse(match.Groups[2].Value) * 100 + + int.Parse(match.Groups[3].Value) * 10 + ); + } + if (!hasVersionString && errorLevel >= FbxErrorLevel.Strict) + throw new FbxException(line, column, + "Invalid version string; first line must match \"" + versionString + "\""); + FbxNode node; + while ((node = ReadNode()) != null) + ret.Nodes.Add(node); + return ret; + } + } + + /// + /// Writes an FBX document in a text format + /// + public class FbxAsciiWriter + { + private readonly Stream stream; + + /// + /// Creates a new reader + /// + /// + public FbxAsciiWriter(Stream stream) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + this.stream = stream; + } + + /// + /// The maximum line length in characters when outputting arrays + /// + /// + /// Lines might end up being a few characters longer than this, visibly and otherwise, + /// so don't rely on it as a hard limit in code! + /// + public int MaxLineLength { get; set; } = 260; + + readonly Stack nodePath = new Stack(); + + // Adds the given node text to the string + void BuildString(FbxNode node, StringBuilder sb, bool writeArrayLength, int indentLevel = 0) + { + nodePath.Push(node.Name ?? ""); + int lineStart = sb.Length; + // Write identifier + for (int i = 0; i < indentLevel; i++) + sb.Append('\t'); + sb.Append(node.Name).Append(':'); + + // Write properties + var first = true; + for (int j = 0; j < node.Properties.Count; j++) + { + var p = node.Properties[j]; + if (p == null) + continue; + if (!first) + sb.Append(','); + sb.Append(' '); + if (p is string) + { + sb.Append('"').Append(p).Append('"'); + } + else if (p is Array) + { + var array = (Array)p; + var elementType = p.GetType().GetElementType(); + // ReSharper disable once PossibleNullReferenceException + // We know it's an array, so we don't need to check for null + if (array.Rank != 1 || !elementType.IsPrimitive) + throw new FbxException(nodePath, j, + "Invalid array type " + p.GetType()); + if (writeArrayLength) + { + sb.Append('*').Append(array.Length).Append(" {\n"); + lineStart = sb.Length; + for (int i = -1; i < indentLevel; i++) + sb.Append('\t'); + sb.Append("a: "); + } + bool pFirst = true; + foreach (var v in (Array)p) + { + if (!pFirst) + sb.Append(','); + var vstr = v.ToString(); + if ((sb.Length - lineStart) + vstr.Length >= MaxLineLength) + { + sb.Append('\n'); + lineStart = sb.Length; + } + sb.Append(vstr); + pFirst = false; + } + if (writeArrayLength) + { + sb.Append('\n'); + for (int i = 0; i < indentLevel; i++) + sb.Append('\t'); + sb.Append('}'); + } + } + else if (p is char) + sb.Append((char)p); + else if (p.GetType().IsPrimitive && p is IFormattable) + sb.Append(p); + else + throw new FbxException(nodePath, j, + "Invalid property type " + p.GetType()); + first = false; + } + + // Write child nodes + if (node.Nodes.Count > 0) + { + sb.Append(" {\n"); + foreach (var n in node.Nodes) + { + if (n == null) + continue; + BuildString(n, sb, writeArrayLength, indentLevel + 1); + } + for (int i = 0; i < indentLevel; i++) + sb.Append('\t'); + sb.Append('}'); + } + sb.Append('\n'); + + nodePath.Pop(); + } + + /// + /// Writes an FBX document to the stream + /// + /// + /// + /// ASCII FBX files have no header or footer, so you can call this multiple times + /// + public void Write(FbxDocument document) + { + if (document == null) + throw new ArgumentNullException(nameof(document)); + var sb = new StringBuilder(); + + // Write version header (a comment, but required for many importers) + var vMajor = (int)document.Version / 1000; + var vMinor = ((int)document.Version % 1000) / 100; + var vRev = ((int)document.Version % 100) / 10; + sb.Append($"; FBX {vMajor}.{vMinor}.{vRev} project file\n\n"); + + nodePath.Clear(); + foreach (var n in document.Nodes) + { + if (n == null) + continue; + BuildString(n, sb, document.Version >= FbxVersion.v7_1); + sb.Append('\n'); + } + var b = Encoding.ASCII.GetBytes(sb.ToString()); + stream.Write(b, 0, b.Length); + } + } + + + /// + /// Base class for binary stream wrappers + /// + public abstract class FbxBinary + { + // Header string, found at the top of all compliant files + private static readonly byte[] headerString + = Encoding.ASCII.GetBytes("Kaydara FBX Binary \0\x1a\0"); + + // This data was entirely calculated by me, honest. Turns out it works, fancy that! + private static readonly byte[] sourceId = + { 0x58, 0xAB, 0xA9, 0xF0, 0x6C, 0xA2, 0xD8, 0x3F, 0x4D, 0x47, 0x49, 0xA3, 0xB4, 0xB2, 0xE7, 0x3D }; + private static readonly byte[] key = + { 0xE2, 0x4F, 0x7B, 0x5F, 0xCD, 0xE4, 0xC8, 0x6D, 0xDB, 0xD8, 0xFB, 0xD7, 0x40, 0x58, 0xC6, 0x78 }; + // This wasn't - it just appears at the end of every compliant file + private static readonly byte[] extension = + { 0xF8, 0x5A, 0x8C, 0x6A, 0xDE, 0xF5, 0xD9, 0x7E, 0xEC, 0xE9, 0x0C, 0xE3, 0x75, 0x8F, 0x29, 0x0B }; + + // Number of null bytes between the footer code and the version + private const int footerZeroes1 = 20; + // Number of null bytes between the footer version and extension code + private const int footerZeroes2 = 120; + + /// + /// The size of the footer code + /// + protected const int footerCodeSize = 16; + + /// + /// The namespace separator in the binary format (remember to reverse the identifiers) + /// + protected const string binarySeparator = "\0\x1"; + + /// + /// The namespace separator in the ASCII format and in object data + /// + protected const string asciiSeparator = "::"; + + /// + /// Checks if the first part of 'data' matches 'original' + /// + /// + /// + /// true if it does, otherwise false + protected static bool CheckEqual(byte[] data, byte[] original) + { + for (int i = 0; i < original.Length; i++) + if (data[i] != original[i]) + return false; + return true; + } + + + public static bool IsBinary(Stream stream) + { + var isb = ReadHeader(stream); + stream.Position = 0; + return isb; + } + + + + /// + /// Writes the FBX header string + /// + /// + protected static void WriteHeader(Stream stream) + { + stream.Write(headerString, 0, headerString.Length); + } + + /// + /// Reads the FBX header string + /// + /// + /// true if it's compliant + protected static bool ReadHeader(Stream stream) + { + var buf = new byte[headerString.Length]; + stream.Read(buf, 0, buf.Length); + return CheckEqual(buf, headerString); + } + + // Turns out this is the algorithm they use to generate the footer. Who knew! + static void Encrypt(byte[] a, byte[] b) + { + byte c = 64; + for (int i = 0; i < footerCodeSize; i++) + { + a[i] = (byte)(a[i] ^ (byte)(c ^ b[i])); + c = a[i]; + } + } + + const string timePath1 = "FBXHeaderExtension"; + const string timePath2 = "CreationTimeStamp"; + static readonly Stack timePath = new Stack(new[] { timePath1, timePath2 }); + + // Gets a single timestamp component + static int GetTimestampVar(FbxNode timestamp, string element) + { + var elementNode = timestamp[element]; + if (elementNode != null && elementNode.Properties.Count > 0) + { + var prop = elementNode.Properties[0]; + if (prop is int || prop is long) + return (int)prop; + } + throw new FbxException(timePath, -1, "Timestamp has no " + element); + } + + /// + /// Generates the unique footer code based on the document's timestamp + /// + /// + /// A 16-byte code + protected static byte[] GenerateFooterCode(FbxNodeList document) + { + var timestamp = document.GetRelative(timePath1 + "/" + timePath2); + if (timestamp == null) + throw new FbxException(timePath, -1, "No creation timestamp"); + try + { + return GenerateFooterCode( + GetTimestampVar(timestamp, "Year"), + GetTimestampVar(timestamp, "Month"), + GetTimestampVar(timestamp, "Day"), + GetTimestampVar(timestamp, "Hour"), + GetTimestampVar(timestamp, "Minute"), + GetTimestampVar(timestamp, "Second"), + GetTimestampVar(timestamp, "Millisecond") + ); + } + catch (ArgumentOutOfRangeException) + { + throw new FbxException(timePath, -1, "Invalid timestamp"); + } + } + + /// + /// Generates a unique footer code based on a timestamp + /// + /// + /// + /// + /// + /// + /// + /// + /// A 16-byte code + protected static byte[] GenerateFooterCode( + int year, int month, int day, + int hour, int minute, int second, int millisecond) + { + if (year < 0 || year > 9999) + throw new ArgumentOutOfRangeException(nameof(year)); + if (month < 0 || month > 12) + throw new ArgumentOutOfRangeException(nameof(month)); + if (day < 0 || day > 31) + throw new ArgumentOutOfRangeException(nameof(day)); + if (hour < 0 || hour >= 24) + throw new ArgumentOutOfRangeException(nameof(hour)); + if (minute < 0 || minute >= 60) + throw new ArgumentOutOfRangeException(nameof(minute)); + if (second < 0 || second >= 60) + throw new ArgumentOutOfRangeException(nameof(second)); + if (millisecond < 0 || millisecond >= 1000) + throw new ArgumentOutOfRangeException(nameof(millisecond)); + + var str = (byte[])sourceId.Clone(); + var mangledTime = $"{second:00}{month:00}{hour:00}{day:00}{(millisecond / 10):00}{year:0000}{minute:00}"; + var mangledBytes = Encoding.ASCII.GetBytes(mangledTime); + Encrypt(str, mangledBytes); + Encrypt(str, key); + Encrypt(str, mangledBytes); + return str; + } + + /// + /// Writes the FBX footer extension (NB - not the unique footer code) + /// + /// + /// + protected void WriteFooter(BinaryWriter stream, int version) + { + var zeroes = new byte[Math.Max(footerZeroes1, footerZeroes2)]; + stream.Write(zeroes, 0, footerZeroes1); + stream.Write(version); + stream.Write(zeroes, 0, footerZeroes2); + stream.Write(extension, 0, extension.Length); + } + + static bool AllZero(byte[] array) + { + foreach (var b in array) + if (b != 0) + return false; + return true; + } + + /// + /// Reads and checks the FBX footer extension (NB - not the unique footer code) + /// + /// + /// + /// true if it's compliant + protected bool CheckFooter(BinaryReader stream, FbxVersion version) + { + var buffer = new byte[Math.Max(footerZeroes1, footerZeroes2)]; + stream.Read(buffer, 0, footerZeroes1); + bool correct = AllZero(buffer); + var readVersion = stream.ReadInt32(); + correct &= (readVersion == (int)version); + stream.Read(buffer, 0, footerZeroes2); + correct &= AllZero(buffer); + stream.Read(buffer, 0, extension.Length); + correct &= CheckEqual(buffer, extension); + return correct; + } + } + + /// + /// Reads FBX nodes from a binary stream + /// + public class FbxBinaryReader : FbxBinary + { + private readonly BinaryReader stream; + private readonly FbxErrorLevel errorLevel; + + private delegate object ReadPrimitive(BinaryReader reader); + + /// + /// Creates a new reader + /// + /// The stream to read from + /// When to throw an + /// does + /// not support seeking + public FbxBinaryReader(Stream stream, FbxErrorLevel errorLevel = FbxErrorLevel.Checked) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + if (!stream.CanSeek) + throw new ArgumentException( + "The stream must support seeking. Try reading the data into a buffer first"); + this.stream = new BinaryReader(stream, Encoding.ASCII); + this.errorLevel = errorLevel; + } + + // Reads a single property + object ReadProperty() + { + var dataType = (char)stream.ReadByte(); + switch (dataType) + { + case 'Y': + return stream.ReadInt16(); + case 'C': + return (char)stream.ReadByte(); + case 'I': + return stream.ReadInt32(); + case 'F': + return stream.ReadSingle(); + case 'D': + return stream.ReadDouble(); + case 'L': + return stream.ReadInt64(); + case 'f': + return ReadArray(br => br.ReadSingle(), typeof(float)); + case 'd': + return ReadArray(br => br.ReadDouble(), typeof(double)); + case 'l': + return ReadArray(br => br.ReadInt64(), typeof(long)); + case 'i': + return ReadArray(br => br.ReadInt32(), typeof(int)); + case 'b': + return ReadArray(br => br.ReadBoolean(), typeof(bool)); + case 'S': + var len = stream.ReadInt32(); + var str = len == 0 ? "" : Encoding.ASCII.GetString(stream.ReadBytes(len)); + // Convert \0\1 to '::' and reverse the tokens + if (str.Contains(binarySeparator)) + { + var tokens = str.Split(new[] { binarySeparator }, StringSplitOptions.None); + var sb = new StringBuilder(); + bool first = true; + for (int i = tokens.Length - 1; i >= 0; i--) + { + if (!first) + sb.Append(asciiSeparator); + sb.Append(tokens[i]); + first = false; + } + str = sb.ToString(); + } + return str; + case 'R': + return stream.ReadBytes(stream.ReadInt32()); + default: + throw new FbxException(stream.BaseStream.Position - 1, + "Invalid property data type `" + dataType + "'"); + } + } + + // Reads an array, decompressing it if required + Array ReadArray(ReadPrimitive readPrimitive, Type arrayType) + { + var len = stream.ReadInt32(); + var encoding = stream.ReadInt32(); + var compressedLen = stream.ReadInt32(); + var ret = Array.CreateInstance(arrayType, len); + var s = stream; + var endPos = stream.BaseStream.Position + compressedLen; + if (encoding != 0) + { + if (errorLevel >= FbxErrorLevel.Checked) + { + if (encoding != 1) + throw new FbxException(stream.BaseStream.Position - 1, + "Invalid compression encoding (must be 0 or 1)"); + var cmf = stream.ReadByte(); + if ((cmf & 0xF) != 8 || (cmf >> 4) > 7) + throw new FbxException(stream.BaseStream.Position - 1, + "Invalid compression format " + cmf); + var flg = stream.ReadByte(); + if (errorLevel >= FbxErrorLevel.Strict && ((cmf << 8) + flg) % 31 != 0) + throw new FbxException(stream.BaseStream.Position - 1, + "Invalid compression FCHECK"); + if ((flg & (1 << 5)) != 0) + throw new FbxException(stream.BaseStream.Position - 1, + "Invalid compression flags; dictionary not supported"); + } + else + { + stream.BaseStream.Position += 2; + } + var codec = new FbxDeflateWithChecksum(stream.BaseStream, CompressionMode.Decompress); + s = new BinaryReader(codec); + } + try + { + for (int i = 0; i < len; i++) + ret.SetValue(readPrimitive(s), i); + } + catch (InvalidDataException) + { + throw new FbxException(stream.BaseStream.Position - 1, + "Compressed data was malformed"); + } + if (encoding != 0) + { + if (errorLevel >= FbxErrorLevel.Checked) + { + stream.BaseStream.Position = endPos - sizeof(int); + var checksumBytes = new byte[sizeof(int)]; + stream.BaseStream.Read(checksumBytes, 0, checksumBytes.Length); + int checksum = 0; + for (int i = 0; i < checksumBytes.Length; i++) + checksum = (checksum << 8) + checksumBytes[i]; + if (checksum != ((FbxDeflateWithChecksum)s.BaseStream).Checksum) + throw new FbxException(stream.BaseStream.Position, + "Compressed data has invalid checksum"); + } + else + { + stream.BaseStream.Position = endPos; + } + } + return ret; + } + + /// + /// Reads a single node. + /// + /// + /// This won't read the file header or footer, and as such will fail if the stream is a full FBX file + /// + /// The node + /// The FBX data was malformed + /// for the reader's error level + public FbxNode ReadNode() + { + var endOffset = stream.ReadInt32(); + var numProperties = stream.ReadInt32(); + var propertyListLen = stream.ReadInt32(); + var nameLen = stream.ReadByte(); + var name = nameLen == 0 ? "" : Encoding.ASCII.GetString(stream.ReadBytes(nameLen)); + + if (endOffset == 0) + { + // The end offset should only be 0 in a null node + if (errorLevel >= FbxErrorLevel.Checked && (numProperties != 0 || propertyListLen != 0 || !string.IsNullOrEmpty(name))) + throw new FbxException(stream.BaseStream.Position, + "Invalid node; expected NULL record"); + return null; + } + + var node = new FbxNode { Name = name }; + + var propertyEnd = stream.BaseStream.Position + propertyListLen; + // Read properties + for (int i = 0; i < numProperties; i++) + node.Properties.Add(ReadProperty()); + + if (errorLevel >= FbxErrorLevel.Checked && stream.BaseStream.Position != propertyEnd) + throw new FbxException(stream.BaseStream.Position, + "Too many bytes in property list, end point is " + propertyEnd); + + // Read nested nodes + var listLen = endOffset - stream.BaseStream.Position; + if (errorLevel >= FbxErrorLevel.Checked && listLen < 0) + throw new FbxException(stream.BaseStream.Position, + "Node has invalid end point"); + if (listLen > 0) + { + FbxNode nested; + do + { + nested = ReadNode(); + node.Nodes.Add(nested); + } while (nested != null); + if (errorLevel >= FbxErrorLevel.Checked && stream.BaseStream.Position != endOffset) + throw new FbxException(stream.BaseStream.Position, + "Too many bytes in node, end point is " + endOffset); + } + return node; + } + + /// + /// Reads an FBX document from the stream + /// + /// The top-level node + /// The FBX data was malformed + /// for the reader's error level + public FbxDocument Read() + { + // Read header + bool validHeader = ReadHeader(stream.BaseStream); + if (errorLevel >= FbxErrorLevel.Strict && !validHeader) + throw new FbxException(stream.BaseStream.Position, + "Invalid header string"); + var document = new FbxDocument { Version = (FbxVersion)stream.ReadInt32() }; + + // Read nodes + var dataPos = stream.BaseStream.Position; + FbxNode nested; + do + { + nested = ReadNode(); + if (nested != null) + document.Nodes.Add(nested); + } while (nested != null); + + // Read footer code + var footerCode = new byte[footerCodeSize]; + stream.BaseStream.Read(footerCode, 0, footerCode.Length); + if (errorLevel >= FbxErrorLevel.Strict) + { + var validCode = GenerateFooterCode(document); + if (!CheckEqual(footerCode, validCode)) + throw new FbxException(stream.BaseStream.Position - footerCodeSize, + "Incorrect footer code"); + } + + // Read footer extension + dataPos = stream.BaseStream.Position; + var validFooterExtension = CheckFooter(stream, document.Version); + if (errorLevel >= FbxErrorLevel.Strict && !validFooterExtension) + throw new FbxException(dataPos, "Invalid footer"); + return document; + } + } + + /// + /// Writes an FBX document to a binary stream + /// + public class FbxBinaryWriter : FbxBinary + { + private readonly Stream output; + private readonly MemoryStream memory; + private readonly BinaryWriter stream; + + readonly Stack nodePath = new Stack(); + + /// + /// The minimum size of an array in bytes before it is compressed + /// + public int CompressionThreshold { get; set; } = 1024; + + /// + /// Creates a new writer + /// + /// + public FbxBinaryWriter(Stream stream) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + output = stream; + // Wrap in a memory stream to guarantee seeking + memory = new MemoryStream(); + this.stream = new BinaryWriter(memory, Encoding.ASCII); + } + + private delegate void PropertyWriter(BinaryWriter sw, object obj); + + struct WriterInfo + { + public readonly char id; + public readonly PropertyWriter writer; + + public WriterInfo(char id, PropertyWriter writer) + { + this.id = id; + this.writer = writer; + } + } + + private static readonly Dictionary writePropertyActions + = new Dictionary + { + { typeof(int), new WriterInfo('I', (sw, obj) => sw.Write((int)obj)) }, + { typeof(short), new WriterInfo('Y', (sw, obj) => sw.Write((short)obj)) }, + { typeof(long), new WriterInfo('L', (sw, obj) => sw.Write((long)obj)) }, + { typeof(float), new WriterInfo('F', (sw, obj) => sw.Write((float)obj)) }, + { typeof(double), new WriterInfo('D', (sw, obj) => sw.Write((double)obj)) }, + { typeof(bool), new WriterInfo('C', (sw, obj) => sw.Write((byte)(char)obj)) }, + { typeof(byte[]), new WriterInfo('R', WriteRaw) }, + { typeof(string), new WriterInfo('S', WriteString) }, + // null elements indicate arrays - they are checked again with their element type + { typeof(int[]), new WriterInfo('i', null) }, + { typeof(long[]), new WriterInfo('l', null) }, + { typeof(float[]), new WriterInfo('f', null) }, + { typeof(double[]), new WriterInfo('d', null) }, + { typeof(bool[]), new WriterInfo('b', null) }, + }; + + static void WriteRaw(BinaryWriter stream, object obj) + { + var bytes = (byte[])obj; + stream.Write(bytes.Length); + stream.Write(bytes); + } + + static void WriteString(BinaryWriter stream, object obj) + { + var str = obj.ToString(); + // Replace "::" with \0\1 and reverse the tokens + if (str.Contains(asciiSeparator)) + { + var tokens = str.Split(new[] { asciiSeparator }, StringSplitOptions.None); + var sb = new StringBuilder(); + bool first = true; + for (int i = tokens.Length - 1; i >= 0; i--) + { + if (!first) + sb.Append(binarySeparator); + sb.Append(tokens[i]); + first = false; + } + str = sb.ToString(); + } + var bytes = Encoding.ASCII.GetBytes(str); + stream.Write(bytes.Length); + stream.Write(bytes); + } + + void WriteArray(Array array, Type elementType, PropertyWriter writer) + { + stream.Write(array.Length); + + var size = array.Length * Marshal.SizeOf(elementType); + bool compress = size >= CompressionThreshold; + stream.Write(compress ? 1 : 0); + + var sw = stream; + FbxDeflateWithChecksum codec = null; + + var compressLengthPos = stream.BaseStream.Position; + stream.Write(0); // Placeholder compressed length + var dataStart = stream.BaseStream.Position; + if (compress) + { + stream.Write(new byte[] { 0x58, 0x85 }, 0, 2); // Header bytes for DeflateStream settings + codec = new FbxDeflateWithChecksum(stream.BaseStream, CompressionMode.Compress, true); + sw = new BinaryWriter(codec); + } + foreach (var obj in array) + writer(sw, obj); + if (compress) + { + codec.Close(); // This is important - otherwise bytes can be incorrect + var checksum = codec.Checksum; + byte[] bytes = + { + (byte)((checksum >> 24) & 0xFF), + (byte)((checksum >> 16) & 0xFF), + (byte)((checksum >> 8) & 0xFF), + (byte)(checksum & 0xFF), + }; + stream.Write(bytes); + } + + // Now we can write the compressed data length, since we know the size + if (compress) + { + var dataEnd = stream.BaseStream.Position; + stream.BaseStream.Position = compressLengthPos; + stream.Write((int)(dataEnd - dataStart)); + stream.BaseStream.Position = dataEnd; + } + } + + void WriteProperty(object obj, int id) + { + if (obj == null) + return; + WriterInfo writerInfo; + if (!writePropertyActions.TryGetValue(obj.GetType(), out writerInfo)) + throw new FbxException(nodePath, id, + "Invalid property type " + obj.GetType()); + stream.Write((byte)writerInfo.id); + // ReSharper disable once AssignNullToNotNullAttribute + if (writerInfo.writer == null) // Array type + { + var elementType = obj.GetType().GetElementType(); + WriteArray((Array)obj, elementType, writePropertyActions[elementType].writer); + } + else + writerInfo.writer(stream, obj); + } + + // Data for a null node + static readonly byte[] nullData = new byte[13]; + + // Writes a single document to the buffer + void WriteNode(FbxNode node) + { + if (node == null) + { + stream.BaseStream.Write(nullData, 0, nullData.Length); + } + else + { + nodePath.Push(node.Name ?? ""); + var name = string.IsNullOrEmpty(node.Name) ? null : Encoding.ASCII.GetBytes(node.Name); + if (name != null && name.Length > byte.MaxValue) + throw new FbxException(stream.BaseStream.Position, + "Node name is too long"); + + // Header + var endOffsetPos = stream.BaseStream.Position; + stream.Write(0); // End offset placeholder + stream.Write(node.Properties.Count); + var propertyLengthPos = stream.BaseStream.Position; + stream.Write(0); // Property length placeholder + stream.Write((byte)(name?.Length ?? 0)); + if (name != null) + stream.Write(name); + + // Write properties and length + var propertyBegin = stream.BaseStream.Position; + for (int i = 0; i < node.Properties.Count; i++) + { + WriteProperty(node.Properties[i], i); + } + var propertyEnd = stream.BaseStream.Position; + stream.BaseStream.Position = propertyLengthPos; + stream.Write((int)(propertyEnd - propertyBegin)); + stream.BaseStream.Position = propertyEnd; + + // Write child nodes + if (node.Nodes.Count > 0) + { + foreach (var n in node.Nodes) + { + if (n == null) + continue; + WriteNode(n); + } + WriteNode(null); + } + + // Write end offset + var dataEnd = stream.BaseStream.Position; + stream.BaseStream.Position = endOffsetPos; + stream.Write((int)dataEnd); + stream.BaseStream.Position = dataEnd; + + nodePath.Pop(); + } + } + + /// + /// Writes an FBX file to the output + /// + /// + public void Write(FbxDocument document) + { + stream.BaseStream.Position = 0; + WriteHeader(stream.BaseStream); + stream.Write((int)document.Version); + // TODO: Do we write a top level node or not? Maybe check the version? + nodePath.Clear(); + foreach (var node in document.Nodes) + WriteNode(node); + WriteNode(null); + stream.Write(GenerateFooterCode(document)); + WriteFooter(stream, (int)document.Version); + output.Write(memory.GetBuffer(), 0, (int)memory.Position); + } + } + + + + /// + /// A top-level FBX node + /// + public class FbxDocument : FbxNodeList + { + /// + /// Describes the format and data of the document + /// + /// + /// It isn't recommended that you change this value directly, because + /// it won't change any of the document's data which can be version-specific. + /// Most FBX importers can cope with any version. + /// + public FbxVersion Version { get; set; } = FbxVersion.v7_4; + + + /// + /// Creates connections between objects and returns the root nodes. + /// (added by dexyfex) + /// + /// + public List GetSceneNodes() + { + var fobjs = this["Objects"]; + if (fobjs?.Nodes == null) + return null; + + var fconns = this["Connections"]; + if (fconns?.Nodes == null) + return null; + + var fobjdict = new Dictionary(); + var rootnodes = new List(); + + foreach (var node in fobjs.Nodes) //put all the object nodes into a decktionary for the connections + { + if (node == null) continue; + long id = 0; + if (node.Value is long) + { id = (long)node.Value; } + if (id == 0) + { }//shouldn't happen.. + fobjdict[id] = node; + } + + foreach (var node in fconns.Nodes) //build the scene hierarchy by adding connections to object nodes + { + if (node == null) continue; + var connType = node.Value as string; + if ((connType == "OO") || (connType == "OP")) + { + if (node.Properties.Count < 3) { continue; } + if (!(node.Properties[1] is long)) { continue; } + if (!(node.Properties[2] is long)) { continue; } + long cid = (long)node.Properties[1]; + long pid = (long)node.Properties[2]; + FbxNode cnode; + FbxNode pnode; + fobjdict.TryGetValue(cid, out cnode); + fobjdict.TryGetValue(pid, out pnode); + if (cnode == null) { continue; } + if (pnode == null) + { + rootnodes.Add(cnode); + } + else + { + pnode.Connections.Add(cnode); + } + } + else + { } + } + + return rootnodes; + } + + } + + /// + /// An error with the FBX data input + /// + public class FbxException : Exception + { + /// + /// An error at a binary stream offset + /// + /// + /// + public FbxException(long position, string message) : + base($"{message}, near offset {position}") + { + } + + /// + /// An error in a text file + /// + /// + /// + /// + public FbxException(int line, int column, string message) : + base($"{message}, near line {line} column {column}") + { + } + + /// + /// An error in a node object + /// + /// + /// + /// + public FbxException(Stack nodePath, int propertyID, string message) : + base(message + ", at " + string.Join("/", nodePath.ToArray()) + (propertyID < 0 ? "" : $"[{propertyID}]")) + { + } + } + + /// + /// Represents a node in an FBX file + /// + public class FbxNode : FbxNodeList + { + /// + /// The node name, which is often a class type + /// + /// + /// The name must be smaller than 256 characters to be written to a binary stream + /// + public string Name { get; set; } + + /// + /// The list of properties associated with the node + /// + /// + /// Supported types are primitives (apart from byte and char),arrays of primitives, and strings + /// + public List Properties { get; } = new List(); + + /// + /// List of FbxNodes that are connected to this node via the Connections section. + /// (Added by dexyfex, used by FbxConverter) + /// + public List Connections { get; } = new List(); + + /// + /// The first property element + /// + public object Value + { + get { return Properties.Count < 1 ? null : Properties[0]; } + set + { + if (Properties.Count < 1) + Properties.Add(value); + else + Properties[0] = value; + } + } + + /// + /// Whether the node is empty of data + /// + public bool IsEmpty => string.IsNullOrEmpty(Name) && Properties.Count == 0 && Nodes.Count == 0; + + public override string ToString() + { + return Name + ((Value != null) ? (": " + Value.ToString()) : ""); + } + } + + /// + /// Base class for nodes and documents + /// + public abstract class FbxNodeList + { + /// + /// The list of child/nested nodes + /// + /// + /// A list with one or more null elements is treated differently than an empty list, + /// and represented differently in all FBX output files. + /// + public List Nodes { get; } = new List(); + + /// + /// Gets a named child node + /// + /// + /// The child node, or null + public FbxNode this[string name] { get { return Nodes.Find(n => n != null && n.Name == name); } } + + /// + /// Gets a child node, using a '/' separated path + /// + /// + /// The child node, or null + public FbxNode GetRelative(string path) + { + var tokens = path.Split('/'); + FbxNodeList n = this; + foreach (var t in tokens) + { + if (t == "") + continue; + n = n[t]; + if (n == null) + break; + } + return n as FbxNode; + } + } + + /// + /// Enumerates the FBX file versions + /// + public enum FbxVersion + { + /// + /// FBX version 6.0 + /// + v6_0 = 6000, + + /// + /// FBX version 6.1 + /// + v6_1 = 6100, + + /// + /// FBX version 7.0 + /// + v7_0 = 7000, + + /// + /// FBX 2011 version + /// + v7_1 = 7100, + + /// + /// FBX 2012 version + /// + v7_2 = 7200, + + /// + /// FBX 2013 version + /// + v7_3 = 7300, + + /// + /// FBX 2014 version + /// + v7_4 = 7400, + } + + + + /// + /// Indicates when a reader should throw errors + /// + public enum FbxErrorLevel + { + /// + /// Ignores inconsistencies unless the parser can no longer continue + /// + Permissive = 0, + + /// + /// Checks data integrity, such as checksums and end points + /// + Checked = 1, + + /// + /// Checks everything, including magic bytes + /// + Strict = 2, + } + + + /// + /// A wrapper for DeflateStream that calculates the Adler32 checksum of the payload + /// + public class FbxDeflateWithChecksum : DeflateStream + { + private const int modAdler = 65521; + private uint checksumA; + private uint checksumB; + + /// + /// Gets the Adler32 checksum at the current point in the stream + /// + public int Checksum + { + get + { + checksumA %= modAdler; + checksumB %= modAdler; + return (int)((checksumB << 16) | checksumA); + } + } + + /// + public FbxDeflateWithChecksum(Stream stream, CompressionMode mode) : base(stream, mode) + { + ResetChecksum(); + } + + /// + public FbxDeflateWithChecksum(Stream stream, CompressionMode mode, bool leaveOpen) : base(stream, mode, leaveOpen) + { + ResetChecksum(); + } + + // Efficiently extends the checksum with the given buffer + void CalcChecksum(byte[] array, int offset, int count) + { + checksumA %= modAdler; + checksumB %= modAdler; + for (int i = offset, c = 0; i < (offset + count); i++, c++) + { + checksumA += array[i]; + checksumB += checksumA; + if (c > 4000) // This is about how many iterations it takes for B to reach IntMax + { + checksumA %= modAdler; + checksumB %= modAdler; + c = 0; + } + } + } + + /// + public override void Write(byte[] array, int offset, int count) + { + base.Write(array, offset, count); + CalcChecksum(array, offset, count); + } + + /// + public override int Read(byte[] array, int offset, int count) + { + var ret = base.Read(array, offset, count); + CalcChecksum(array, offset, count); + return ret; + } + + /// + /// Initializes the checksum values + /// + public void ResetChecksum() + { + checksumA = 1; + checksumB = 0; + } + } + +} diff --git a/CodeWalker.Core/Utils/FbxConverter.cs b/CodeWalker.Core/Utils/FbxConverter.cs new file mode 100644 index 0000000..e868887 --- /dev/null +++ b/CodeWalker.Core/Utils/FbxConverter.cs @@ -0,0 +1,1010 @@ +using CodeWalker.GameFiles; +using SharpDX; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CodeWalker +{ + public class FbxConverter + { + + + + public YdrFile ConvertToYdr(string name, byte[] fbxdata) + { + var fdoc = FbxIO.Read(fbxdata); + if (fdoc == null) + return null; + + var dwbl = TryConvertDrawable(fdoc, name); + + + YdrFile ydr = new YdrFile(); + ydr.Drawable = dwbl; + + //byte[] ddata = ydr.Save(); + //File.WriteAllBytes("C:\\Dump\\TestFbx\\" + name + ".ydr", ddata); + + + return ydr; + } + + + + private Drawable TryConvertDrawable(FbxDocument fdoc, string name) + { + + var rootnodes = fdoc.GetSceneNodes(); + + var mlists = new List>(); + var mlistall = new List(); + + foreach (var node in rootnodes) + { + if (node.Name == "Model") + { + var mlist = TryConvertModels(node); //flatten any models structure with depth >2 + if (mlist != null) + { + mlists.Add(mlist); + mlistall.AddRange(mlist); + } + } + } + + var mlHigh = new List(); + var mlMed = new List(); + var mlLow = new List(); + var mlVlow = new List(); + var mlUnks = new List(); + var mlAll = new List(); + foreach (var m in mlistall) + { + var mnl = m.Name.ToLowerInvariant(); + if (mnl.EndsWith("_vlow")) + { + mlVlow.Add(m.Model); + } + else if (mnl.EndsWith("_low")) + { + mlLow.Add(m.Model); + } + else if (mnl.EndsWith("_med")) + { + mlMed.Add(m.Model); + } + else if (mnl.EndsWith("_high")) + { + mlHigh.Add(m.Model); + } + else + { + mlUnks.Add(m.Model); + } + } + if (mlHigh.Count == 0)//mlUnks could be embedded collisions... ignore for now + { + mlHigh.AddRange(mlUnks); + } + mlAll.AddRange(mlHigh); + mlAll.AddRange(mlMed); + mlAll.AddRange(mlLow); + mlAll.AddRange(mlVlow); + + + + var allVerts = new List(); + var bbMin = new Vector3(float.MaxValue); + var bbMax = new Vector3(float.MinValue); + var bsCen = Vector3.Zero; + var bsRad = 0.0f; + foreach (var m in mlistall) + { + if (m?.Model?.Geometries?.data_items == null) continue; + foreach (var g in m.Model.Geometries.data_items) + { + var vb = g.VertexData.VertexBytes; + var vs = g.VertexData.VertexStride; + var vc = g.VertexData.VertexCount; + for (int i = 0; i < vc; i++) + { + var vp = MetaTypes.ConvertData(vb, i * vs);//position offset should always be 0! + allVerts.Add(vp); + bbMin = Vector3.Min(bbMin, vp); + bbMax = Vector3.Max(bbMax, vp); + //bsCen += vp; + } + } + } + if (allVerts.Count > 0) + { + //bsCen = bsCen / allVerts.Count; + bsCen = (bbMin + bbMax) * 0.5f; + foreach (var vp in allVerts) + { + bsRad = Math.Max(bsRad, (vp - bsCen).Length()); + } + } + + + + var sgrp = new ShaderGroup(); + var slist = new List(); + var smapp = new List(); + foreach (var m in mlAll) + { + if (m?.Geometries?.data_items == null) continue; + smapp.Clear(); + foreach (var g in m.Geometries.data_items) + { + smapp.Add((ushort)slist.Count); + slist.Add(g.Shader); + } + m.ShaderMapping = smapp.ToArray();//TODO: re-use shaders!! + } + sgrp.Shaders = new ResourcePointerArray64(); + sgrp.Shaders.data_items = slist.ToArray(); + sgrp.ShadersCount1 = (ushort)slist.Count; + sgrp.ShadersCount2 = (ushort)slist.Count; + sgrp.VFT = 1080113376;//is this needed? + sgrp.Unknown_4h = 1; + sgrp.Unknown_30h = (uint)(8 + slist.Count*3);//WTF is this? + + + var d = new Drawable(); + d.Name = name + ".#dr"; + d.ShaderGroup = sgrp; + d.BoundingCenter = bsCen; + d.BoundingSphereRadius = bsRad; + d.BoundingBoxMin = new Vector4(bbMin, float.NaN); + d.BoundingBoxMax = new Vector4(bbMax, float.NaN); + d.LodDistHigh = 9998;//lod dist defaults + d.LodDistMed = 9998; + d.LodDistLow = 9998; + d.LodDistVlow = 9998; + d.Unknown_80h = 0;//TODO: figure these out! related to high/med/low/vlow 0xFF00 + ? + d.Unknown_84h = 0; + d.Unknown_88h = 0; + d.Unknown_8Ch = 0; + d.Unknown_9Ah = 33;//WTF is this??? + d.FileVFT = 1079446584; + d.FileUnknown = 1; + if (mlHigh.Count > 0) + { + d.DrawableModelsHigh = new ResourcePointerList64(); + d.DrawableModelsHigh.data_items = mlHigh.ToArray(); + d.Unknown_80h = 65281;//WTF is this??? 0xFF00 + ? + } + if (mlMed.Count > 0) + { + d.DrawableModelsMedium = new ResourcePointerList64(); + d.DrawableModelsMedium.data_items = mlMed.ToArray(); + d.LodDistHigh = bsRad * 2.0f; //when med models present, generate a high lod dist.. + d.Unknown_84h = 65281;//WTF is this??? 0xFF00 + ? + } + if (mlLow.Count > 0) + { + d.DrawableModelsLow = new ResourcePointerList64(); + d.DrawableModelsLow.data_items = mlLow.ToArray(); + d.LodDistMed = bsRad * 8.0f; //when low models present, generate a med lod dist.. + d.Unknown_88h = 65281;//WTF is this??? 0xFF00 + ? + } + if (mlVlow.Count > 0) + { + d.DrawableModelsVeryLow = new ResourcePointerList64(); + d.DrawableModelsVeryLow.data_items = mlVlow.ToArray(); + d.LodDistLow = bsRad * 32.0f; //when vlow models present, generate a low lod dist.. + d.Unknown_8Ch = 65281;//WTF is this??? 0xFF00 + ? + } + d.DrawableModelsX = d.DrawableModelsHigh; + + d.LightAttributes = new ResourceSimpleList64_s(); + //todo: light attributes? + + + return d; + } + + + + private List TryConvertModels(FbxNode mnode) + { + var result = new List(); + + var nodemodel = TryConvertModel(mnode); + if (nodemodel != null) + { + result.Add(nodemodel); + } + + foreach (var cnode in mnode.Connections) + { + if (cnode.Name == "Model") + { + var mlist = TryConvertModels(cnode); + if (mlist != null) + { + result.AddRange(mlist); + } + } + } + + return result; + } + + private FbxModel TryConvertModel(FbxNode mnode) + { + + FbxNode geonode = null; + var matnodes = new List(); + foreach (var cnode in mnode.Connections) + { + if (cnode == null) continue; + switch (cnode.Name) + { + case "Geometry": + geonode = cnode; + break; + case "Material": + matnodes.Add(cnode); + break; + } + } + + if (geonode == null) return null; + if (matnodes.Count == 0) + return null; //need atleast one material... + + var fnEdges = geonode["Edges"]?.Value as int[];//do we need this? maybe for collision/navmesh + var fnVerts = geonode["Vertices"]?.Value as double[]; + var fnIndices = geonode["PolygonVertexIndex"]?.Value as int[]; + + if ((fnVerts == null) || (fnIndices == null)) + { return null; } //no mesh data.. abort! + + var fnNormals = new List(); + var fnBinormals = new List(); + var fnTangents = new List(); + var fnTexcoords = new List(); + var fnColours = new List(); + var fnMaterials = new List(); + + foreach (var cnode in geonode.Nodes) + { + if (cnode == null) continue; + switch (cnode.Name) + { + case "LayerElementNormal": fnNormals.Add(cnode); break; + case "LayerElementBinormal": fnBinormals.Add(cnode); break; + case "LayerElementTangent": fnTangents.Add(cnode); break; + case "LayerElementUV": fnTexcoords.Add(cnode); break; + case "LayerElementColor": fnColours.Add(cnode); break; + case "LayerElementMaterial": fnMaterials.Add(cnode); break; + case "LayerElementSmoothing": break;//ignore currently + case "Layer": break;//ignore- merge all layers data instead + } + } + + var nNormals = fnNormals.Count; + var nBinormals = fnBinormals.Count; + var nTangents = fnTangents.Count; + var nTexcoords = fnTexcoords.Count; + var nColours = fnColours.Count; + var nMaterials = fnMaterials.Count; + + var fPolys = new List(); + var fPolyVerts = new List(); + var fPolysByMat = new List[matnodes.Count]; + + foreach (var fnIndex in fnIndices) //build the polygons. + { + var pVert = new FbxVertex(); + pVert.Position = GetVector3FromDoubleArray(fnVerts, (fnIndex < 0) ? (-fnIndex-1) : fnIndex); + pVert.Normals = nNormals > 0 ? new Vector3[nNormals] : null; + pVert.Binormals = nBinormals > 0 ? new Vector3[nBinormals] : null; + pVert.Tangents = nTangents > 0 ? new Vector3[nTangents] : null; + pVert.Texcoords = nTexcoords > 0 ? new Vector2[nTexcoords] : null; + pVert.Colours = nColours > 0 ? new Vector4[nColours] : null; + fPolyVerts.Add(pVert); + if (fnIndex < 0) //yeah because negative index means end of polygon... + { + var fPoly = new FbxPolygon(); + fPoly.Vertices = fPolyVerts.ToArray(); + fPoly.Materials = nMaterials > 0 ? new FbxNode[nMaterials] : null; + fPolyVerts.Clear(); + fPolys.Add(fPoly); + if (fPoly.Vertices.Length > 3) + { } //more than 3 vertices in this poly! will need to split it into triangles!! but do it later since all poly verts are needed for next steps + } + } + + for (int i = 0; i < nNormals; i++) + { + var fnNorms = fnNormals[i]; + var arNorms = fnNorms["Normals"]?.Value as double[]; + var aiNorms = fnNorms["NormalIndex"]?.Value as int[]; + if (!IsByPolygonVertexMapType(fnNorms)) + { continue; } + var indexed = IsIndexToDirectRefType(fnNorms); + if (indexed && (aiNorms == null)) + { continue; } //need the index array if it's IndexToDirect! + int j = 0; + foreach (var fPoly in fPolys) + { + foreach (var fVert in fPoly.Vertices) + { + var ai = indexed ? aiNorms[j] : j; + fVert.Normals[i] = GetVector3FromDoubleArray(arNorms, ai); + j++; + } + } + } + for (int i = 0; i < nBinormals; i++) + { + var fnBinorms = fnBinormals[i]; + var arBinorms = fnBinorms["Binormals"]?.Value as double[]; + var aiBinorms = fnBinorms["BinormalIndex"]?.Value as int[]; + if (!IsByPolygonVertexMapType(fnBinorms)) + { continue; } + var indexed = IsIndexToDirectRefType(fnBinorms); + if (indexed && (aiBinorms == null)) + { continue; } //need the index array if it's IndexToDirect! + int j = 0; + foreach (var fPoly in fPolys) + { + foreach (var fVert in fPoly.Vertices) + { + var ai = indexed ? aiBinorms[j] : j; + fVert.Binormals[i] = GetVector3FromDoubleArray(arBinorms, ai); + j++; + } + } + } + for (int i = 0; i < nTangents; i++) + { + var fnTangs = fnTangents[i]; + var arTangs = fnTangs["Tangents"]?.Value as double[]; + var aiTangs = fnTangs["TangentIndex"]?.Value as int[]; + if (!IsByPolygonVertexMapType(fnTangs)) + { continue; } + var indexed = IsIndexToDirectRefType(fnTangs); + if (indexed && (aiTangs == null)) + { continue; } //need the index array if it's IndexToDirect! + int j = 0; + foreach (var fPoly in fPolys) + { + foreach (var fVert in fPoly.Vertices) + { + var ai = indexed ? aiTangs[j] : j; + fVert.Tangents[i] = GetVector3FromDoubleArray(arTangs, ai); + j++; + } + } + } + for (int i = 0; i < nTexcoords; i++) + { + var fnTexcs = fnTexcoords[i]; + var arTexcs = fnTexcs["UV"]?.Value as double[]; + var aiTexcs = fnTexcs["UVIndex"]?.Value as int[]; + if (!IsByPolygonVertexMapType(fnTexcs)) + { continue; } + var indexed = IsIndexToDirectRefType(fnTexcs); + if (indexed && (aiTexcs == null)) + { continue; } //need the index array if it's IndexToDirect! + int j = 0; + foreach (var fPoly in fPolys) + { + foreach (var fVert in fPoly.Vertices) + { + var ai = indexed ? aiTexcs[j] : j; + fVert.Texcoords[i] = GetVector2FromDoubleArray(arTexcs, ai); + j++; + } + } + } + for (int i = 0; i < nColours; i++) + { + var fnCols = fnColours[i]; + var arCols = fnCols["Colors"]?.Value as double[]; + var aiCols = fnCols["ColorIndex"]?.Value as int[]; + if (!IsByPolygonVertexMapType(fnCols)) + { continue; } + var indexed = IsIndexToDirectRefType(fnCols); + if (indexed && (aiCols == null)) + { continue; } //need the index array if it's IndexToDirect! + int j = 0; + foreach (var fPoly in fPolys) + { + foreach (var fVert in fPoly.Vertices) + { + var ai = indexed ? aiCols[j] : j; + fVert.Colours[i] = GetVector4FromDoubleArray(arCols, ai); + j++; + } + } + } + for (int i = 0; i < nMaterials; i++) + { + var fnMats = fnMaterials[i]; + var arMats = fnMats["Materials"]?.Value as int[]; + var mapType = fnMats["MappingInformationType"]?.Value as string; + var refType = fnMats["ReferenceInformationType"]?.Value as string; + var allSame = false; + switch (mapType) + { + case "ByPolygon": break; + case "AllSame": allSame = true; break; + default: + continue; + } + switch (refType) + { + case "IndexToDirect": break; + default: + continue; + } + for (int j = 0; j < fPolys.Count; j++) + { + var fPoly = fPolys[j]; + var iMat = allSame ? arMats[0] : arMats[j]; + fPoly.Materials[i] = matnodes[iMat]; + + //group all the polygons by material... + var matPolys = fPolysByMat[iMat]; + if (matPolys == null) + { + matPolys = new List(); + fPolysByMat[iMat] = matPolys; + } + matPolys.Add(fPoly); + } + } + + + + + var dModel = new DrawableModel(); + + var dGeoms = new List(); + var dGeomAABBs = new List(); + var dModelAABB = new AABB_s(); + for (int i = 0; i < fPolysByMat.Length; i++) + { + AABB_s dGeomAABB; + var dGeom = TryConvertGeometry(fPolysByMat[i], matnodes[i], out dGeomAABB); + if (dGeom != null) + { + dGeoms.Add(dGeom); + dGeomAABBs.Add(dGeomAABB); + } + } + if (dGeomAABBs.Count > 1)//need to include whole model AABB first, if more than one geometry.. + { + var dGeomAABBs2 = new List(); + dModelAABB.Min = new Vector4(float.MaxValue); + dModelAABB.Max = new Vector4(float.MinValue); + foreach (var aabb in dGeomAABBs) + { + dModelAABB.Min = Vector4.Min(dModelAABB.Min, aabb.Min); + dModelAABB.Max = Vector4.Max(dModelAABB.Max, aabb.Max); + } + dGeomAABBs2.Add(dModelAABB); + dGeomAABBs2.AddRange(dGeomAABBs); + dGeomAABBs = dGeomAABBs2; + } + + + dModel.VFT = 1080101496;//is this needed? + dModel.Unknown_4h = 1; + dModel.RenderMaskFlags = 0x00FF; //GIMS "Mask" + dModel.Geometries = new ResourcePointerArray64(); + dModel.Geometries.data_items = dGeoms.ToArray(); + dModel.GeometriesCount1 = (ushort)dGeoms.Count; + dModel.GeometriesCount2 = (ushort)dGeoms.Count; + dModel.GeometriesCount3 = (ushort)dGeoms.Count; + dModel.BoundsData = dGeomAABBs.ToArray(); + //shader mappings array will be added when adding models to drawable. + + + + var fModel = new FbxModel(); + fModel.Name = (mnode.Properties.Count > 1) ? (mnode.Properties[1] as string)?.Replace("Model::", "") : null; + fModel.Node = mnode; + fModel.Model = dModel; + + return fModel; + } + + private DrawableGeometry TryConvertGeometry(List fPolys, FbxNode matNode, out AABB_s aabb) + { + aabb = new AABB_s(); + + if (matNode == null) return null; + if (fPolys == null) return null; + if (fPolys.Count == 0) return null; + + + var dShader = TryConvertMaterial(matNode); + var dVertDecl = GetVertexDeclaration(dShader); + + var vDict = new Dictionary();//TODO:use final vertex data instead of FbxVertex!!! + var vList = new List(); + var iList = new List(); + + foreach (var fPoly in fPolys) + { + if (fPoly.Vertices == null) continue; + if (vList.Count >= 65535) + break;//too many vertices in this geometry!! + + ushort i0 = 0;//first generated index + ushort iP = 0;//previous generated index + ushort iN = 0;//current index + for (int v = 0; v < fPoly.Vertices.Length; v++) + { + var vert = fPoly.Vertices[v]; + vert.GenVertexBytes(dVertDecl); + + if (!vDict.TryGetValue(vert, out iN)) + { + iN = (ushort)vList.Count; + vDict[vert] = iN; + vList.Add(vert); + } + else + { }//found identical vertex, use its index + if (v == 0) i0 = iN; + if (v < 3) //first triangle + { + iList.Add(iN); + } + else //for each extra vertex, make triangle from v0, vN-1, vN - assumes convex polygon!! + { + iList.Add(i0); + iList.Add(iP); + iList.Add(iN); + } + iP = iN; + } + } + + + var vStride = dVertDecl.Stride; + var vBytes = new byte[vList.Count * vStride]; + for (int i = 0; i < vList.Count; i++) + { + var v = vList[i].Bytes; + var o = i * vStride; + for (int j = 0; j < vStride; j++) + { + vBytes[o + j] = v[j]; + } + } + + if (vList.Count > 0) + { + aabb.Min = new Vector4(float.MaxValue); + aabb.Max = new Vector4(float.MinValue); + foreach (var vert in vList) + { + var v = new Vector4(vert.Position, vert.Position.X); + aabb.Min = Vector4.Min(aabb.Min, v); + aabb.Max = Vector4.Max(aabb.Max, v); + } + } + + + var vData = new VertexData(); + vData.info = dVertDecl; + vData.VertexType = (VertexType)dVertDecl.Flags; + vData.VertexStride = dVertDecl.Stride; + vData.VertexCount = vList.Count; + vData.VertexBytes = vBytes; + + var vBuff = new VertexBuffer(); + vBuff.Data1 = vData; + vBuff.Data2 = vData; + vBuff.Info = dVertDecl; + vBuff.VertexCount = (uint)vList.Count; + vBuff.VertexStride = vStride; + vBuff.VFT = 1080153064;//is this needed? + vBuff.Unknown_4h = 1; + + var iBuff = new IndexBuffer(); + iBuff.IndicesCount = (uint)iList.Count; + iBuff.Indices = iList.ToArray(); + iBuff.VFT = 1080111576;//is this needed? + iBuff.Unknown_4h = 1; + + + var dGeom = new DrawableGeometry(); + dGeom.Shader = dShader; + dGeom.VertexData = vData; + dGeom.VertexBuffer = vBuff; + dGeom.IndexBuffer = iBuff; + dGeom.VFT = 1080133736;//is this needed? + dGeom.Unknown_4h = 1; + dGeom.IndicesCount = (uint)iList.Count; + dGeom.TrianglesCount = (uint)iList.Count / 3; + dGeom.VerticesCount = (ushort)vList.Count; + dGeom.Unknown_62h = 3; //indices per triangle..? + dGeom.VertexStride = vStride; + dGeom.BoneIdsCount = 0;//todo: bones + + + return dGeom; + } + + private ShaderFX TryConvertMaterial(FbxNode matNode) + { + var shader = new ShaderFX(); + + var spsName = "default"; + var texConns = new List(); + var texNames = new List(); + + #region 3dsmax/GIMS properties + //var floatValueNames = new List(); + //var floatValues = new List(); + //var texValueNames = new List(); + //var texValues = new List(); + //var matProps = matNode["Properties70"]; + //foreach (var matProp in matProps.Nodes)//currently broken due to GIMS not doing things right + //{ + // if (matProp == null) continue; + // if (matProp.Name != "P") continue; + // var propStr = GetStringFromObjectList(matProp.Properties, 4); + // var propId = matProp.Value as string; + // if (propId == null) continue; + // if (propId == "3dsMax|params|SPSName") spsName = propStr?.ToLowerInvariant() ?? "default"; + // if (propId.StartsWith("3dsMax|params|FloatValueNames|FloatValueNames")) floatValueNames.Add(propStr); + // if (propId.StartsWith("3dsMax|params|FloatValues|FloatValues")) floatValues.Add(GetVector4FromObjectList(matProp.Properties, 4)); + // if (propId.StartsWith("3dsMax|params|TexValueNames|TexValueNames")) texValueNames.Add(propStr); + // if (propId.StartsWith("3dsMax|params|TexValues|TexValues")) texValues.Add(matProp); + //} + #endregion + + foreach (var conn in matNode.Connections) + { + if (conn.Name == "Texture") + { + texConns.Add(conn); + var texName = GetStringFromObjectList(conn.Properties, 1)?.Replace("Texture::", ""); + var ftexName = conn["FileName"]?.Value as string; + if (ftexName != null) + { + try + { + texName = Path.GetFileNameWithoutExtension(ftexName); + } + catch + { } + } + texNames.Add(texName); + } + } + + if (texNames.Count > 1) + { + spsName = "normal"; + } + + var spsFileName = spsName + ".sps"; + + shader.Name = JenkHash.GenHash(spsName); + shader.FileName = JenkHash.GenHash(spsFileName); + + shader.ParametersList = new ShaderParametersBlock(); + var paramsBlock = shader.ParametersList; + var pNames = new List(); + var pVals = new List(); + + + shader.Unknown_Ch = 0; + shader.RenderBucket = 0; + shader.Unknown_12h = 32768;//shrugs + shader.Unknown_1Ch = 0; + shader.Unknown_24h = 0; + shader.Unknown_26h = 0; + shader.Unknown_28h = 0; + shader.Unknown_2Ch = 0; + + + switch (spsName) + { + default: + case "default": + //shader.RenderBucket = 3; + //shader.ParameterSize = 208; + //shader.ParameterDataSize = 272; + AddShaderParam(pNames, pVals, MetaName.DiffuseSampler, GetTextureBaseParam(texNames, 0));//assume first texture is diffuse... + AddShaderParam(pNames, pVals, MetaName.matMaterialColorScale, new Vector4(1, 0, 0, 1)); + AddShaderParam(pNames, pVals, MetaName.HardAlphaBlend, new Vector4(0, 0, 0, 0)); + AddShaderParam(pNames, pVals, MetaName.useTessellation, new Vector4(0, 0, 0, 0)); + AddShaderParam(pNames, pVals, MetaName.wetnessMultiplier, new Vector4(1, 0, 0, 0)); + AddShaderParam(pNames, pVals, MetaName.globalAnimUV1, new Vector4(0, 1, 0, 0)); + AddShaderParam(pNames, pVals, MetaName.globalAnimUV0, new Vector4(1, 0, 0, 0)); + break; + case "normal": + //shader.RenderBucket = 0; + //shader.ParameterSize = 320; + //shader.ParameterDataSize = 400; + AddShaderParam(pNames, pVals, MetaName.DiffuseSampler, GetTextureBaseParam(texNames, 0));//assume first texture is diffuse... + AddShaderParam(pNames, pVals, MetaName.BumpSampler, GetTextureBaseParam(texNames, 1));//assume 2nd texture is normalmap.. + AddShaderParam(pNames, pVals, MetaName.HardAlphaBlend, new Vector4(1, 0, 0, 0)); + AddShaderParam(pNames, pVals, MetaName.useTessellation, new Vector4(0, 0, 0, 0)); + AddShaderParam(pNames, pVals, MetaName.wetnessMultiplier, new Vector4(1, 0, 0, 0)); + AddShaderParam(pNames, pVals, MetaName.bumpiness, new Vector4(1, 0, 0, 0)); + AddShaderParam(pNames, pVals, MetaName.specularIntensityMult, new Vector4(0.5f, 0, 0, 0)); + AddShaderParam(pNames, pVals, MetaName.specularFalloffMult, new Vector4(20, 0, 0, 0));//too metallic? + AddShaderParam(pNames, pVals, MetaName.specularFresnel, new Vector4(0.9f, 0, 0, 0)); + AddShaderParam(pNames, pVals, MetaName.globalAnimUV1, new Vector4(0, 1, 0, 0)); + AddShaderParam(pNames, pVals, MetaName.globalAnimUV0, new Vector4(1, 0, 0, 0)); + break; + } + + for (int i = 0; i < pVals.Count; i++) + { + var pVal = pVals[i]; + if (pVal.DataType == 1) + { + pVal.Unknown_1h = (byte)(160 + ((pVals.Count - 1) - i));//seriously wtf is this and why + } + } + + paramsBlock.Hashes = pNames.ToArray(); + paramsBlock.Parameters = pVals.ToArray(); + paramsBlock.Count = pVals.Count; + + shader.ParameterSize = paramsBlock.ParametersSize; + shader.ParameterDataSize = (ushort)(paramsBlock.BlockLength + 36);//but why +36? + shader.ParameterCount = (byte)pVals.Count; + shader.TextureParametersCount = paramsBlock.TextureParamsCount; + shader.RenderBucketMask = (1u << shader.RenderBucket) | 0xFF00; + + + return shader; + } + + private TextureBase GetTextureBaseParam(List texNames, int index) + { + var name = "givemechecker"; + if (texNames.Count > index) + { + var nameval = texNames[index]; + if (nameval != null) + { + name = texNames[index]; + } + } + var texParam = new TextureBase(); + texParam.Unknown_4h = 1; + texParam.Unknown_30h = 131073;//wtf is this? 2x shorts, 0x00020001 + texParam.Name = name; + texParam.NameHash = JenkHash.GenHash(name.ToLowerInvariant()); + return texParam; + } + private void AddShaderParam(List paramNames, List paramValues, MetaName paramName, object paramValue) + { + var p = new ShaderParameter(); + p.Data = paramValue; + if (paramValue is TextureBase) + { + p.DataType = 0; + p.Unknown_1h = (byte)((paramNames.Count > 0) ? paramNames.Count + 1 : 0);//seriously wtf is this? + } + else if (paramValue is Vector4) + { + p.DataType = 1; + } + else + { } + + paramNames.Add(paramName); + paramValues.Add(p); + } + + private VertexDeclaration GetVertexDeclaration(ShaderFX shader) + { + var d = new VertexDeclaration(); + d.Types = 8598872888530528662; + d.Unknown_6h = 0; + + switch (shader.Name) + { + default: + case 3839837909: //default + d.Flags = 89; + d.Stride = 36; + d.Count = 4; + break; + case 1330140418: //normal + d.Flags = 16473; + d.Stride = 52; + d.Count = 5; + break; + } + + return d; + } + + + private bool IsByPolygonVertexMapType(FbxNode node) + { + var mapType = node["MappingInformationType"]?.Value as string; + if (mapType != "ByPolygonVertex") + { return false; } //any other types? + return true; + } + private bool IsIndexToDirectRefType(FbxNode node) + { + var refType = node["ReferenceInformationType"]?.Value as string; + var indexed = false; + switch (refType) + { + case "Direct": break; + case "IndexToDirect": indexed = true; break; + default: + break;//shouldn't be possible + } + return indexed; + } + + private Vector2 GetVector2FromDoubleArray(double[] arr, int i) + { + var aIndX = i * 2; + var aIndY = aIndX + 1; + var pX = aIndX < arr.Length ? arr[aIndX] : 0; + var pY = aIndY < arr.Length ? arr[aIndY] : 0; + return new Vector2((float)pX, (float)pY); + } + private Vector3 GetVector3FromDoubleArray(double[] arr, int i) + { + var aIndX = i * 3; + var aIndY = aIndX + 1; + var aIndZ = aIndX + 2; + var pX = aIndX < arr.Length ? arr[aIndX] : 0; + var pY = aIndY < arr.Length ? arr[aIndY] : 0; + var pZ = aIndZ < arr.Length ? arr[aIndZ] : 0; + return new Vector3((float)pX, (float)pY, (float)pZ); + } + private Vector4 GetVector4FromDoubleArray(double[] arr, int i) + { + var aIndX = i * 4; + var aIndY = aIndX + 1; + var aIndZ = aIndX + 2; + var aIndW = aIndX + 3; + var pX = aIndX < arr.Length ? arr[aIndX] : 0; + var pY = aIndY < arr.Length ? arr[aIndY] : 0; + var pZ = aIndZ < arr.Length ? arr[aIndZ] : 0; + var pW = aIndW < arr.Length ? arr[aIndW] : 0; + return new Vector4((float)pX, (float)pY, (float)pZ, (float)pW); + } + private Vector4 GetVector4FromObjectList(List list, int i) + { + var aIndX = i; + var aIndY = aIndX + 1; + var aIndZ = aIndX + 2; + var aIndW = aIndX + 3; + var pX = aIndX < list.Count ? list[aIndX] : 0; + var pY = aIndY < list.Count ? list[aIndY] : 0; + var pZ = aIndZ < list.Count ? list[aIndZ] : 0; + var pW = aIndW < list.Count ? list[aIndW] : 0; + var r = Vector4.Zero; + if (pX is double) r.X = (float)(double)pX; + if (pY is double) r.Y = (float)(double)pY; + if (pZ is double) r.Z = (float)(double)pZ; + if (pW is double) r.W = (float)(double)pW; + return r; + } + private string GetStringFromObjectList(List list, int i) + { + return (list.Count > i) ? list[i] as string : string.Empty; + } + + } + + + public class FbxModel + { + public string Name { get; set; } + public FbxNode Node { get; set; } + public DrawableModel Model { get; set; } + + public override string ToString() + { + return Name; + } + } + + public class FbxPolygon + { + public FbxVertex[] Vertices { get; set; } + public FbxNode[] Materials { get; set; } + } + + public class FbxVertex : IEquatable + { + public Vector3 Position { get; set; } + public Vector3[] Normals { get; set; } + public Vector3[] Binormals { get; set; } + public Vector3[] Tangents { get; set; } + public Vector2[] Texcoords { get; set; } + public Vector4[] Colours { get; set; } + public byte[] Bytes { get; set; } + + public void GenVertexBytes(VertexDeclaration decl) + { + Bytes = new byte[decl.Stride]; + + switch ((VertexType)decl.Flags) + { + default: + case VertexType.Default://PNCT + WriteBytes(Position, 0); + WriteBytes((Normals?.Length > 0) ? Normals[0] : Vector3.UnitZ, 12); + WriteBytes(GetColourInt((Colours?.Length > 0) ? Colours[0] : Vector4.One), 24); + WriteBytes((Texcoords?.Length > 0) ? Texcoords[0] : Vector2.Zero, 28); + break; + case VertexType.DefaultEx://PNCTX + WriteBytes(Position, 0); + WriteBytes((Normals?.Length > 0) ? Normals[0] : Vector3.UnitZ, 12); + WriteBytes(GetColourInt((Colours?.Length > 0) ? Colours[0] : Vector4.One), 24); + WriteBytes((Texcoords?.Length > 0) ? Texcoords[0] : Vector2.Zero, 28); + WriteBytes(new Vector4((Tangents?.Length > 0) ? Tangents[0] : Vector3.UnitX, 0), 36); + //WriteBytes(new Vector4((Binormals?.Length > 0) ? Binormals[0] : Vector3.UnitX, 0), 36); + break; + } + + + } + private void WriteBytes(T val, int offset) where T : struct + { + var data = MetaTypes.ConvertToBytes(val); + for (int i = 0; i < data.Length; i++) + { + Bytes[offset + i] = data[i]; + } + } + private int GetColourInt(Vector4 c) + { + Color v = new Color(c); + return v.ToRgba(); + } + + + public override bool Equals(object obj) + { + return Equals(obj as FbxVertex); + } + public bool Equals(FbxVertex other) + { + return (other != null) + && ((Bytes == null) ? (other.Bytes == null) : Bytes.SequenceEqual(other.Bytes)); + //&& Position.Equals(other.Position) + //&& ((Normals == null) ? (other.Normals == null) : Normals.SequenceEqual(other.Normals)) + //&& ((Binormals == null) ? (other.Binormals == null) : Binormals.SequenceEqual(other.Binormals)) + //&& ((Tangents == null) ? (other.Tangents == null) : Tangents.SequenceEqual(other.Tangents)) + //&& ((Texcoords == null) ? (other.Texcoords == null) : Texcoords.SequenceEqual(other.Texcoords)) + //&& ((Colours == null) ? (other.Colours == null) : Colours.SequenceEqual(other.Colours)); + } + public override int GetHashCode() + { + var hashCode = -907793594; + if (Bytes != null) hashCode = hashCode * -1521134295 + ((IStructuralEquatable)Bytes).GetHashCode(EqualityComparer.Default); + //hashCode = hashCode * -1521134295 + Position.GetHashCode(); + //if (Normals != null) hashCode = hashCode * -1521134295 + ((IStructuralEquatable)Normals).GetHashCode(EqualityComparer.Default); + //if (Binormals != null) hashCode = hashCode * -1521134295 + ((IStructuralEquatable)Binormals).GetHashCode(EqualityComparer.Default); + //if (Tangents != null) hashCode = hashCode * -1521134295 + ((IStructuralEquatable)Tangents).GetHashCode(EqualityComparer.Default); + //if (Texcoords != null) hashCode = hashCode * -1521134295 + ((IStructuralEquatable)Texcoords).GetHashCode(EqualityComparer.Default); + //if (Colours != null) hashCode = hashCode * -1521134295 + ((IStructuralEquatable)Colours).GetHashCode(EqualityComparer.Default); + return hashCode; + } + } + +} diff --git a/CodeWalker.Core/World/Space.cs b/CodeWalker.Core/World/Space.cs index 0454e99..ae2b6ef 100644 --- a/CodeWalker.Core/World/Space.cs +++ b/CodeWalker.Core/World/Space.cs @@ -432,6 +432,7 @@ namespace CodeWalker.World } foreach (var dlcrpf in GameFileCache.DlcActiveRpfs) //load nodes from current dlc rpfs { + if (dlcrpf.Path.StartsWith("x64")) continue; //don't override update.rpf YNDs with x64 ones! *hack foreach (var rpffile in dlcrpf.Children) { AddRpfYnds(rpffile, yndentries); diff --git a/CodeWalker.csproj b/CodeWalker.csproj index 7221417..c03a77b 100644 --- a/CodeWalker.csproj +++ b/CodeWalker.csproj @@ -271,6 +271,12 @@ + + Form + + + ImportFbxForm.cs + Form @@ -640,6 +646,9 @@ YwrForm.cs + + ImportFbxForm.cs + JenkGenForm.cs diff --git a/ExploreForm.Designer.cs b/ExploreForm.Designer.cs index d3da7e2..8e5906f 100644 --- a/ExploreForm.Designer.cs +++ b/ExploreForm.Designer.cs @@ -41,6 +41,7 @@ this.EditExtractRawMenu = new System.Windows.Forms.ToolStripMenuItem(); this.EditExtractAllMenu = new System.Windows.Forms.ToolStripMenuItem(); this.toolStripSeparator4 = new System.Windows.Forms.ToolStripSeparator(); + this.EditImportFbxMenu = new System.Windows.Forms.ToolStripMenuItem(); this.EditImportXmlMenu = new System.Windows.Forms.ToolStripMenuItem(); this.EditImportRawMenu = new System.Windows.Forms.ToolStripMenuItem(); this.EditImportMenuSeparator = new System.Windows.Forms.ToolStripSeparator(); @@ -115,6 +116,7 @@ this.ListContextNewMenu = new System.Windows.Forms.ToolStripMenuItem(); this.ListContextNewFolderMenu = new System.Windows.Forms.ToolStripMenuItem(); this.ListContextNewRpfArchiveMenu = new System.Windows.Forms.ToolStripMenuItem(); + this.ListContextImportFbxMenu = new System.Windows.Forms.ToolStripMenuItem(); this.ListContextImportXmlMenu = new System.Windows.Forms.ToolStripMenuItem(); this.ListContextImportRawMenu = new System.Windows.Forms.ToolStripMenuItem(); this.ListContextImportSeparator = new System.Windows.Forms.ToolStripSeparator(); @@ -196,6 +198,7 @@ this.EditExtractRawMenu, this.EditExtractAllMenu, this.toolStripSeparator4, + this.EditImportFbxMenu, this.EditImportXmlMenu, this.EditImportRawMenu, this.EditImportMenuSeparator, @@ -267,6 +270,14 @@ this.toolStripSeparator4.Name = "toolStripSeparator4"; this.toolStripSeparator4.Size = new System.Drawing.Size(204, 6); // + // EditImportFbxMenu + // + this.EditImportFbxMenu.Name = "EditImportFbxMenu"; + this.EditImportFbxMenu.Size = new System.Drawing.Size(207, 22); + this.EditImportFbxMenu.Text = "Import FBX..."; + this.EditImportFbxMenu.Visible = false; + this.EditImportFbxMenu.Click += new System.EventHandler(this.EditImportFbxMenu_Click); + // // EditImportXmlMenu // this.EditImportXmlMenu.Image = ((System.Drawing.Image)(resources.GetObject("EditImportXmlMenu.Image"))); @@ -383,21 +394,21 @@ // ViewLargeIconsMenu // this.ViewLargeIconsMenu.Name = "ViewLargeIconsMenu"; - this.ViewLargeIconsMenu.Size = new System.Drawing.Size(152, 22); + this.ViewLargeIconsMenu.Size = new System.Drawing.Size(134, 22); this.ViewLargeIconsMenu.Text = "Large Icons"; this.ViewLargeIconsMenu.Click += new System.EventHandler(this.ViewLargeIconsMenu_Click); // // ViewSmallIconsMenu // this.ViewSmallIconsMenu.Name = "ViewSmallIconsMenu"; - this.ViewSmallIconsMenu.Size = new System.Drawing.Size(152, 22); + this.ViewSmallIconsMenu.Size = new System.Drawing.Size(134, 22); this.ViewSmallIconsMenu.Text = "Small Icons"; this.ViewSmallIconsMenu.Click += new System.EventHandler(this.ViewSmallIconsMenu_Click); // // ViewListMenu // this.ViewListMenu.Name = "ViewListMenu"; - this.ViewListMenu.Size = new System.Drawing.Size(152, 22); + this.ViewListMenu.Size = new System.Drawing.Size(134, 22); this.ViewListMenu.Text = "List"; this.ViewListMenu.Click += new System.EventHandler(this.ViewListMenu_Click); // @@ -406,14 +417,14 @@ this.ViewDetailsMenu.Checked = true; this.ViewDetailsMenu.CheckState = System.Windows.Forms.CheckState.Checked; this.ViewDetailsMenu.Name = "ViewDetailsMenu"; - this.ViewDetailsMenu.Size = new System.Drawing.Size(152, 22); + this.ViewDetailsMenu.Size = new System.Drawing.Size(134, 22); this.ViewDetailsMenu.Text = "Details"; this.ViewDetailsMenu.Click += new System.EventHandler(this.ViewDetailsMenu_Click); // // toolStripSeparator11 // this.toolStripSeparator11.Name = "toolStripSeparator11"; - this.toolStripSeparator11.Size = new System.Drawing.Size(149, 6); + this.toolStripSeparator11.Size = new System.Drawing.Size(131, 6); // // ViewThemeMenu // @@ -423,7 +434,7 @@ this.ViewThemeLightMenu, this.ViewThemeDarkMenu}); this.ViewThemeMenu.Name = "ViewThemeMenu"; - this.ViewThemeMenu.Size = new System.Drawing.Size(152, 22); + this.ViewThemeMenu.Size = new System.Drawing.Size(134, 22); this.ViewThemeMenu.Text = "Theme"; // // ViewThemeWindowsMenu @@ -431,28 +442,28 @@ this.ViewThemeWindowsMenu.Checked = true; this.ViewThemeWindowsMenu.CheckState = System.Windows.Forms.CheckState.Checked; this.ViewThemeWindowsMenu.Name = "ViewThemeWindowsMenu"; - this.ViewThemeWindowsMenu.Size = new System.Drawing.Size(152, 22); + this.ViewThemeWindowsMenu.Size = new System.Drawing.Size(123, 22); this.ViewThemeWindowsMenu.Text = "Windows"; this.ViewThemeWindowsMenu.Click += new System.EventHandler(this.ViewThemeWindowsMenu_Click); // // ViewThemeBlueMenu // this.ViewThemeBlueMenu.Name = "ViewThemeBlueMenu"; - this.ViewThemeBlueMenu.Size = new System.Drawing.Size(152, 22); + this.ViewThemeBlueMenu.Size = new System.Drawing.Size(123, 22); this.ViewThemeBlueMenu.Text = "Blue"; this.ViewThemeBlueMenu.Click += new System.EventHandler(this.ViewThemeBlueMenu_Click); // // ViewThemeLightMenu // this.ViewThemeLightMenu.Name = "ViewThemeLightMenu"; - this.ViewThemeLightMenu.Size = new System.Drawing.Size(152, 22); + this.ViewThemeLightMenu.Size = new System.Drawing.Size(123, 22); this.ViewThemeLightMenu.Text = "Light"; this.ViewThemeLightMenu.Click += new System.EventHandler(this.ViewThemeLightMenu_Click); // // ViewThemeDarkMenu // this.ViewThemeDarkMenu.Name = "ViewThemeDarkMenu"; - this.ViewThemeDarkMenu.Size = new System.Drawing.Size(152, 22); + this.ViewThemeDarkMenu.Size = new System.Drawing.Size(123, 22); this.ViewThemeDarkMenu.Text = "Dark"; this.ViewThemeDarkMenu.Click += new System.EventHandler(this.ViewThemeDarkMenu_Click); // @@ -891,6 +902,7 @@ this.ListContextExtractAllMenu, this.toolStripSeparator5, this.ListContextNewMenu, + this.ListContextImportFbxMenu, this.ListContextImportXmlMenu, this.ListContextImportRawMenu, this.ListContextImportSeparator, @@ -908,7 +920,7 @@ this.ListContextDefragmentSeparator, this.ListContextSelectAllMenu}); this.ListContextMenu.Name = "MainContextMenu"; - this.ListContextMenu.Size = new System.Drawing.Size(208, 442); + this.ListContextMenu.Size = new System.Drawing.Size(208, 464); // // ListContextViewMenu // @@ -995,6 +1007,13 @@ this.ListContextNewRpfArchiveMenu.Text = "RPF Archive..."; this.ListContextNewRpfArchiveMenu.Click += new System.EventHandler(this.ListContextNewRpfArchiveMenu_Click); // + // ListContextImportFbxMenu + // + this.ListContextImportFbxMenu.Name = "ListContextImportFbxMenu"; + this.ListContextImportFbxMenu.Size = new System.Drawing.Size(207, 22); + this.ListContextImportFbxMenu.Text = "Import FBX..."; + this.ListContextImportFbxMenu.Click += new System.EventHandler(this.ListContextImportFbxMenu_Click); + // // ListContextImportXmlMenu // this.ListContextImportXmlMenu.Image = ((System.Drawing.Image)(resources.GetObject("ListContextImportXmlMenu.Image"))); @@ -1326,5 +1345,7 @@ private System.Windows.Forms.ToolStripMenuItem ViewThemeDarkMenu; private WeifenLuo.WinFormsUI.Docking.VisualStudioToolStripExtender VSExtender; private System.Windows.Forms.ToolStripMenuItem ViewThemeWindowsMenu; + private System.Windows.Forms.ToolStripMenuItem ListContextImportFbxMenu; + private System.Windows.Forms.ToolStripMenuItem EditImportFbxMenu; } } \ No newline at end of file diff --git a/ExploreForm.cs b/ExploreForm.cs index 8525d7e..3793b3a 100644 --- a/ExploreForm.cs +++ b/ExploreForm.cs @@ -537,10 +537,11 @@ namespace CodeWalker var str = ic.ToString() + " item" + ((ic != 1) ? "s" : "") + " shown"; bool isitem = false; bool isfile = false; + bool issearch = CurrentFolder?.IsSearchResults ?? false; bool canview = false; bool canedit = false; bool canexportxml = false; - bool canimport = false; + bool canimport = EditMode && (CurrentFolder?.RpfFolder != null) && !issearch; if (sc != 0) { long bc = 0; @@ -578,6 +579,7 @@ namespace CodeWalker EditExtractRawMenu.Enabled = isfile; EditImportRawMenu.Visible = canimport; + EditImportFbxMenu.Visible = canimport; EditImportXmlMenu.Visible = canimport; EditImportMenuSeparator.Visible = canimport; @@ -689,6 +691,11 @@ namespace CodeWalker rpf.ScanStructure(UpdateStatus, UpdateErrorLog); + if (rpf.LastException != null) //incase of corrupted rpf (or renamed NG encrypted RPF) + { + continue; + } + node = CreateRpfTreeFolder(rpf, relpath, path); RecurseMainTreeViewRPF(node, allRpfs); @@ -1703,6 +1710,7 @@ namespace CodeWalker ListContextNewMenu.Visible = cancreate; ListContextImportRawMenu.Visible = canimport; + ListContextImportFbxMenu.Visible = canimport; ListContextImportXmlMenu.Visible = canimport; ListContextImportSeparator.Visible = cancreate; @@ -1747,6 +1755,7 @@ namespace CodeWalker MainListView.LabelEdit = enable; EnsureEditModeWarning(); + UpdateSelectionUI(); } private void EnsureEditModeWarning() @@ -2215,6 +2224,98 @@ namespace CodeWalker AddNewFolderTreeNode(node); } + } + private void ImportFbx() + { + if (!EditMode) return; + if (CurrentFolder?.IsSearchResults ?? false) return; + + + RpfDirectoryEntry parentrpffldr = CurrentFolder.RpfFolder; + if (parentrpffldr == null) + { + MessageBox.Show("No parent RPF folder selected! This shouldn't happen. Refresh the view and try again."); + return; + } + + if (!EnsureRpfValidEncryption()) return; + + + OpenFileDialog.Filter = "FBX Files|*.fbx"; + if (OpenFileDialog.ShowDialog(this) != DialogResult.OK) + { + return;//canceled + } + + var fpaths = OpenFileDialog.FileNames; + var fdict = new Dictionary(); + + foreach (var fpath in fpaths) + { +#if !DEBUG + try +#endif + { + if (!File.Exists(fpath)) + { + continue;//this shouldn't happen... + } + + var fi = new FileInfo(fpath); + var fname = fi.Name; + var fnamel = fname.ToLowerInvariant(); + var trimlength = 4; + + if (!fnamel.EndsWith(".fbx")) + { + MessageBox.Show(fname + ": Not an FBX file!", "Cannot import FBX"); + continue; + } + + fname = fname.Substring(0, fname.Length - trimlength); + + var data = File.ReadAllBytes(fpath); + fdict[fname] = data; + + } +#if !DEBUG + catch (Exception ex) + { + MessageBox.Show(ex.Message, "Unable to read file " + fpath); + } +#endif + + } + + + var fbxForm = new ImportFbxForm(); + fbxForm.SetInputFiles(fdict); + fbxForm.ShowDialog(); + + if (fbxForm.DialogResult != DialogResult.OK) + { + return; //fbx import canceled + } + + var converted = fbxForm.GetOutputFiles(); + if (converted == null) + { + return; + } + + foreach (var kvp in converted) + { + var fname = kvp.Key; + var data = kvp.Value; + if (data != null) + { + RpfFile.CreateFile(parentrpffldr, fname, data); + } + } + + + RefreshMainListView(); + } private void ImportXml() { @@ -3413,6 +3514,11 @@ namespace CodeWalker NewRpfArchive(); } + private void ListContextImportFbxMenu_Click(object sender, EventArgs e) + { + ImportFbx(); + } + private void ListContextImportXmlMenu_Click(object sender, EventArgs e) { ImportXml(); @@ -3498,6 +3604,11 @@ namespace CodeWalker ExtractAll(); } + private void EditImportFbxMenu_Click(object sender, EventArgs e) + { + ImportFbx(); + } + private void EditImportXmlMenu_Click(object sender, EventArgs e) { ImportXml(); diff --git a/ExploreForm.resx b/ExploreForm.resx index 704ffb8..52c6b67 100644 --- a/ExploreForm.resx +++ b/ExploreForm.resx @@ -313,7 +313,7 @@ AAEAAAD/////AQAAAAAAAAAMAgAAAFdTeXN0ZW0uV2luZG93cy5Gb3JtcywgVmVyc2lvbj00LjAuMC4w LCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkFAQAAACZTeXN0 ZW0uV2luZG93cy5Gb3Jtcy5JbWFnZUxpc3RTdHJlYW1lcgEAAAAERGF0YQcCAgAAAAkDAAAADwMAAADo - HwAAAk1TRnQBSQFMAgEBGAEAARgBAQEYAQEBEAEAARABAAT/AQkBAAj/AUIBTQE2AQQGAAE2AQQCAAEo + HwAAAk1TRnQBSQFMAgEBGAEAATgBAQE4AQEBEAEAARABAAT/AQkBAAj/AUIBTQE2AQQGAAE2AQQCAAEo AwABQAMAAXADAAEBAQABCAYAARwYAAGAAgABgAMAAoABAAGAAwABgAEAAYABAAKAAgADwAEAAcAB3AHA AQAB8AHKAaYBAAEzBQABMwEAATMBAAEzAQACMwIAAxYBAAMcAQADIgEAAykBAANVAQADTQEAA0IBAAM5 AQABgAF8Af8BAAJQAf8BAAGTAQAB1gEAAf8B7AHMAQABxgHWAe8BAAHWAucBAAGQAakBrQIAAf8BMwMA diff --git a/Notice.txt b/Notice.txt index 2aa7ea8..3bbc04b 100644 --- a/Notice.txt +++ b/Notice.txt @@ -1,5 +1,5 @@  - Copyright(c) 2017 dexyfex + Copyright(c) 2017-2019 dexyfex @@ -55,3 +55,20 @@ SOFTWARE. + +/* + +Copyright (c) 2015 Hamish Milne + +"An FBX library for .NET" +Contains source code for FBX file format reading and writing from: +https://github.com/hamish-milne/FbxWriter +Published under GPL license, for full terms and conditions see the above link. + +*/ + + + + + + diff --git a/Rendering/Renderable.cs b/Rendering/Renderable.cs index 0f155ca..27cbe4d 100644 --- a/Rendering/Renderable.cs +++ b/Rendering/Renderable.cs @@ -238,7 +238,7 @@ namespace CodeWalker.Rendering if (hastransforms) { - int boneidx = (int)((model.Unk28h >> 24) & 0xFF); + int boneidx = (int)((model.SkeletonBinding >> 24) & 0xFF); Matrix trans = (boneidx < modeltransforms.Length) ? modeltransforms[boneidx] : Matrix.Identity; Bone bone = (hasbones && (boneidx < bones.Count)) ? bones[boneidx] : null; @@ -267,7 +267,7 @@ namespace CodeWalker.Rendering } } - if (((model.Unk28h >> 8) & 0xFF) > 0) //skin mesh? + if (((model.SkeletonBinding >> 8) & 0xFF) > 0) //skin mesh? { model.Transform = Matrix.Identity; } @@ -337,20 +337,16 @@ namespace CodeWalker.Rendering public AABB_s[] GeometryBounds; public long GeometrySize { get; private set; } - public uint Unk4h; - public uint Unk14h; - public uint Unk28h; - public uint Unk2Ch; //flags....... + public uint SkeletonBinding; + public uint RenderMaskFlags; //flags....... public bool UseTransform; public Matrix Transform; public void Init(DrawableModel dmodel) { - Unk4h = dmodel.Unknown_4h; - Unk14h = dmodel.Unknown_14h; - Unk28h = dmodel.Unknown_28h; - Unk2Ch = dmodel.Unknown_2Ch; //only the first byte of Unknown_2Ch seems be related to this + SkeletonBinding = dmodel.SkeletonBinding;//4th byte is bone index, 2nd byte for skin meshes + RenderMaskFlags = dmodel.RenderMaskFlags; //only the first byte seems be related to this DrawableModel = dmodel; diff --git a/Rendering/Renderer.cs b/Rendering/Renderer.cs index 1ca4576..21b1028 100644 --- a/Rendering/Renderer.cs +++ b/Rendering/Renderer.cs @@ -1904,7 +1904,7 @@ namespace CodeWalker.Rendering private bool RenderIsModelFinalRender(RenderableModel model) { - if ((model.Unk2Ch & 1) == 0) //smallest bit is proxy/"final render" bit? seems to work... + if ((model.RenderMaskFlags & 1) == 0) //smallest bit is proxy/"final render" bit? seems to work... { return renderproxies; } diff --git a/Rendering/ShaderManager.cs b/Rendering/ShaderManager.cs index e65e835..5b7096f 100644 --- a/Rendering/ShaderManager.cs +++ b/Rendering/ShaderManager.cs @@ -739,7 +739,7 @@ namespace CodeWalker.Rendering { var shader = geom.Geom.DrawableGeom.Shader; - var b = (shader!=null) ? shader.Unknown_11h : 0; //rage render bucket? + var b = (shader!=null) ? shader.RenderBucket : 0; //rage render bucket? var bucket = EnsureRenderBucket(b); diff --git a/Shaders/Common.hlsli b/Shaders/Common.hlsli index 569aa20..68bc413 100644 --- a/Shaders/Common.hlsli +++ b/Shaders/Common.hlsli @@ -32,7 +32,15 @@ float4 Unpack4x8UNF(uint v) float DepthFunc(float2 zw) { - return zw.x;// + return zw.x; + + ////this sort of works for reverse depth buffering, but has issues with vertices behind the near clip plane. + ////might need to adjust the viewproj matrix to fix that... + ////(for this to work, also need to change GpuBuffers.Clear,.ClearDepth and ShaderManager DepthComparison,RenderFinalPass) + //return max(0.001 / zw.x, 0); + + + //return zw.x * (0.1 + 0.00001*(abs(zw.y))); //return zw.x * (0.1 + 0.00001*((zw.y))); diff --git a/Tools/ImportFbxForm.Designer.cs b/Tools/ImportFbxForm.Designer.cs new file mode 100644 index 0000000..4ce3d52 --- /dev/null +++ b/Tools/ImportFbxForm.Designer.cs @@ -0,0 +1,145 @@ +namespace CodeWalker +{ + partial class ImportFbxForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ImportFbxForm)); + this.CancelThisButton = new System.Windows.Forms.Button(); + this.ImportButton = new System.Windows.Forms.Button(); + this.FbxFilesListBox = new System.Windows.Forms.ListBox(); + this.label1 = new System.Windows.Forms.Label(); + this.OutputTypeCombo = new System.Windows.Forms.ComboBox(); + this.label2 = new System.Windows.Forms.Label(); + this.StatusLabel = new System.Windows.Forms.Label(); + this.SuspendLayout(); + // + // CancelThisButton + // + this.CancelThisButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.CancelThisButton.Location = new System.Drawing.Point(392, 310); + this.CancelThisButton.Name = "CancelThisButton"; + this.CancelThisButton.Size = new System.Drawing.Size(75, 23); + this.CancelThisButton.TabIndex = 4; + this.CancelThisButton.Text = "Cancel"; + this.CancelThisButton.UseVisualStyleBackColor = true; + this.CancelThisButton.Click += new System.EventHandler(this.CancelThisButton_Click); + // + // ImportButton + // + this.ImportButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.ImportButton.Location = new System.Drawing.Point(473, 310); + this.ImportButton.Name = "ImportButton"; + this.ImportButton.Size = new System.Drawing.Size(75, 23); + this.ImportButton.TabIndex = 3; + this.ImportButton.Text = "Import"; + this.ImportButton.UseVisualStyleBackColor = true; + this.ImportButton.Click += new System.EventHandler(this.ImportButton_Click); + // + // FbxFilesListBox + // + this.FbxFilesListBox.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.FbxFilesListBox.FormattingEnabled = true; + this.FbxFilesListBox.Location = new System.Drawing.Point(12, 25); + this.FbxFilesListBox.Name = "FbxFilesListBox"; + this.FbxFilesListBox.Size = new System.Drawing.Size(246, 121); + this.FbxFilesListBox.TabIndex = 5; + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(12, 9); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(94, 13); + this.label1.TabIndex = 6; + this.label1.Text = "FBX files to import:"; + // + // OutputTypeCombo + // + this.OutputTypeCombo.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.OutputTypeCombo.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.OutputTypeCombo.FormattingEnabled = true; + this.OutputTypeCombo.Items.AddRange(new object[] { + "YDR"}); + this.OutputTypeCombo.Location = new System.Drawing.Point(427, 25); + this.OutputTypeCombo.Name = "OutputTypeCombo"; + this.OutputTypeCombo.Size = new System.Drawing.Size(121, 21); + this.OutputTypeCombo.TabIndex = 7; + // + // label2 + // + this.label2.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.label2.AutoSize = true; + this.label2.Location = new System.Drawing.Point(356, 28); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(65, 13); + this.label2.TabIndex = 8; + this.label2.Text = "Output type:"; + // + // StatusLabel + // + this.StatusLabel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.StatusLabel.AutoSize = true; + this.StatusLabel.Location = new System.Drawing.Point(12, 315); + this.StatusLabel.Name = "StatusLabel"; + this.StatusLabel.Size = new System.Drawing.Size(81, 13); + this.StatusLabel.TabIndex = 9; + this.StatusLabel.Text = "Ready to import"; + // + // ImportFbxForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(560, 345); + this.Controls.Add(this.StatusLabel); + this.Controls.Add(this.label2); + this.Controls.Add(this.OutputTypeCombo); + this.Controls.Add(this.label1); + this.Controls.Add(this.FbxFilesListBox); + this.Controls.Add(this.CancelThisButton); + this.Controls.Add(this.ImportButton); + this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); + this.Name = "ImportFbxForm"; + this.Text = "Import FBX - CodeWalker by dexyfex"; + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.Button CancelThisButton; + private System.Windows.Forms.Button ImportButton; + private System.Windows.Forms.ListBox FbxFilesListBox; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.ComboBox OutputTypeCombo; + private System.Windows.Forms.Label label2; + private System.Windows.Forms.Label StatusLabel; + } +} \ No newline at end of file diff --git a/Tools/ImportFbxForm.cs b/Tools/ImportFbxForm.cs new file mode 100644 index 0000000..f277c65 --- /dev/null +++ b/Tools/ImportFbxForm.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace CodeWalker +{ + public partial class ImportFbxForm : Form + { + + private Dictionary InputFiles { get; set; } + private Dictionary OutputFiles { get; set; } + + public ImportFbxForm() + { + InitializeComponent(); + DialogResult = DialogResult.Cancel; + OutputTypeCombo.Text = "YDR"; + } + + + public void SetInputFiles(Dictionary fdict) + { + InputFiles = fdict; + + FbxFilesListBox.Items.Clear(); + foreach (var kvp in fdict) + { + FbxFilesListBox.Items.Add(kvp.Key); + } + + } + + public Dictionary GetOutputFiles() + { + return OutputFiles; + } + + + private void ConvertFiles() + { + if (InputFiles == null) return; + + Cursor = Cursors.WaitCursor; + + + Task.Run(() => + { + + OutputFiles = new Dictionary(); + + foreach (var kvp in InputFiles) + { + var fname = kvp.Key; + var idata = kvp.Value; + + UpdateStatus("Converting " + fname + "..."); + + FbxConverter fc = new FbxConverter(); + + var ydr = fc.ConvertToYdr(fname, idata); + + + if (ydr == null) + { + UpdateStatus("Converting " + fname + " failed!"); //TODO: error message + + continue; //something went wrong.. + } + + byte[] odata = ydr.Save(); + + OutputFiles.Add(fname + ".ydr", odata); + } + + UpdateStatus("Process complete."); + + ConvertComplete(); + + }); + } + + private void ConvertComplete() + { + try + { + if (InvokeRequired) + { + BeginInvoke(new Action(() => { ConvertComplete(); })); + } + else + { + Cursor = Cursors.Default; + DialogResult = DialogResult.OK; + Close(); + } + } + catch { } + } + + public void UpdateStatus(string text) + { + try + { + if (InvokeRequired) + { + BeginInvoke(new Action(() => { UpdateStatus(text); })); + } + else + { + StatusLabel.Text = text; + } + } + catch { } + } + + + private void CancelThisButton_Click(object sender, EventArgs e) + { + Close(); + } + + private void ImportButton_Click(object sender, EventArgs e) + { + ConvertFiles(); + } + } +} diff --git a/Tools/ImportFbxForm.resx b/Tools/ImportFbxForm.resx new file mode 100644 index 0000000..1431f6b --- /dev/null +++ b/Tools/ImportFbxForm.resx @@ -0,0 +1,409 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + + 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 + 6P///ysvjWVqtP///////wAAAAAAAAAAAP////D0/0hPpsDG6////////6y02d7k8////3qAx+/z/f// + /wAAAAAAAAAAAAAAAAAAAP///////////////8zT8V5ns1Rcrdzh9f///////////wAAAAAAAAAAAAAA + AAAAAP////////7+/6ix3nmBxFthtmdwu09WqbC54/v9//r8//j6//39/wAAAAAAAAAAAOjt/H6I0FJc + skpSqHF+wRMahFZhs4iT1AsNc1pgrm52v2RsuO/z/gAAAP////////L2/cLJ7rrD64+V4DY+ozU+mYmU + 0X2Hy1hfss7V8urv/PP2/v///wAAAP///+Pp+d/k9////////+Pp/4uR3ysymW14xYOM0fD0/P///+Xq + +ri/6Pj6/wAAAOrv/j5DnbS75P////////////X4/+/0/ubr+/r7/////////9rh+hgZhKGo2QAAAPDz + /eLn+f////j6/2Nqttrg9////+Hn+P3+//3+/1hescLJ6/////L2/eru/AAAAAAAAAAAAP///8rR70tR + p/3+//v8/zY6jNPY7////09WqWpwu////wAAAAAAAAAAAAAAAAAAAAAAAPb4/vr7//////v8/5Wd1eHm + +P////v8//T3/wAAAAAAAAAAAAAAAP//AAD8PwAA/D8AAPwDAACAAwAAgAMAAIAHAADABwAAwAEAAMAB + AAAAAQAAAAEAAAABAAAAAQAAwAcAAOAPAAAoAAAAQAAAAIAAAAABABgAAAAAAAAwAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/v7//f7//P3//P3//P3/ + /f7//v7/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/v7//P3/ + +fv/+fv/+Pr/+fv/+vv//P3//v//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAA/f7/+fr/8/b/7PL/5+3/6e/+9Pf/+vv//v7/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAA/P3/9/r/6O7/cXe1UVaet7z17fL/+Pr//f3/AAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+/z/9Pj/4Oj/NzyCUlOd2dz/6O//9Pf//P3/AAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+vv/8vb/2+P9X2OmREGLnqPd + 4+v/8vb/+/z/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+vv/8fb/ + 1N35bXK1JSRtbHGz5O7/8fX/+/z/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAA+vv/8PX/3Ob/U1eaDwtXjZLT4+z/8fX/+/z/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAA+vv/8fb/2eP+MjR6AAA+c3i34Or/8fX/+/z/AAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+vv/8vb/1d/7MS91AAA1UFSS4On/8vb/+/z/AAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + 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 + /v7/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAA/v7//P3/+vz/+fv/+Pr/+Pr/+vv//f3//v//AAAAAAAAAAAAAAAA + /v//+fv/9Pf/2+L/SVGtAABsLTaZytL58fX/9/n//f7/AAAAAAAAAAAAAAAA/v7/+/z/9/n/9fj/9vn/ + 9fj/9vj/+vz//f7/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/v7//f7//f3//f3//f3//v7//v//AAAA + AAAAAAAAAAAAAAAAAAAA+/z/9vn/6e//mZ7gTVarr7bp6/H/9fj/+vv//v7/AAAAAAAAAAAAAAAAAAAA + /v7//f7/+/z/+/z/+/z//P3//v7/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/f3/+Pr/9fj/6e7/4+n/8fb/9Pf/+Pr//f3/AAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/v7//P3/+fv/+fv/+vv/+Pr/+vv/ + /P3//v7/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/v7//f7/ + /f3//P3//f7//v7//v//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//////// + ///////4D/////////AH////////8Af////////wB/////////AH////////8Af////////wB/////// + //AH////////8Af////////wB/////////AH////////8AfwP//////wB8Af//+Af/AHgB///wA/8AcA + H///AB/wBgAf//8AD/AGAB///wAH8AYAH///AAPwBAAf//8AA/AEAD///wAD8AQAP///AAPwBAB///+A + A/AEAP///8AD4AAA////4AcAAAH////wDgAAAf/////8AAAH//////gAAAf/////4AAAAf/////gAAAA + /f//+AAAAAAAD//AAAAAAAAH/4AAAAAAAAf/gAAAAAAAB/+AAAAAAAAH/4AAAAAAAAf/gAAAAAAAB/+A + AAAAAAAP/4AAAAAAAB//wAAAAABAf/4HwAAAAYAf8APAAAADgA/gA+AAAAMAA8AD8AAABwADgAP8AAAf + AAOAA/4AAB8AA4ADAAAAAQADgAIAcA4AgAOABgBwDgBAA4AMAGAMADADwDwAYAwAOAfg+ABgBAAeH//4 + AEAEAB////gAwAYAH///+ADABgAf///4AcAGAB////gBwAcAH///+APAB4A////8B+AHwH//////4A// + ///////gD/////////Af//////////////8= + + + \ No newline at end of file