Skip to content

Commit

Permalink
Initial support for prefab libraries added to JACK beta version
Browse files Browse the repository at this point in the history
  • Loading branch information
LogicAndTrick committed Dec 28, 2024
1 parent f1d34f0 commit fe515c8
Show file tree
Hide file tree
Showing 5 changed files with 268 additions and 2 deletions.
96 changes: 96 additions & 0 deletions Sledge.Formats.Map.Tests/Formats/TestJackhammerPrefabLibrary.cs
Original file line number Diff line number Diff line change
@@ -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<Entity>().First().ClassName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
todo: replace this file with boxes.jol once you are able to create prefabs in free JACK
168 changes: 168 additions & 0 deletions Sledge.Formats.Map/Formats/JackhammerPrefabLibrary.cs
Original file line number Diff line number Diff line change
@@ -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<Prefab> 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<Prefab>();
}

public JackhammerPrefabLibrary(Stream stream)
{
Prefabs = new List<Prefab>();
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>
{
"" // 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<Entry>();

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; }
}
}
}
1 change: 1 addition & 0 deletions Sledge.Formats.Map/Objects/Prefab.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
4 changes: 2 additions & 2 deletions Sledge.Formats.Map/Sledge.Formats.Map.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
<RepositoryUrl>https://github.com/LogicAndTrick/sledge-formats</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<PackageTags>half-life quake valve hammer worldcraft jackhammer jack rmf vmf map jmf</PackageTags>
<PackageReleaseNotes>Fix incorrect loading of texture rotation and shift values for pre-2.2 RMF files</PackageReleaseNotes>
<Version>1.2.4</Version>
<PackageReleaseNotes>Initial support for JACK prefab libraries (the JACK version with prefabs is in beta, so compatibility might change)</PackageReleaseNotes>
<Version>1.2.5</Version>
<DebugType>full</DebugType>
<DebugSymbols>true</DebugSymbols>
</PropertyGroup>
Expand Down

0 comments on commit fe515c8

Please sign in to comment.