diff --git a/KnuxLib/Engines/Wayforward/Collision.cs b/KnuxLib/Engines/Wayforward/Collision.cs new file mode 100644 index 0000000..df26070 --- /dev/null +++ b/KnuxLib/Engines/Wayforward/Collision.cs @@ -0,0 +1,357 @@ +ο»Ώnamespace KnuxLib.Engines.Wayforward +{ + // TODO: Format saving. + // TODO: Figure out the unknown behaviour flags for the different versions of this format. + // Especially the Ducktales Remastered ones, as I've never even played it and am trying to go off videos at a quick glance. + // TODO: Figure out the unknown values in non Shantae: Half Genie Hero versions of this format. + // TODO: Figure out the massive chunk of data that controls screen transitions(?) in Shantae and the Seven Sirens. + // TODO: Format importing. + public class Collision : FileBase + { + // Generic VS stuff to allow creating an object that instantly loads a file. + public Collision() { } + public Collision(string filepath, FormatVersion version = FormatVersion.hero, bool bigEndian = false, bool export = false) + { + // Load this file. + Load(filepath, version, bigEndian); + + // If the export flag is set, then export this format. + if (export) + ExportOBJ($@"{Helpers.GetExtension(filepath, true)}.obj"); + } + + // Classes for this format. + public enum FormatVersion + { + /// + /// Ducktales Remastered. + /// + duck = 0, + + /// + /// Shantae: Half-Genie Hero. + /// + hero = 1, + + /// + /// Shantae and the Seven Sirens. + /// + sevensirens = 2 + } + + [Flags] + public enum Behaviour_Duck + { + // TODO: Some models in exeanm.clb have a behaviour value of 0? + Solid = 0x00000001, + Unknown1 = 0x00000010, + BottomlessPit = 0x00000020, + Water = 0x00000040, + Unknown2 = 0x00000080, // Area transition maybe? Just based on placements. + Rail = 0x00000100, // Not all rails seem to have this? Just some end pieces? + Unknown3 = 0x00000800, // Only used on a single block in vesuvius.clb + Snow = 0x00100000, // Only used through himalayas.clb, verify that this is correct. + Ice = 0x00200000, // Only used through himalayas.clb, verify that this is correct. + Unknown4 = 0x00800000 // Only used on two platforms in amazon.clb, one block in himalayas.clb and a roof in moon.clb + } + + [Flags] + public enum Behaviour_Hero : uint + { + Solid = 0x00000001, + TopSolid = 0x00000002, + Boundry = 0x00000004, // TODO: Confirm + Spikes = 0x00000008, + NoMonkey = 0x00000010, + BottomlessPit = 0x00000020, + Unknown1 = 0x00000100, + WoodSound = 0x00000400, + Unknown2 = 0x00004000, // Often found out of bounds? + Slide = 0x00400000, + Unknown3 = 0x00800000, // Only used on a single block at the top of Mermaid Falls Act 1? Something to do with the mouse maybe? + Unknown4 = 0x01000000, // Used in Tassle Town Act 1 and Hypno Baron's Castle Act 1 for the down left slopes. Also found on some walls in the Intro Caves. + Unknown5 = 0x02000000, // Used in Tassle Town Act 1 and Hypno Baron's Castle Act 1 for the down right slopes. Also found on a single wall in the Intro Workshop. + Unknown6 = 0x10000000, // Used in a couple of odd spots in Burning Town Act 1 and multiple acts of Risky's Hideout. + Lava = 0x80000000 // TODO: Confirm, only assumed based on placement, as its all over Risky's Hideout and nowhere else. + } + + [Flags] + public enum Behaviour_SevenSirens + { + Solid = 0x00000001, + TopSolid = 0x00000002, + Unknown1 = 0x00000005, // Often placed on weird corners? Always has the NoNewt tag as well for some reason. + Spikes = 0x00000008, + NoNewt = 0x00000010, + BottomlessPit = 0x00000020, + DamageZone = 0x00000040, // Has to be paired with Water to use. + HealingZone = 0x00000400, // Has to be paired with Water to use. + DrillZone = 0x00008000, // How does this one actually work? + Water = 0x00200000, + Unknown2 = 0x10000000, // Seems to be some sort of door way almost? Just based on placement. + Unknown3 = 0x20000000 // Only used on four pillars in level_labyrinth_02.clb. + } + + public class FormatData + { + public Model[] Models { get; set; } = []; + + public byte[]? ScreenTransition { get; set; } + } + + public class Model + { + /// + /// This model's axis aligned bounding box. + /// + public AABB? AxisAlignedBoundingBox { get; set; } + + /// + /// An unknown Vector3 value that is only present in Ducktales Remastered. + /// TODO: What is this? Do this and the next value replace the AABB in some way? + /// + public Vector3? UnknownVector3_1 { get; set; } + + /// + /// An unknown integer value that is only present in Ducktales Remastered. + /// TODO: What is this? Do this and the last value replace the AABB in some way? + /// + public uint? UnknownUInt32_1 { get; set; } + + /// + /// The behaviour/surface type of this collision. + /// TODO: Figure out the values for each type. + /// + public object Behaviour { get; set; } + + /// + /// An unknown integer value. + /// TODO: What is this? It's always 0 except for a couple of instances in Seven Sirens. + /// + public uint UnknownUInt32_2 { get; set; } + + /// + /// An unknown 64 bit(?) integer value that is only present in Seven Sirens. + /// TODO: What is this? + /// + public ulong? UnknownULong_1 { get; set; } + + /// + /// The coordinates for the various vertices that make up this model. + /// + public Vector3[] Vertices { get; set; } = []; + + /// + /// The faces that make up this model. + /// + public Face[] Faces { get; set; } = []; + + /// + /// Initialises this model with default data. + /// + public Model() { } + + /// + /// Initialises this model by reading its data from a BinaryReader. + /// + public Model(ExtendedBinaryReader reader, FormatVersion version) => Read(reader, version); + + /// + /// Reads the data for this model. + /// + public void Read(ExtendedBinaryReader reader, FormatVersion version) + { + // If this isn't a Ducktales Remastered format, then read this model's axis aligned bounding box. + if (version != FormatVersion.duck) + AxisAlignedBoundingBox = new(reader); + + // Check if this is a Ducktales Remastered format. + else + { + // Read an unknown Vector3. + UnknownVector3_1 = reader.ReadVector3(); + + // Read an unknown integer value. + UnknownUInt32_1 = reader.ReadUInt32(); + } + + // Read this model's behaviour flag, depending on the format version. + switch (version) + { + case FormatVersion.duck: Behaviour = (Behaviour_Duck)reader.ReadUInt32(); break; + case FormatVersion.hero: Behaviour = (Behaviour_Hero)reader.ReadUInt32(); break; + case FormatVersion.sevensirens: Behaviour = (Behaviour_SevenSirens)reader.ReadUInt32(); break; + default: Behaviour = reader.ReadUInt32(); break; + } + + // Read an unknown integer value. + UnknownUInt32_2 = reader.ReadUInt32(); + + // If this is a Shantae and the Seven Sirens format, then read an unknown long value. + if (version == FormatVersion.sevensirens) + UnknownULong_1 = reader.ReadUInt64(); + + // Read the offset to this model's data. + ulong modelDataOffset = reader.ReadUInt64(); + + // Save our current position so we can jump back for the next model. + long position = reader.BaseStream.Position; + + // Jump to this model's data. + reader.JumpTo(modelDataOffset); + + // Check for a value of 0. + reader.CheckValue(0x00); + + // Initialise this model's vertex array. + Vertices = new Vector3[reader.ReadUInt32()]; + + // Read the offset to this model's vertex table, additive to modelDataOffset. This seems to always be 0x20. + ulong vertexTableOffset = reader.ReadUInt64(); + + // Check for a value of 0. + reader.CheckValue(0x00); + + // Initialise this model's face array. + Faces = new Face[reader.ReadUInt32()]; + + // Read the offset to this model's face table, additive to modelDataOffset. + ulong faceTableOffset = reader.ReadUInt64(); + + // Jump to this model's vertex table. + reader.JumpTo(modelDataOffset + vertexTableOffset); + + // Loop through and read all of this model's vertices. + for (int vertexIndex = 0; vertexIndex < Vertices.Length; vertexIndex++) + Vertices[vertexIndex] = reader.ReadVector3(); + + // Jump to this model's face table. + reader.JumpTo(modelDataOffset + faceTableOffset); + + // Loop through and read each of this model's faces. + for (int faceIndex = 0; faceIndex < Faces.Length; faceIndex++) + { + Faces[faceIndex] = new() + { + IndexA = reader.ReadUInt32(), + IndexB = reader.ReadUInt32(), + IndexC = reader.ReadUInt32() + }; + } + + // Jump back for the next model. + reader.JumpTo(position); + } + } + + // Actual data presented to the end user. + public FormatData Data = new(); + + /// + /// Loads and parses this format's file. + /// + /// The path to the file to load and parse. + /// The format version to read this file as. + /// Whether we need to read this file in big endian or not. + public void Load(string filepath, FormatVersion version = FormatVersion.hero, bool bigEndian = false) + { + // Load this file into a BinaryReader. + ExtendedBinaryReader reader = new(File.OpenRead(filepath), bigEndian); + + // Skip an unknown value that is always 0. + reader.CheckValue(0x00); + + // Initialise this file's model array. + Data.Models = new Model[reader.ReadUInt32()]; + + // Read the offset to this file's model table. + ulong modelTableOffset = reader.ReadUInt64(); + + // Read whether or not this file has any screen transition data. It seems that only Shantae and the Seven Sirens does? + bool hasScreenTransitionData = reader.ReadBoolean(0x08); + + // Read the offset to this file's screen transition data, will be 0 if it has none. + ulong screenTransitionTableOffset = reader.ReadUInt64(); + + // Realign to 0x40 bytes. + reader.FixPadding(0x40); + + // Jump to this file's model table offset. + reader.JumpTo(modelTableOffset); + + // Loop through and read each model. + for (int modelIndex = 0; modelIndex < Data.Models.Length; modelIndex++) + Data.Models[modelIndex] = new(reader, version); + + // Check if this file has screen transition data. + if (hasScreenTransitionData) + { + // Jump to the offset for this data. + reader.JumpTo(screenTransitionTableOffset); + + // TODO: Actually read this data properly. + + // Calculate the length of this file's screen transition data. + uint length = (uint)(reader.BaseStream.Length - reader.BaseStream.Position); + + // Read the bytes that make up this file's screen transition data. + Data.ScreenTransition = reader.ReadBytes(length); + } + + // Close our BinaryReader. + reader.Close(); + } + + /// + /// Exports this collision's model data to an OBJ file. + /// TODO: Figure out what I want to do with the screen transition data when it comes to this. + /// TODO: Figure out how I want to present the tags, the @ system is what I want but 3DS Max's OBJ importer is stupid and changes most special characters to an underscore. + /// + /// The filepath to export to. + public void ExportOBJ(string filepath) + { + // Set up an integer to keep track of the amount of vertices. + int vertexCount = 0; + + // Create the StreamWriter. + StreamWriter obj = new(filepath); + + // Loop through each model. + for (int modelIndex = 0; modelIndex < Data.Models.Length; modelIndex++) + { + // Write the Vertex Comment for this model. + obj.WriteLine($"# Model {modelIndex} Vertices\r\n"); + + // Write each vertex. + foreach (Vector3 vertex in Data.Models[modelIndex].Vertices) + obj.WriteLine($"v {vertex.X} {vertex.Y} {vertex.Z}"); + + // Write the Name/Behaviour Tags Comment for this model. + obj.WriteLine($"\r\n# Model {modelIndex} Name and Behaviour Tags\r\n"); + + // Split the flags for this model. + string flags = $"@{string.Join('@', Data.Models[modelIndex].Behaviour.ToString().Split(", "))}"; + + // Write this model's name and flags. + obj.WriteLine($"g model{modelIndex}{flags}"); + obj.WriteLine($"o model{modelIndex}{flags}"); + + // Write the Faces Comment for this model. + obj.WriteLine($"\r\n# Model {modelIndex} Faces\r\n"); + + // Write each face for this model, with the indices incremented by 1 (and the current value of vertexCount) due to OBJ counting from 1 not 0. + foreach (Face face in Data.Models[modelIndex].Faces) + obj.WriteLine($"f {face.IndexA + 1 + vertexCount} {face.IndexB + 1 + vertexCount} {face.IndexC + 1 + vertexCount}"); + + // Add the amount of vertices in this model to the count. + vertexCount += Data.Models[modelIndex].Vertices.Length; + + // Write an empty line for neatness. + obj.WriteLine(); + } + + // Close the StreamWriter. + obj.Close(); + } + } +} diff --git a/KnuxTools/FormatPrints.cs b/KnuxTools/FormatPrints.cs index ea1f5e1..71cf28a 100644 --- a/KnuxTools/FormatPrints.cs +++ b/KnuxTools/FormatPrints.cs @@ -150,6 +150,12 @@ public static void Wayforward() Console.WriteLine("Package Archive (.pak) - Extracts to a directory of the same name as the input archive and creates an archive from an\r\ninput directory."); Helpers.ColourConsole(" Version Flag - wayforward", true, ConsoleColor.Yellow); Helpers.ColourConsole(" Version Flag - wayforward_bigendian", true, ConsoleColor.Yellow); + Helpers.ColourConsole("Collision (.clb)"); + Helpers.ColourConsole(" Version Flag (Ducktales Remastered) - duck", true, ConsoleColor.Yellow); + Helpers.ColourConsole(" Version Flag (Ducktales Remastered (Wii U)) - duck_cafe", true, ConsoleColor.Yellow); + Helpers.ColourConsole(" Version Flag (Shantae: Half-Genie Hero) - hero", true, ConsoleColor.Yellow); + Helpers.ColourConsole(" Version Flag (Shantae: Half-Genie Hero (Wii U)) - hero_cafe", true, ConsoleColor.Yellow); + Helpers.ColourConsole(" Version Flag (Shantae and the Seven Sirens) - sevensirens", true, ConsoleColor.Yellow); } } } diff --git a/KnuxTools/Program.cs b/KnuxTools/Program.cs index 34d64a7..a522fc9 100644 --- a/KnuxTools/Program.cs +++ b/KnuxTools/Program.cs @@ -295,6 +295,34 @@ private static void HandleFile(string arg) break; + case ".clb": + // Check for a format version. + Helpers.VersionChecker("This file has multiple variants that can't be auto detected, please specifiy the variant:", + new() + { + { "duck\t(Ducktales Remastered)", false }, + { "duck_cafe\t(Ducktales Remastered (Wii U))", true }, + { "hero\t(Shantae: Half-Genie Hero)", false }, + { "hero_cafe\t(Shantae: Half-Genie Hero (Wii U))", false }, + { "sevensirens\t(Shantae and the Seven Sirens)", false } + }); + + // If the version is still null or empty, then abort. + if (string.IsNullOrEmpty(Version)) + return; + + switch (Version.ToLower()) + { + case "duck": _ = new KnuxLib.Engines.Wayforward.Collision(arg, KnuxLib.Engines.Wayforward.Collision.FormatVersion.duck, false, true); break; + case "duck_cafe": _ = new KnuxLib.Engines.Wayforward.Collision(arg, KnuxLib.Engines.Wayforward.Collision.FormatVersion.duck, true, true); break; + case "hero": _ = new KnuxLib.Engines.Wayforward.Collision(arg, KnuxLib.Engines.Wayforward.Collision.FormatVersion.hero, false, true); break; + case "hero_cafe": _ = new KnuxLib.Engines.Wayforward.Collision(arg, KnuxLib.Engines.Wayforward.Collision.FormatVersion.hero, true, true); break; + case "sevensirens": _ = new KnuxLib.Engines.Wayforward.Collision(arg, KnuxLib.Engines.Wayforward.Collision.FormatVersion.sevensirens, false, true); break; + default: Helpers.InvalidFormatVersion("Wayforward Engine Collision"); return; + } + + break; + case ".crt": case ".nu2.cratetable.json": // Check for a format version. diff --git a/README.md b/README.md index 454e62f..8ba17d1 100644 --- a/README.md +++ b/README.md @@ -267,6 +267,7 @@ Shantae and the Seven Sirens|iOS, Switch, PlayStation 4, PC, Xbox ONE Name|Extension(s)|Support|[1:1](## "Whether or not KnuxLib can make a binary identical copy of a source file.")|Description ----|----|---------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------|----------- +[Collision](KnuxLib/Engines/Wayforward/Collision.cs)|`*.clb`|[πŸ“œ](## "Read") [πŸ“€](## "Export") [πŸ”§](## "Experimental, a few collision flags are unknown and the version in Seven Sirens has a large chunk of data that has yet to be reverse engineered.")|N/A|A format used by the Wayforward Engine to handle stage collision. [Environment Table](KnuxLib/Engines/Wayforward/Environment.cs)|`*.env`|[πŸ“œ](## "Read") [πŸ’Ύ](## "Write") [πŸ“₯](## "Import") [πŸ“€](## "Export")|βœ”οΈ|A format used by the Wayforward Engine to place static meshes into a scene. [Package Archive](KnuxLib/Engines/Wayforward/Package.cs)|`*.pak`|[πŸ“œ](## "Read") [πŸ’Ύ](## "Write") [πŸ“₯](## "Import") [πŸ“€](## "Export")|βœ”οΈ|An archive format used by the Wayforward Engine.