Add loading of neighborhood object XML files.

These are the files that assosciate the neighborhood GUIDs to the
models. This also adds a class to handle cGZPropertySetString
files since we will likely need to parse those more in the future.
This commit is contained in:
Ammar Askar 2023-07-19 22:50:35 -04:00
parent fcb7484d80
commit 2d439d9552
15 changed files with 230 additions and 14 deletions

View file

@ -0,0 +1,18 @@
using OpenTS2.Files.Formats.XML;
namespace OpenTS2.Content.DBPF
{
public class NeighborhoodObjectAsset : AbstractAsset
{
/// <summary>
/// The name of the model for this neighborhood object.
/// </summary>
public string ModelName { get; }
/// <summary>
/// The global unique id for the object.
/// </summary>
public uint Guid { get; }
public NeighborhoodObjectAsset(string modelName, uint guid) => (ModelName, Guid) = (modelName, guid);
}
}

View file

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2817bdbb142146e69613242edf3d8f21
timeCreated: 1689819849

View file

@ -26,6 +26,7 @@ namespace OpenTS2.Files.Formats.DBPF
public const uint NHOOD_INFO = 0xAC8A7A2E;
// Called neighborhood "occupants" in game.
public const uint NHOOD_DECORATIONS = 0xABD0DC63;
public const uint NHOOD_OBJECT = 0x6D619378;
public const uint SCENEGRAPH_GMDC = Scenegraph.Block.GeometryDataContainerBlock.TYPE_ID;
public const uint STR = 0x53545223;
public const uint IMG = 0x856DDBAC;

View file

@ -47,8 +47,9 @@ namespace OpenTS2.Files.Formats.DBPF
private static FloraDecoration[] ReadFlora(IoBuffer reader)
{
var version = reader.ReadUInt16();
// Versions above this are read differently but don't seem to exist in practice.
Debug.Assert(version >= 6);
// Versions above this are read differently (e.g they don't have individual objectVersion fields inside)
// but don't seem to exist in practice.
Debug.Assert(version > 7);
var count = reader.ReadUInt32();
var flora = new FloraDecoration[count];
@ -57,21 +58,14 @@ namespace OpenTS2.Files.Formats.DBPF
{
var position = ReadDecorationPosition(reader);
var objectVersion = reader.ReadByte();
// Same as above, there is code to handle versions below this in the game but these don't seem to exist
// and don't even have a rotation or GUID.
Debug.Assert(objectVersion > 7);
var rotation = 0.0f;
if (objectVersion >= 7)
{
rotation = reader.ReadFloat();
}
var rotation = reader.ReadFloat();
position.Rotation = rotation;
uint objectId = 0;
if (objectVersion > 7)
{
objectId = reader.ReadUInt32();
}
var objectId = reader.ReadUInt32();
flora[i] = new FloraDecoration(position, objectId);
}

View file

@ -0,0 +1,29 @@
using System;
using System.Text;
using OpenTS2.Common;
using OpenTS2.Content;
using OpenTS2.Content.DBPF;
using OpenTS2.Files.Formats.XML;
namespace OpenTS2.Files.Formats.DBPF
{
[Codec(TypeIDs.NHOOD_OBJECT)]
public class NeighborhoodObjectCodec : AbstractCodec
{
public override AbstractAsset Deserialize(byte[] bytes, ResourceKey tgi, DBPFFile sourceFile)
{
var propertySet = new PropertySet(Encoding.UTF8.GetString(bytes));
var modelName = propertySet.GetProperty<StringProp>("modelname").Value;
// Yup, the game just tacks on a _cres to the end of this.
if (!modelName.EndsWith("_cres"))
{
modelName += "_cres";
}
return new NeighborhoodObjectAsset(
modelName,
propertySet.GetProperty<Uint32Prop>("guid").Value);
}
}
}

View file

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: dfc8dbc0be2a4fc4b646e74ae9dfb3f6
timeCreated: 1689819769

View file

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7d1809ee5d5a4b6fad8a81f87bf3b9f9
timeCreated: 1689816978

View file

@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using UnityEngine;
namespace OpenTS2.Files.Formats.XML
{
/// <summary>
/// A cGZPropertySetString element that stores strings keys and arbitrary values in xml files.
/// </summary>
public class PropertySet
{
public Dictionary<string, IPropertyType> Properties { get; } = new Dictionary<string, IPropertyType>();
/// <summary>
/// Gets a property of a particular type. Throws if type does not match.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T GetProperty<T>(string key) where T: IPropertyType
{
if (Properties[key] is T type)
{
return type;
}
throw new ArgumentException($"{key} is of type {Properties[key].GetType()}, not {typeof(T)}");
}
public PropertySet(string xml)
{
var parsed = XElement.Parse(xml);
if (parsed.Name.LocalName != "cGZPropertySetString")
{
throw new ArgumentException("cGZPropertySetString not in xml");
}
foreach (var property in parsed.Elements())
{
var key = property.Attribute("key")?.Value;
if (key == null)
throw new ArgumentException("Property with no key attribute: " + property);
var innerText = string.Concat(property.Nodes());
IPropertyType value = property.Name.LocalName switch
{
"AnyString" => new StringProp { Value = innerText },
"AnyUint32" => new Uint32Prop { Value = uint.Parse(innerText) },
_ => null
};
Properties[key] = value;
}
}
}
public interface IPropertyType
{
}
public struct StringProp : IPropertyType
{
public string Value;
}
public struct Uint32Prop : IPropertyType
{
public uint Value;
}
}

View file

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e330210470394dc19ec5972e68b97ed3
timeCreated: 1689816018

View file

@ -0,0 +1,25 @@
using System.Linq;
using NUnit.Framework;
using OpenTS2.Content;
using OpenTS2.Content.DBPF;
using OpenTS2.Files.Formats.DBPF;
public class NeighborhoodObjectCodecTest
{
[SetUp]
public void SetUp()
{
TestMain.Initialize();
ContentProvider.Get().AddPackage("TestAssets/Codecs/NeighborhoodDecorations.package");
}
[Test]
public void TestSuccessfullyLoadsNeighborhoodObject()
{
var objectAsset = ContentProvider.Get()
.GetAssetsOfType<NeighborhoodObjectAsset>(TypeIDs.NHOOD_OBJECT).Single();
Assert.That(objectAsset.ModelName, Is.EqualTo("ufoCrash_cres"));
Assert.That(objectAsset.Guid, Is.EqualTo(0x16));
}
}

View file

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d86756d05ef7430e842b66f542ba6195
timeCreated: 1689820647

View file

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 62012cc9c4b840519ad97f4121e0d761
timeCreated: 1689817013

View file

@ -0,0 +1,58 @@
using System;
using NUnit.Framework;
using OpenTS2.Files.Formats.XML;
public class PropertySetTest
{
// Note: the double double-quotes in these tests are because we use the C#
// @"string" feature.
[Test]
public void ParsesPropertySetSuccessfully()
{
const string xml = @"<?xml version=""1.0"" encoding=""UTF-8""?>
<cGZPropertySetString>
<AnyUint32 key=""key-with-int"" type=""0xeb61e4f7"">42</AnyUint32>
<AnyString key=""key-with-string"" type=""0x0b8bea18"">hello world</AnyString>
</cGZPropertySetString>";
var set = new PropertySet(xml);
Assert.That(set.Properties, Contains.Key("key-with-int"));
Assert.That(set.Properties, Contains.Key("key-with-string"));
Assert.That(set.GetProperty<StringProp>("key-with-string").Value, Is.EqualTo("hello world"));
Assert.That(set.GetProperty<Uint32Prop>("key-with-int").Value, Is.EqualTo(42));
}
[Test]
public void GetPropertyThrowsWhenTypeDoesNotMatch()
{
const string xml = @"
<cGZPropertySetString>
<AnyUint32 key=""key-with-int"" type=""0xeb61e4f7"">42</AnyUint32>
</cGZPropertySetString>";
var set = new PropertySet(xml);
Assert.That(set.Properties, Contains.Key("key-with-int"));
Assert.Throws<ArgumentException>(() => set.GetProperty<StringProp>("key-with-int"));
}
[Test]
public void ParserFailsWhenXmlDoesNotHaveCorrectElement()
{
const string xml = @"<?xml version=""1.0"" encoding=""UTF-8""?>
<oops>
</oops>";
Assert.Throws<ArgumentException>(() => new PropertySet(xml));
}
[Test]
public void ParseFailsWhenPropertyDoesNotHaveKey()
{
const string xml = @"<?xml version=""1.0"" encoding=""UTF-8""?>
<cGZPropertySetString>
<AnyUint32 type=""0xeb61e4f7"">42</AnyUint32>
</cGZPropertySetString>";
Assert.Throws<ArgumentException>(() => new PropertySet(xml));
}
}

View file

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c3226a0fd5ee4fa7a7a24d5d027b8ab7
timeCreated: 1689817764