diff --git a/Sledge.Formats.Map.Tests/Formats/TestJackhammerPrefabLibrary.cs b/Sledge.Formats.Map.Tests/Formats/TestJackhammerPrefabLibrary.cs new file mode 100644 index 0000000..e060ab4 --- /dev/null +++ b/Sledge.Formats.Map.Tests/Formats/TestJackhammerPrefabLibrary.cs @@ -0,0 +1,96 @@ +using System.IO; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Sledge.Formats.Map.Formats; +using Sledge.Formats.Map.Objects; + +namespace Sledge.Formats.Map.Tests.Formats; + +[TestClass] +public class TestJackhammerPrefabLibrary +{ + /* + // Commented out because I don't want to include the default JACK files due to licensing. + // Put them into the jack_prefab as embedded resources if you want to run this test. + [DataTestMethod] + [DataRow("Computers.jol")] + [DataRow("Crates.jol")] + [DataRow("Old Stuff.jol")] + [DataRow("Random Objects.jol")] + [DataRow("Usable Objects.jol")] + public void TestDefaultPrefabs(string name) + { + using var file = typeof(TestJackhammerPrefabLibrary).Assembly.GetManifestResourceStream($"Sledge.Formats.Map.Tests.Resources.jack_prefab.{name}"); + var library = new JackhammerPrefabLibrary(file); + Console.WriteLine(library.Description); + foreach (var p in library.Prefabs) + { + Console.WriteLine(p.Name + " : " + p.Description); + } + } + */ + + // todo: need to create the boxes.jol file for the test, however as of writing it requires the paid version + [TestMethod] + public void TestSimplePrefab() + { + using var file = typeof(TestJackhammerPrefabLibrary).Assembly.GetManifestResourceStream("Sledge.Formats.Map.Tests.Resources.jack_prefab.boxes.jol"); + var library = new JackhammerPrefabLibrary(file); + + Assert.AreEqual(2, library.Prefabs.Count); + Assert.AreEqual("some boxes", library.Description); + + Assert.AreEqual("box1", library.Prefabs[0].Name); + Assert.AreEqual("box number one", library.Prefabs[0].Description); + + Assert.AreEqual("box2", library.Prefabs[1].Name); + Assert.AreEqual("second box", library.Prefabs[1].Description); + } + + // todo: writing not supported yet. need to know how the unknown fields are handled by the app to find out more + [TestMethod] + public void TestWritingPrefab() + { + using var file = typeof(TestJackhammerPrefabLibrary).Assembly.GetManifestResourceStream("Sledge.Formats.Map.Tests.Resources.jack_prefab.boxes.jol"); + var library = new JackhammerPrefabLibrary(file); + + var newLib = new JackhammerPrefabLibrary + { + Description = "write test" + }; + + var testMap = new MapFile(); + testMap.Worldspawn.Children.Add(new Entity + { + ClassName = "this_is_a_test" + }); + + newLib.Prefabs.Add(new Prefab("test1", "testing", library.Prefabs[0].Map)); + newLib.Prefabs.Add(new Prefab("test2", "more testing", library.Prefabs[1].Map)); + newLib.Prefabs.Add(new Prefab("test3", "even more testing", library.Prefabs[1].Map)); + newLib.Prefabs.Add(new Prefab("test4", "final test", testMap)); + + var ms = new MemoryStream(); + newLib.Write(ms); + ms.Position = 0; + + var openLib = new JackhammerPrefabLibrary(ms); + + Assert.AreEqual(4, openLib.Prefabs.Count); + Assert.AreEqual("write test", openLib.Description); + + Assert.AreEqual("test1", openLib.Prefabs[0].Name); + Assert.AreEqual("testing", openLib.Prefabs[0].Description); + + Assert.AreEqual("test2", openLib.Prefabs[1].Name); + Assert.AreEqual("more testing", openLib.Prefabs[1].Description); + + Assert.AreEqual("test3", openLib.Prefabs[2].Name); + Assert.AreEqual("even more testing", openLib.Prefabs[2].Description); + + Assert.AreEqual("test4", openLib.Prefabs[3].Name); + Assert.AreEqual("final test", openLib.Prefabs[3].Description); + Assert.AreEqual(1, openLib.Prefabs[3].Map.Worldspawn.Children.Count); + Assert.AreEqual("this_is_a_test", openLib.Prefabs[3].Map.Worldspawn.Children.OfType().First().ClassName); + } +} \ No newline at end of file diff --git a/Sledge.Formats.Map.Tests/Resources/jack_prefab/placeholder.txt b/Sledge.Formats.Map.Tests/Resources/jack_prefab/placeholder.txt new file mode 100644 index 0000000..473c3c6 --- /dev/null +++ b/Sledge.Formats.Map.Tests/Resources/jack_prefab/placeholder.txt @@ -0,0 +1 @@ +todo: replace this file with boxes.jol once you are able to create prefabs in free JACK \ No newline at end of file diff --git a/Sledge.Formats.Map/Formats/JackhammerPrefabLibrary.cs b/Sledge.Formats.Map/Formats/JackhammerPrefabLibrary.cs new file mode 100644 index 0000000..e5a2f40 --- /dev/null +++ b/Sledge.Formats.Map/Formats/JackhammerPrefabLibrary.cs @@ -0,0 +1,168 @@ +using Sledge.Formats.Map.Objects; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Sledge.Formats.Map.Formats +{ + public class JackhammerPrefabLibrary + { + private const int Version = 1; + + public string Description { get; set; } + public List Prefabs { get; set; } + + public static JackhammerPrefabLibrary FromFile(string file) + { + using (var stream = File.OpenRead(file)) + { + return new JackhammerPrefabLibrary(stream); + } + } + + public JackhammerPrefabLibrary() + { + Description = ""; + Prefabs = new List(); + } + + public JackhammerPrefabLibrary(Stream stream) + { + Prefabs = new List(); + using (var br = new BinaryReader(stream)) + { + var header = br.ReadFixedLengthString(Encoding.ASCII, 4); + Util.Assert(header == "JHOL", $"Incorrect prefab library header. Expected 'JHOL', got '{header}'."); + + var version = br.ReadInt32(); + Util.Assert(version == Version, $"Unsupported prefab library version number. Expected {Version}, got {version}."); + + // read header data + var descriptionStringNum = br.ReadInt32(); + _ = br.ReadInt32(); // unknown + _ = br.ReadInt32(); // unknown + var singleEntryLength = br.ReadInt32(); + var entryDataOffset = br.ReadInt32(); + var entryDataLength = br.ReadInt32(); + var mapDataOffset = br.ReadInt32(); + var mapDataLength = br.ReadInt32(); + var imageDataOffset = br.ReadInt32(); + var imageDataLength = br.ReadInt32(); + var stringDataOffset = br.ReadInt32(); + var stringDataLength = br.ReadInt32(); + + // read strings + br.BaseStream.Seek(stringDataOffset, SeekOrigin.Begin); + + var stringCount = br.ReadInt32(); + + // for whatever reason the offset to each string is stored next, we don't need those + br.BaseStream.Seek(stringCount * 4, SeekOrigin.Current); + + var strings = new List + { + "" // string numbers seem to be indexed from 1 so add a blank string for index 0 + }; + for (var i = 0; i < stringCount; i++) + { + var len = br.ReadInt32(); + strings.Add(br.ReadFixedLengthString(Encoding.ASCII, len)); + } + + Description = strings[descriptionStringNum]; + + // read entries + br.BaseStream.Seek(entryDataOffset, SeekOrigin.Begin); + + var entries = new List(); + + var numEntries = entryDataLength / singleEntryLength; + for (var i = 0; i < numEntries; i++) + { + var name = strings[br.ReadInt32()]; + var desc = strings[br.ReadInt32()]; + _ = br.ReadInt32(); // unknown + _ = br.ReadInt32(); // unknown + var entryMapOffset = br.ReadInt32(); + var entryMapLength = br.ReadInt32(); + var entryMapType = br.ReadFixedLengthString(Encoding.ASCII, 4); + var entryImageOffset = br.ReadInt32(); + var entryImageLength = br.ReadInt32(); + var entryImageType = br.ReadFixedLengthString(Encoding.ASCII, 4); + + if (entryMapType != "JHMF") + { + throw new NotSupportedException("Unexpected non-JMF format in Jackhammer prefab library."); + } + + // i suspect this is an origin or bounding box, but not sure + (_, _, _) = (br.ReadDouble(), br.ReadDouble(), br.ReadDouble()); + + var numWorldspawns = br.ReadInt32(); // ?? no idea, always seems to be 1 + var numSolids = br.ReadInt32(); + var numPointEnts = br.ReadInt32(); + var numSolidEnts = br.ReadInt32(); + _ = br.ReadInt32(); // unknown + _ = br.ReadInt32(); // unknown + _ = br.ReadInt32(); // unknown + var numUniqueTextures = br.ReadInt32(); + + entries.Add(new Entry + { + Name = name, + Description = desc, + MapOffset = entryMapOffset, + MapLength = entryMapLength, + MapType = entryMapType, + ImageOffset = entryImageOffset, + ImageLength = entryImageLength, + ImageType = entryImageType + }); + } + + // read maps and preview images + var jmf = new JackhammerJmfFormat(); + + foreach (var entry in entries) + { + br.BaseStream.Seek(imageDataOffset + entry.ImageOffset, SeekOrigin.Begin); + var img = br.ReadBytes(entry.ImageLength); + using (var substream = new SubStream(br.BaseStream, mapDataOffset + entry.MapOffset, entry.MapLength)) + { + Prefabs.Add(new Prefab(entry.Name, entry.Description, jmf.Read(substream)) + { + PreviewImage = img + }); + } + } + } + } + + public void WriteToFile(string file) + { + using (var stream = File.OpenWrite(file)) + { + Write(stream); + } + } + + public void Write(Stream stream) + { + throw new NotImplementedException(); + } + + // record class to hold an entry temporarily + private class Entry + { + public string Name { get; set; } + public string Description { get; set; } + public int MapOffset { get; set; } + public int MapLength { get; set; } + public string MapType { get; set; } + public int ImageOffset { get; set; } + public int ImageLength { get; set; } + public string ImageType { get; set; } + } + } +} diff --git a/Sledge.Formats.Map/Objects/Prefab.cs b/Sledge.Formats.Map/Objects/Prefab.cs index 967319a..b8add99 100644 --- a/Sledge.Formats.Map/Objects/Prefab.cs +++ b/Sledge.Formats.Map/Objects/Prefab.cs @@ -5,6 +5,7 @@ public class Prefab public string Name { get; set; } public string Description { get; set; } public MapFile Map { get; set; } + public byte[] PreviewImage { get; set; } public Prefab() { diff --git a/Sledge.Formats.Map/Sledge.Formats.Map.csproj b/Sledge.Formats.Map/Sledge.Formats.Map.csproj index bde98f1..30a0013 100644 --- a/Sledge.Formats.Map/Sledge.Formats.Map.csproj +++ b/Sledge.Formats.Map/Sledge.Formats.Map.csproj @@ -11,8 +11,8 @@ https://github.com/LogicAndTrick/sledge-formats Git half-life quake valve hammer worldcraft jackhammer jack rmf vmf map jmf - Fix incorrect loading of texture rotation and shift values for pre-2.2 RMF files - 1.2.4 + Initial support for JACK prefab libraries (the JACK version with prefabs is in beta, so compatibility might change) + 1.2.5 full true