From dc80644ea75a354edc5f20975e0c007ba077bbf5 Mon Sep 17 00:00:00 2001 From: Daniel Walder Date: Sat, 25 May 2019 14:13:13 +1000 Subject: [PATCH] Add JMF format 121 (read only) --- .../Formats/TestJackhammerFormat.cs | 31 ++ .../Formats/JackhamerJmfFormat.cs | 444 ++++++++++++++++++ Sledge.Formats.Map/Objects/Face.cs | 18 +- Sledge.Formats.Map/Objects/Mesh.cs | 16 + Sledge.Formats.Map/Objects/MeshPoint.cs | 13 + Sledge.Formats.Map/Objects/Solid.cs | 4 + Sledge.Formats.Map/Objects/Surface.cs | 24 + Sledge.Formats.Map/jmf.ksy | 293 ++++++++++++ Sledge.Formats/BinaryExtensions.cs | 1 - 9 files changed, 826 insertions(+), 18 deletions(-) create mode 100644 Sledge.Formats.Map.Tests/Formats/TestJackhammerFormat.cs create mode 100644 Sledge.Formats.Map/Formats/JackhamerJmfFormat.cs create mode 100644 Sledge.Formats.Map/Objects/Mesh.cs create mode 100644 Sledge.Formats.Map/Objects/MeshPoint.cs create mode 100644 Sledge.Formats.Map/Objects/Surface.cs create mode 100644 Sledge.Formats.Map/jmf.ksy diff --git a/Sledge.Formats.Map.Tests/Formats/TestJackhammerFormat.cs b/Sledge.Formats.Map.Tests/Formats/TestJackhammerFormat.cs new file mode 100644 index 0000000..d5a728c --- /dev/null +++ b/Sledge.Formats.Map.Tests/Formats/TestJackhammerFormat.cs @@ -0,0 +1,31 @@ +using System; +using System.IO; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Sledge.Formats.Map.Formats; + +namespace Sledge.Formats.Map.Tests.Formats +{ + [TestClass] + public class TestJackhammerFormat + { + [TestMethod] + public void TestJmfFormatLoading() + { + var format = new JackhammerJmfFormat(); + foreach (var file in Directory.GetFiles(@"D:\Downloads\formats\jmf", "1group.jmf")) + { + using (var r = File.OpenRead(file)) + { + try + { + format.Read(r); + } + catch (Exception ex) + { + Assert.Fail($"Unable to read file: {Path.GetFileName(file)}. {ex.Message}"); + } + } + } + } + } +} \ No newline at end of file diff --git a/Sledge.Formats.Map/Formats/JackhamerJmfFormat.cs b/Sledge.Formats.Map/Formats/JackhamerJmfFormat.cs new file mode 100644 index 0000000..c90969d --- /dev/null +++ b/Sledge.Formats.Map/Formats/JackhamerJmfFormat.cs @@ -0,0 +1,444 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Numerics; +using System.Text; +using Sledge.Formats.Map.Objects; +using Path = Sledge.Formats.Map.Objects.Path; + +namespace Sledge.Formats.Map.Formats +{ + public class JackhammerJmfFormat : IMapFormat + { + public string Name => "Jackhammer JMF"; + public string Description => "The .jmf file format used by Jackhammer and JACK."; + public string ApplicationName => "JACK"; + public string Extension => "jmf"; + public string[] AdditionalExtensions => new[] { "jmf" }; + public string[] SupportedStyleHints => new[] { "" }; + + public MapFile Read(Stream stream) + { + using (var br = new BinaryReader(stream, Encoding.ASCII, true)) + { + // JMF header test + var header = br.ReadFixedLengthString(Encoding.ASCII, 4); + Util.Assert(header == "JHMF", $"Incorrect JMF header. Expected 'JHMF', got '{header}'."); + + // Only JHMF version 121 is supported for the moment. + var version = br.ReadInt32(); + Util.Assert(version == 121, $"Unsupported JMF version number. Expected 121, got {version}."); + + // Appears to be an array of locations to export to .map + var numExportStrings = br.ReadInt32(); + for (var i = 0; i < numExportStrings; i++) + { + ReadString(br); + } + + var map = new MapFile(); + + var groups = ReadGroups(map, br); + ReadVisgroups(map, br); + var cordonLow = br.ReadVector3(); + var cordonHigh = br.ReadVector3(); + ReadCameras(map, br); + ReadPaths(map, br); + var entities = ReadEntities(map, br); + + BuildTree(map, groups, entities); + + return map; + } + } + + #region Read + + private void BuildTree(MapFile map, IEnumerable groups, IReadOnlyCollection entities) + { + var groupIds = new Dictionary(); // file group id -> actual id + var objTree = new Dictionary(); // object id -> object + + var currentId = 2; // worldspawn is 1 + objTree[1] = map.Worldspawn; + + var worldspawnEntity = entities.FirstOrDefault(x => x.Entity.ClassName == "worldspawn"); + if (worldspawnEntity != null) + { + map.Worldspawn.Properties = worldspawnEntity.Entity.Properties; + map.Worldspawn.Color = worldspawnEntity.Entity.Color; + map.Worldspawn.SpawnFlags = worldspawnEntity.Entity.SpawnFlags; + map.Worldspawn.Visgroups = worldspawnEntity.Entity.Visgroups; + } + + // Jackhammer doesn't allow a group within an entity, so groups + // will only be children of worldspawn or another group. We can + // build the group hierarchy immediately. + var groupList = groups.ToList(); + var groupCount = groupList.Count; + while (groupList.Any()) + { + var pcs = groupList.Where(x => x.ID == x.ParentID || x.ParentID == 0 || groupIds.ContainsKey(x.ParentID)).ToList(); + foreach (var g in pcs) + { + var gid = currentId++; + groupIds[g.ID] = gid; + groupList.Remove(g); + + var group = new Group + { + Color = g.Color + }; + + var parentObjId = g.ID == g.ParentID || g.ParentID == 0 ? 1 : groupIds[g.ParentID]; + objTree[parentObjId].Children.Add(group); + objTree[gid] = group; + } + + if (groupList.Count == groupCount) break; // no groups processed, can't continue + groupCount = groupList.Count; + } + + // For non-worldspawn solids, they are direct children of their entity. + // For non-worldspawn entities, they're either a child of a group or of the worldspawn. + foreach (var entity in entities.Where(x => x != worldspawnEntity)) + { + var parentId = groupIds.ContainsKey(entity.GroupID) ? groupIds[entity.GroupID] : 1; + objTree[parentId].Children.Add(entity.Entity); + + // Put all the entity's solids straight underneath this entity + entity.Entity.Children.AddRange(entity.Solids.Select(x => x.Solid)); + } + + // For worldspawn solids, they're either a child of a group or of the worldspawn. + if (worldspawnEntity != null) + { + foreach (var solid in worldspawnEntity.Solids) + { + var parentId = groupIds.ContainsKey(solid.GroupID) ? groupIds[solid.GroupID] : 1; + objTree[parentId].Children.Add(solid.Solid); + } + } + } + + private List ReadGroups(MapFile map, BinaryReader br) + { + var groups = new List(); + + var numGroups = br.ReadInt32(); + for (var i = 0; i < numGroups; i++) + { + var g = new JmfGroup + { + ID = br.ReadInt32(), + ParentID = br.ReadInt32(), + Flags = br.ReadInt32(), + NumObjects = br.ReadInt32(), + Color = br.ReadRGBAColour() + }; + groups.Add(g); + } + + return groups; + } + + private static void ReadVisgroups(MapFile map, BinaryReader br) + { + var numVisgroups = br.ReadInt32(); + for (var i = 0; i < numVisgroups; i++) + { + var vis = new Visgroup + { + Name = ReadString(br), + ID = br.ReadInt32(), + Color = br.ReadRGBAColour(), + Visible = br.ReadBoolean() + }; + map.Visgroups.Add(vis); + } + } + + private void ReadCameras(MapFile map, BinaryReader br) + { + var numCameras = br.ReadInt32(); + for (var i = 0; i < numCameras; i++) + { + var vis = new Camera + { + EyePosition = br.ReadVector3(), + LookPosition = br.ReadVector3() + }; + br.ReadInt32(); // something + br.ReadInt32(); // something 2 + map.Cameras.Add(vis); + } + } + + private void ReadPaths(MapFile map, BinaryReader br) + { + var numPaths = br.ReadInt32(); + for (var i = 0; i < numPaths; i++) + { + map.Paths.Add(ReadPath(br)); + } + } + + private static Path ReadPath(BinaryReader br) + { + var path = new Path + { + Type = ReadString(br), + Name = ReadString(br), + Direction = (PathDirection) br.ReadInt32() + }; + br.ReadInt32(); // flags + br.ReadRGBAColour(); // colour + + var numNodes = br.ReadInt32(); + for (var i = 0; i < numNodes; i++) + { + var name = ReadString(br); + var fire = ReadString(br); // fire on pass + var node = new PathNode + { + Name = name, + Position = br.ReadVector3() + }; + + if (!String.IsNullOrWhiteSpace(fire)) node.Properties["message"] = fire; + + var angles = br.ReadVector3(); + node.Properties["angles"] = $"{angles.X} {angles.Y} {angles.Z}"; + + node.Properties["spawnflags"] = br.ReadInt32().ToString(); + + br.ReadRGBAColour(); // colour + + var numProps = br.ReadInt32(); + for (var j = 0; j < numProps; j++) + { + var key = ReadString(br); + var value = ReadString(br); + if (key != null && value != null) node.Properties[key] = value; + } + + path.Nodes.Add(node); + } + return path; + } + + private List ReadEntities(MapFile map, BinaryReader br) + { + var entities = new List(); + while (br.BaseStream.Position < br.BaseStream.Length) + { + var ent = new JmfEntity + { + Entity = new Entity + { + ClassName = ReadString(br) + } + }; + + var origin = br.ReadVector3(); + ent.Entity.Properties["origin"] = $"{origin.X} {origin.Y} {origin.Z}"; + + ent.Flags = br.ReadInt32(); + ent.GroupID = br.ReadInt32(); + br.ReadInt32(); // group id again + ent.Entity.Color = br.ReadRGBAColour(); + + // useless (?) list of 13 strings + for (var i = 0; i < 13; i++) ReadString(br); + + ent.Entity.SpawnFlags = br.ReadInt32(); + + br.ReadBytes(76); // unknown (!) + + var numProps = br.ReadInt32(); + for (var i = 0; i < numProps; i++) + { + var key = ReadString(br); + var value = ReadString(br); + if (key != null && value != null) ent.Entity.Properties[key] = value; + } + + ent.Entity.Visgroups = new List(); + + var numVisgroups = br.ReadInt32(); + for (var i = 0; i < numVisgroups; i++) + { + ent.Entity.Visgroups.Add(br.ReadInt32()); + } + + var numSolids = br.ReadInt32(); + for (var i = 0; i < numSolids; i++) + { + ent.Solids.Add(ReadSolid(map, br)); + } + + entities.Add(ent); + } + + return entities; + } + + private JmfSolid ReadSolid(MapFile map, BinaryReader br) + { + var solid = new JmfSolid + { + Solid = new Solid() + }; + + var numPatches = br.ReadInt32(); + solid.Flags = br.ReadInt32(); + solid.GroupID = br.ReadInt32(); + br.ReadInt32(); // group id again + solid.Solid.Color = br.ReadRGBAColour(); + + var numVisgroups = br.ReadInt32(); + for (var i = 0; i < numVisgroups; i++) + { + solid.Solid.Visgroups.Add(br.ReadInt32()); + } + + var numFaces = br.ReadInt32(); + for (var i = 0; i < numFaces; i++) + { + solid.Solid.Faces.Add(ReadFace(br)); + } + + for (var i = 0; i < numPatches; i++) + { + solid.Solid.Meshes.Add(ReadPatch(br)); + } + + return solid; + } + + private Face ReadFace(BinaryReader br) + { + var face = new Face(); + + br.ReadInt32(); // something + + var numVertices = br.ReadInt32(); + ReadSurfaceProperties(face, br); + + var norm = br.ReadVector3(); + var distance = br.ReadSingle(); + face.Plane = new Plane(norm, distance); + + br.ReadInt32(); // something 2 + + for (var i = 0; i < numVertices; i++) + { + br.ReadVector3(); // texture coordinate + face.Vertices.Add(br.ReadVector3()); + } + + return face; + } + + private Mesh ReadPatch(BinaryReader br) + { + var mesh = new Mesh + { + Width = br.ReadInt32(), + Height = br.ReadInt32(), + + }; + + ReadSurfaceProperties(mesh, br); + + br.ReadInt32(); // something + + for (var i = 0; i < 32; i++) + { + for (var j = 0; j < 32; j++) + { + var point = new MeshPoint + { + X = i, + Y = j, + Position = br.ReadVector3(), + Normal = br.ReadVector3(), + Texture = br.ReadVector3() + }; + + if (i < mesh.Width && j < mesh.Height) + { + mesh.Points.Add(point); + } + } + } + + return mesh; + } + + private void ReadSurfaceProperties(Surface surface, BinaryReader br) + { + surface.UAxis = br.ReadVector3(); + surface.XShift = br.ReadSingle(); + surface.VAxis = br.ReadVector3(); + surface.YShift = br.ReadSingle(); + surface.XScale = br.ReadSingle(); + surface.YScale = br.ReadSingle(); + surface.Rotation = br.ReadSingle(); + + br.ReadInt32(); // something 1 + br.ReadInt32(); // something 2 + br.ReadInt32(); // something 3 + br.ReadInt32(); // something 4 + + surface.SurfaceFlags = br.ReadInt32(); // or content flags? + surface.TextureName = br.ReadFixedLengthString(Encoding.ASCII, 64); + } + + #endregion + + public void Write(Stream stream, MapFile map, string styleHint) + { + throw new NotImplementedException(); + } + + private static string ReadString(BinaryReader br) + { + var len = br.ReadInt32(); + if (len < 0) return null; + var chars = br.ReadChars(len); + return new string(chars).Trim('\0'); + } + + private class JmfGroup + { + public int ID { get; set; } + public int ParentID { get; set; } + public int Flags { get; set; } + public int NumObjects { get; set; } + public Color Color { get; set; } + } + + private class JmfEntity + { + public int Flags { get; set; } + public int GroupID { get; set; } + public Entity Entity { get; set; } + public List Solids { get; set; } + + public JmfEntity() + { + Solids = new List(); + } + } + + private class JmfSolid + { + public int Flags { get; set; } + public int GroupID { get; set; } + public Solid Solid { get; set; } + } + } +} diff --git a/Sledge.Formats.Map/Objects/Face.cs b/Sledge.Formats.Map/Objects/Face.cs index baf6888..0279bed 100644 --- a/Sledge.Formats.Map/Objects/Face.cs +++ b/Sledge.Formats.Map/Objects/Face.cs @@ -3,27 +3,11 @@ namespace Sledge.Formats.Map.Objects { - public class Face + public class Face : Surface { public Plane Plane { get; set; } public List Vertices { get; set; } - public string TextureName { get; set; } - public Vector3 UAxis { get; set; } - public Vector3 VAxis { get; set; } - public float XScale { get; set; } - public float YScale { get; set; } - public float XShift { get; set; } - public float YShift { get; set; } - public float Rotation { get; set; } - - public int ContentFlags { get; set; } - public int SurfaceFlags { get; set; } - public float Value { get; set; } - - public float LightmapScale { get; set; } - public string SmoothingGroups { get; set; } - public Face() { Vertices = new List(); diff --git a/Sledge.Formats.Map/Objects/Mesh.cs b/Sledge.Formats.Map/Objects/Mesh.cs new file mode 100644 index 0000000..7c8ba9f --- /dev/null +++ b/Sledge.Formats.Map/Objects/Mesh.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace Sledge.Formats.Map.Objects +{ + public class Mesh : Surface + { + public int Width { get; set; } + public int Height { get; set; } + public List Points { get; set; } + + public Mesh() + { + Points = new List(); + } + } +} \ No newline at end of file diff --git a/Sledge.Formats.Map/Objects/MeshPoint.cs b/Sledge.Formats.Map/Objects/MeshPoint.cs new file mode 100644 index 0000000..b55a2f5 --- /dev/null +++ b/Sledge.Formats.Map/Objects/MeshPoint.cs @@ -0,0 +1,13 @@ +using System.Numerics; + +namespace Sledge.Formats.Map.Objects +{ + public class MeshPoint + { + public int X { get; set; } + public int Y { get; set; } + public Vector3 Position { get; set; } + public Vector3 Normal { get; set; } + public Vector3 Texture { get; set; } + } +} \ No newline at end of file diff --git a/Sledge.Formats.Map/Objects/Solid.cs b/Sledge.Formats.Map/Objects/Solid.cs index 5d03b73..19377e7 100644 --- a/Sledge.Formats.Map/Objects/Solid.cs +++ b/Sledge.Formats.Map/Objects/Solid.cs @@ -7,14 +7,18 @@ namespace Sledge.Formats.Map.Objects public class Solid : MapObject { public List Faces { get; set; } + public List Meshes { get; set; } public Solid() { Faces = new List(); + Meshes = new List(); } public void ComputeVertices() { + if (Faces.Count < 4) return; + var poly = new Polyhedron(Faces.Select(x => new Plane(x.Plane.Normal.ToPrecisionVector3(), x.Plane.D))); foreach (var face in Faces) diff --git a/Sledge.Formats.Map/Objects/Surface.cs b/Sledge.Formats.Map/Objects/Surface.cs new file mode 100644 index 0000000..a9704ca --- /dev/null +++ b/Sledge.Formats.Map/Objects/Surface.cs @@ -0,0 +1,24 @@ +using System.Numerics; + +namespace Sledge.Formats.Map.Objects +{ + public class Surface + { + public string TextureName { get; set; } + + public Vector3 UAxis { get; set; } + public Vector3 VAxis { get; set; } + public float XScale { get; set; } + public float YScale { get; set; } + public float XShift { get; set; } + public float YShift { get; set; } + public float Rotation { get; set; } + + public int ContentFlags { get; set; } + public int SurfaceFlags { get; set; } + public float Value { get; set; } + + public float LightmapScale { get; set; } + public string SmoothingGroups { get; set; } + } +} \ No newline at end of file diff --git a/Sledge.Formats.Map/jmf.ksy b/Sledge.Formats.Map/jmf.ksy new file mode 100644 index 0000000..3b50d9f --- /dev/null +++ b/Sledge.Formats.Map/jmf.ksy @@ -0,0 +1,293 @@ +meta: + id: jmf + file-extension: jmf + endian: le +seq: + - id: header + type: header + - id: entities + type: entity + repeat: eos +types: + header: + seq: + - id: magic + contents: 'JHMF' + - id: version + type: s4 + - id: something + type: s4 + - id: num_groups + type: s4 + - id: groups + type: group + repeat: expr + repeat-expr: num_groups + - id: num_visgroups + type: s4 + - id: visgroups + type: visgroup + repeat: expr + repeat-expr: num_visgroups + - id: cordon_low + type: point + - id: cordon_high + type: point + - id: num_cameras + type: s4 + - id: cameras + type: camera + repeat: expr + repeat-expr: num_cameras + - id: num_paths + type: s4 + - id: paths + type: path + repeat: expr + repeat-expr: num_paths + group: + seq: + - id: id + type: s4 + - id: parent_id + type: s4 + - id: flags + type: s4 + - id: num_objects + type: s4 + - id: color + type: color + visgroup: + seq: + - id: name + type: szp_str + - id: id + type: s4 + - id: color + type: color + - id: visible + type: u1 + camera: + seq: + - id: position + type: point + - id: lookat + type: point + - id: something + type: s4 + - id: something2 + type: s4 + path: + seq: + - id: classname + type: szp_str + - id: name + type: szp_str + - id: direction + type: s4 + - id: flags + type: s4 + - id: color + type: color + - id: num_nodes + type: s4 + - id: nodes + type: path_node + repeat: expr + repeat-expr: num_nodes + path_node: + seq: + - id: name_override + type: szp_str + - id: fire_on_pass + type: szp_str + - id: position + type: point + - id: angles + type: point + - id: flags + type: s4 + - id: color + type: color + - id: num_keyvalues + type: s4 + - id: keyvalues + type: keyvalue + repeat: expr + repeat-expr: num_keyvalues + entity: + seq: + - id: classname + type: szp_str + - id: origin + type: point + - id: flags + type: u4 + - id: group_id + type: u4 + - id: group_id_again + type: u4 + - id: color + type: color + - id: hardcoded_properties + type: szp_str + repeat: expr + repeat-expr: 13 + - id: spawnflags + type: u4 + - id: unknown + size: 76 + - id: num_keyvalues + type: s4 + - id: keyvalues + type: keyvalue + repeat: expr + repeat-expr: num_keyvalues + - id: num_visgroups + type: s4 + - id: visgroups + type: s4 + repeat: expr + repeat-expr: num_visgroups + - id: num_solids + type: s4 + - id: solids + type: solid + repeat: expr + repeat-expr: num_solids + solid: + seq: + - id: num_patches + type: s4 + - id: flags + type: u4 + - id: group_id + type: u4 + - id: group_id_again + type: u4 + - id: color + type: color + - id: num_visgroups + type: s4 + - id: visgroups + type: s4 + repeat: expr + repeat-expr: num_visgroups + - id: num_faces + type: s4 + - id: faces + type: face + repeat: expr + repeat-expr: num_faces + - id: patches + type: patch + repeat: expr + repeat-expr: num_patches + face: + seq: + - id: something + type: s4 + - id: num_vertices + type: s4 + - id: texture + type: surface_properties + - id: plane_normal + type: point + - id: plane_distance + type: f4 + - id: something2 + type: u4 + - id: vertices + type: vertex + repeat: expr + repeat-expr: num_vertices + patch: + seq: + - id: width + type: s4 + - id: height + type: s4 + - id: texture + type: surface_properties + - id: something + type: s4 + - id: points + type: patch_point + repeat: expr + repeat-expr: 32 * 32 + patch_point: + seq: + - id: position + type: point + - id: normal + type: point + - id: texture_coordinate + type: point + surface_properties: + seq: + - id: x_axis + type: point + - id: x_shift + type: f4 + - id: y_axis + type: point + - id: y_shift + type: f4 + - id: x_scale + type: f4 + - id: y_scale + type: f4 + - id: rotation + type: f4 + - id: something1 + type: s4 + - id: something2 + type: s4 + - id: something3 + type: s4 + - id: something4 + type: s4 + - id: flags + type: s4 + - id: texture_name + type: strz + encoding: ASCII + size: 64 + point: + seq: + - id: x + type: f4 + - id: y + type: f4 + - id: z + type: f4 + vertex: + seq: + - id: texture_coordinate + type: point + - id: position + type: point + color: + seq: + - id: r + type: u1 + - id: g + type: u1 + - id: b + type: u1 + - id: a + type: u1 + szp_str: + seq: + - id: size + type: s4 + - id: value + type: strz + size: size < 0 ? 0 : size + encoding: ASCII + keyvalue: + seq: + - id: key + type: szp_str + - id: value + type: szp_str \ No newline at end of file diff --git a/Sledge.Formats/BinaryExtensions.cs b/Sledge.Formats/BinaryExtensions.cs index 7f77841..6f5a385 100644 --- a/Sledge.Formats/BinaryExtensions.cs +++ b/Sledge.Formats/BinaryExtensions.cs @@ -81,7 +81,6 @@ public static string ReadCString(this BinaryReader br) return new string(chars).Trim('\0'); } - /// /// Write a length-prefixed string to the writer. ///