HSON Format¶
The Hedgehog Set Object Notation Format ("HSON") is a custom JSON-based file format designed collaboratively by Darío, Radfordhound, ĐeäTh, Skyth, and Sajid to represent object placement in a "universal" (non-game/editor-specific) way.
It was designed with the following principles in-mind:
- Be universal. Specifically, it must be able to represent object placement data from at least any mainline 3D Sonic game in a non-game/editor-specific way.
- Be simple. Specifically, it must not be difficult to parse and to generate.
- Be flexible. Specifically, it must be able to be adapted to do anything future games and tools may require.
JSON Schema¶
The official JSON Schema for the HSON Format can be found at the hson-schema GitHub repository.
It's useful (but not necessary) to validate .hson files against this schema in editors which support this feature, as doing so allows you to have autocompletion, descriptions of each property, and data validation.
To validate all .hson files against the schema in your editor of choice, refer to the instructions in the README from the above repository.
To manually validate against the schema on a per-file basis, include the following line at the top-level of your HSON file(s), before the HSON file format version parameter.
If writing tooling that generates HSON, consider having your tooling write this line to generated files.
Implementations¶
As of the time of writing, the following known implementations exist:
C#¶
- libHSON: The official C# implementation of HSON. Allows for simple, efficient, feature-complete, two-way serialization of HSON data.
C++¶
- HedgeLib: Radfordhound's open-source library and collection of tools that aims to makes modding games in the Sonic the Hedgehog franchise easier. It contains a feature-complete two-way HSON implementation, which is used in its level-editing tooling.
Example¶
The following is an example file in the HSON Format, which represents a "project" called "Sample Project" that contains 5 objects.
This example file will be referenced periodically throughout the format specification.
{
"$schema": "https://raw.githubusercontent.com/hedge-dev/hson-schema/main/hson.schema.json",
"version": 1,
"metadata": {
"name": "Sample Project",
"author": "Takashi Iizuka, Morio Kishimoto",
"date": "2023-02-09T22:38:42Z",
"version": "1.0.0",
"description": "green hill is looking a lot more like sand hill rn",
"myCustomEditor": {
"viewportTabIndex": 0
}
},
"objects": [
{
"id": "{fd3e6bc9-5d2d-4da8-a22d-f88e709b3e48}",
"name": "Spring #1",
"type": "Spring",
"position": [ 100.0, 0.0, 0.0 ],
"rotation": [ 0.0, 0.0, 0.0, 1.0 ],
"scale": [ 1.0, 1.0, 1.0 ],
"isEditorVisible": true,
"isExcluded": false,
"parameters": {
"tags": {
"RangeSpawning": {
"rangeIn": 100.0,
"rangeOut": 20.0
}
},
"visual": "Normal",
"firstSpeed": 420,
"outOfControl": 2.5,
"isHorming": true,
"actions": [
{
"action": "On",
"objectIds": [
"{2737e92f-4842-46cb-a590-e074f7b882f0}",
"{38285a58-9969-4c5f-a649-b91440962a71}"
],
"delayTime": 0.5
},
{
"action": "Off",
"objectIds": [],
"delayTime": 0.0
},
{
"action": "Off",
"objectIds": [],
"delayTime": 0.0
}
],
}
},
{
"type": "Ring"
},
{
"id": "{2737e92f-4842-46cb-a590-e074f7b882f0}",
"parentId": "{fd3e6bc9-5d2d-4da8-a22d-f88e709b3e48}",
"type": "Ring",
"position": [ 0.0, 1.0, 0.0 ],
"parameters": {
"visibility": "Visible",
"respawnTime": 0.0
},
"myCustomValue": 893.5,
"myCustomEditor": {
"presetPlacementType": "LINE",
"presetPlacementDistance": 1.0
}
},
{
"id": "{38285a58-9969-4c5f-a649-b91440962a71}",
"instanceOf": "{2737e92f-4842-46cb-a590-e074f7b882f0}",
"position": [ 0.0, 2.0, 0.0 ],
"parameters": {
"respawnTime": 0.5
}
},
{
"id": "{81fdcaff-aa37-4c47-a665-5b6265a6b780}",
"parentId": "{00000000-0000-0000-0000-000000000000}",
"type": "DashPanel"
}
]
}
Format Specification¶
What follows is a specification of all standard properties supported by the HSON Format.
Important
All of the following properties are OPTIONAL, unless otherwise specified.
1. version¶
Info
- Type:
number
- Minimum: 1
This value is REQUIRED to be present in all HSON files.
The version of the HSON format being used by this file. It is represented as a number that is restricted in the following ways:
- It must have a zero fractional part (e.g.
1.5
would not be allowed, but1.0
or1
are). - It must be greater than or equal to 1.
Because of these restrictions, it is valid for tooling to parse this value into an unsigned int
.
Currently, version 1 is the latest version.
2. metadata¶
Info
- Type:
object
Metadata for the project represented by this file.
It is intended to be used purely for tooling display purposes.
metadata is represented as a JSON object
consisting of the following properties:
2.1. name¶
Info
- Type:
string
The name of the project represented by this file.
2.2. author¶
Info
- Type:
string
The author(s) of the project represented by this file.
If the project has multiple authors, we recommend writing each author's name with a comma and space separating them, like so:
With that said, this is a recommendation and by no means a requirement.
Tooling should treat this as an arbitrary string in no particular format.
2.3. date¶
Info
- Type:
string
The date/time the project was created at, represented as a string in the RFC 3339 format (a similar standard to ISO 8601).
It is invalid to represent the date using any other format.
This limitation makes parsing dates in tooling a lot easier.
Getting the current DateTime in RFC 3339 format programmatically
Feel free to copy-paste any of these code snippets and do what you want with them.
C#:
public static string GetRFC3339Time()
{
return DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssK",
CultureInfo.InvariantCulture);
}
C++11:
#include <string>
#include <stdexcept>
#include <ctime>
std::string getRFC3339Time()
{
char buf[32]; // This is a few extra characters than should be necessary, just to be safe.
const auto time = std::time(nullptr);
const auto len = std::strftime(buf, sizeof(buf), "%FT%TZ", std::gmtime(&time));
if (!len)
{
throw std::runtime_error("Failed to get RFC 3339 format time");
}
return std::string(buf, len);
}
C:
2.4. version¶
Info
- Type:
string
The version number of the project, represented as a string.
How you want to version your project is up to you; these are all valid:
Tooling should treat this as an arbitrary string in no particular format.
2.5. description¶
Info
- Type:
string
The description of the project represented by this file.
3. objects¶
Info
- Type:
array
An array containing all of the objects contained within the project.
Each object is represented as a JSON object
consisting of the following properties:
3.1. id¶
Info
- Type:
string
The UUID (aka GUID) of the object, represented as a case-insensitive string
formatted like this: {11111111-2222-3333-4444-555555555555}
.
Note that it is enclosed in curly brackets; this is to make it clear that it is supposed to be parsed as a UUID, and NOT as a number.
The object's UUID is used to reference the object throughout the project.
It must be unique throughout the project; that is, no two objects within the same project are allowed to have the same UUID. That's what makes them UUIDs.
It also cannot be set to the following value: {00000000-0000-0000-0000-000000000000}
,
as this value is reserved for "null" object references.
This property can be omitted (leaving the object without a specified UUID) if the object never needs to be referenced. This helps to save some space.
Tools are free to generate their own UUIDs for objects without a specified UUID, or to just think of the object as not having a UUID. Whichever is more convenient.
Examples
Valid UUID (lowercase):
Valid UUID (uppercase):
Invalid UUID (no enclosing curly brackets):
Invalid UUID (not a UUID):
Invalid UUID (the special "null" UUID value is not valid to use as the object's id):
Valid object references (the special "null" UUID value is valid to use in object references):
3.2. name¶
Info
- Type:
string
The name of the object. This is an arbitrary string that can be anything you want.
It is intended to be used purely for tooling display purposes.
3.3. parentId¶
Info
- Type:
string
The id of this object's parent object, or a null UUID.
If specified as a valid, non-null UUID, this object is a child of the object with the
given UUID, meaning that the object's transform (represented via position
,
rotation
, and scale
) is relative to the parent's transform
(which is relative to its parent's transform, and so on).
Otherwise, if it is not specified, or is specified as a null UUID, this object has no parent object, and the transform is global.
Parenting Example
{
"id": "{2737e92f-4842-46cb-a590-e074f7b882f0}",
"parentId": "{fd3e6bc9-5d2d-4da8-a22d-f88e709b3e48}",
"type": "Ring",
"position": [ 0.0, 1.0, 0.0 ],
"parameters": {
"visibility": "Visible",
"respawnTime": 0.0
},
"myCustomValue": 893.5,
"myCustomEditor": {
"presetPlacementType": "LINE",
"presetPlacementDistance": 1.0
}
}
In this snippet from the example file, the object has specified
a position of [ 0.0, 1.0, 0.0 ]
, and a parentId of {fd3e6bc9-5d2d-4da8-a22d-f88e709b3e48}
,
which means that its transform is local to the transform of the
object which uses that UUID (not included in this snippet).
This parent object has a position of [ 100.0, 0.0, 0.0 ]
.
So, the above object's global position is [ 100.0, 1.0, 0.0 ]
.
Null Reference Example
{
"id": "{81fdcaff-aa37-4c47-a665-5b6265a6b780}",
"parentId": "{00000000-0000-0000-0000-000000000000}",
"type": "DashPanel"
}
In this snippet from the example file, the object has specified
a parentId of {00000000-0000-0000-0000-000000000000}
(the special UUID
null value), which is equivalent to not specifying a parentId. This means
that this object has no parent.
3.4. instanceOf¶
Info
- Type:
string
The id of the object to be instanced, or a null UUID.
If specified as a valid, non-null UUID, this object is an instance of the object with the
given UUID, meaning that it will inherit ALL unspecified properties from the
instanced object, with the notable exception of the id
property, instead
of falling back to the usual defaults.
Note that it will NOT affect the object's specified properties; these will effectively "override" any properties taken from the instanced object.
Otherwise, if it is not specified, or is specified as a null UUID, this object is not an instance.
Example
{
"id": "{2737e92f-4842-46cb-a590-e074f7b882f0}",
"parentId": "{fd3e6bc9-5d2d-4da8-a22d-f88e709b3e48}",
"type": "Ring",
"position": [ 0.0, 1.0, 0.0 ],
"parameters": {
"visibility": "Visible",
"respawnTime": 0.0
},
"myCustomValue": 893.5,
"myCustomEditor": {
"presetPlacementType": "LINE",
"presetPlacementDistance": 1.0
}
},
{
"id": "{38285a58-9969-4c5f-a649-b91440962a71}",
"instanceOf": "{2737e92f-4842-46cb-a590-e074f7b882f0}",
"position": [ 0.0, 2.0, 0.0 ],
"parameters": {
"respawnTime": 0.5
}
}
In this snippet from the example file, the second listed object (UUID:
38285a58-9969-4c5f-a649-b91440962a71
) is an instance of the first listed object
(UUID: 2737e92f-4842-46cb-a590-e074f7b882f0
).
As such, it will inherit all of the unspecified properties from the first object, including
the parentId
, type
, myCustomValue
, and myCustomEditor
properties, as well as the visibility
parameter.
It will NOT, however, inherit the specified properties, including the id
,
instanceOf
, and position
properties, as well as the
respawnTime
parameter.
This means that the second object in the above snippet will be equivalent to the following object:
{
"id": "{38285a58-9969-4c5f-a649-b91440962a71}",
"parentId": "{fd3e6bc9-5d2d-4da8-a22d-f88e709b3e48}",
"type": "Ring",
"instanceOf": "{2737e92f-4842-46cb-a590-e074f7b882f0}",
"position": [ 0.0, 2.0, 0.0 ],
"parameters": {
"visibility": "Visible",
"respawnTime": 0.5
},
"myCustomValue": 893.5,
"myCustomEditor": {
"presetPlacementType": "LINE",
"presetPlacementDistance": 1.0
}
}
As you can see, this is a simple, yet powerful system that allows for many possibilities.
Note
UUIDs have a special exception; they never get inherited from the instanced object.
For example:
{
"id": "{AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA}",
"type": "Ring"
},
{
"instanceOf": "{AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA}"
}
The second object listed in the above example will inherit all
properties from the first object, except for the id
.
This is due to the fact that every valid object must have its own unique UUID.
3.5. type¶
Info
- Type:
string
This value is REQUIRED to be present, unless this object is an instance of another object, in which case, it is optional, as the type will just be taken from the instanced object.
The type of the object (e.g. "Spring", "Ring", etc.).
Note that this value is not game-specific and can be set to anything, with one exception: it is not valid for type to be an empty string.
3.6. position¶
Info
- Type:
array
- Default:
[ 0.0, 0.0, 0.0 ]
The position of the object within 3D space. Represented as a three-dimensional array
of number
s, representing a vector3 using Y-Up right-handed coordinates (X-right, Y-up, and Z-backwards),
and meters as its units.
If this object is a child of another object, this position value is local to the transform of the parent object (which is local to the transform of its parent, and so on).
Otherwise, this position value is global.
If this property is not specified, the value [ 0.0, 0.0, 0.0 ]
will be used as a fallback.
3.7. rotation¶
Info
- Type:
array
- Default:
[ 0.0, 0.0, 0.0, 1.0 ]
The rotation of the object within 3D space. Represented as a four-dimensional array
of number
s, representing a quaternion using Y-up right-handed coordinates.
If this object is a child of another object, this rotation value is local to the transform of the parent object (which is local to the transform of its parent, and so on).
Otherwise, this rotation value is global.
If this property is not specified, the value [ 0.0, 0.0, 0.0, 1.0 ]
will be used as a fallback.
Note
Rotation values must be represented as a four-dimensional array representing a quaternion.
It is not valid to represent rotation values using other methods, such as by using three-dimensional arrays representing euler angles or binary angle measurement (BAMS).
When dealing with formats that utilize these (or other) methods, tooling must convert to/from quaternions as necessary.
3.8. scale¶
Info
- Type:
array
- Default:
[ 1.0, 1.0, 1.0 ]
The scale of the object within 3D space. Represented as a three-dimensional array
of number
s, representing a vector3 using Y-up right-handed coordinates (X-right, Y-up, and Z-backwards),
and meters as its units.
If this object is a child of another object, this scale value is local to the transform of the parent object (which is local to the transform of its parent, and so on).
Otherwise, this scale value is global.
If this property is not specified, the value [ 1.0, 1.0, 1.0 ]
will be used as a fallback.
3.9. isEditorVisible¶
Info
- Type:
boolean
- Default:
true
Whether the object should be visible in the editor's 3D display (as applicable). Note that this is purely for editors, and has no effect on whether the object is visible in-game.
When converting from HSON to game-specific file(s), tooling should ignore this value.
If this property is not specified, the value true
will be used as a fallback.
3.10. isExcluded¶
Info
- Type:
boolean
- Default:
false
Whether the object should be excluded from game data.
If specified as true
, editors should (as applicable) hide the object from 3D display,
but still show it in the object hierarchy as a disabled object, and provide a mechanism
for users to un-exclude it.
When converting from HSON to a game format, tooling should treat excluded objects as if they do not exist, by simply not writing them to the resulting game-specific file(s).
If this property is not specified, the value false
will be used as a fallback.
3.11. parameters¶
Info
- Type:
object
All named parameters specific to this object type and/or game. For example, the firstSpeed
parameter for objects of the Spring
type in Sonic Frontiers, which specifies the speed
the object will launch the player off with.
This is the place to put all parameters that are specific to objects of the specified type
.
It's also a great place to put game-specific parameters, such as object visibility ranges.
Any number of properties of any JSON type are allowed here, all of which are always entirely optional.
Tooling should treat this as an arbitrary list of key-value pairs, and provide some mechanism that allows the user to modify ALL of them as they please. It does not, however, have to provide a mechanism to add, remove, or edit the type of these parameters, as this may be undesirable depending on your use-case.
Tooling should not require any of these parameters to be present. When converting, tooling should, instead, lookup each parameter by its name as needed, and either error-out or fallback to a default value if a required parameter is not present.
Important
Note that the following are not allowed to be used as parameter names:
- Empty strings.
- Strings which contain forward slashes.
Info
The "no forward slash" rule allows tooling to access parameters by "path", like so:
- "
tags/RangeSpawning/rangeIn
":100.0
- "
tags/RangeSpawning/rangeIn
":20.0
Custom Properties¶
In addition to all of the standard properties listed in the above specification, it is also
completely valid to have your own custom properties which are not part of the specification,
as demonstrated with the myCustomEditor
and myCustomValue
properties shown
in the example file.
If you use custom properties, it's highly recommended to place them in a property named after
your tooling, as demonstrated with the myCustomEditor
properties in the example.
Doing this helps to reduce potential name collisions with custom properties from other tools, which might end up sharing the same name(s).
If your custom property is intended to be used across multiple tools (e.g. an unofficial
"extension" to the HSON format), then it's recommended to ignore the above advice and just
use the custom property directly instead, as demonstrated with the myCustomValue
property in the example.
Important
Note that the following are not allowed to be used as custom property names:
- Names which directly collide with properties that are part of the HSON specification (e.g. you can have a custom property called "position", but it has to be placed such that it does not collide with HSON's position property).
- Empty strings.
- Strings which contain forward slashes.