Actions

SCHG

Sonic Forces

From Sonic Retro

SCHG: Sonic Forces
Main Article


This is the Sonic Community Hacking Guide for the PC version of Sonic Forces.

Sonic Forces you to use this stuff to fix his game.

Filesystem

The files/directories listed below are located in the game's install directory (generally C:\Program Files (x86)\Steam\steamapps\common\SonicForces).

  • _CommonRedist - Installers for DirectX and Visual C++ 2013. Used by Steam when installing the game.
  • build\main\projects\exec - Game executable and Dynamic-Link-Libraries (DLLs).
  • image\x64\disk - Packed CPK archives which contain most of the game's data, including DLC downloaded from Steam.
  • savedata - Player save data, as well as graphics configuration settings.

Tools

Hedge Mod Manager

Hedge Mod Manager is a mod/code loader and manager for various Hedgehog Engine-based games, most noticeably Sonic Forces. It was written in C# by Radfordhound and thesupersonic16, and is fully open-source/under the MIT License. You can download the latest version here.

SFPac

SFPac is an open-source Python script written by Skyth which can be used to extract/repack archives in the Forces variation of the .PAC format. You can download the latest version here.

Simply drop .PAC files onto it to extract them, or drop a folder onto it to repack.

ModelConverter

ModelConverter is a command-line tool written by Skyth in C++ that allows you to convert FBX/DAE files to .model and .material files in the Forces variation of the format (I.E. with the new header). You can download it here.

Simply drop an FBX or DAE file onto it to convert it to Forces .model and .material formats.

Additionally however, you can append "tags" to the end of your model/mesh/material names before exporting to FBX/DAE to set various options.

  • @LYR(layer) Specifies what layer the given material will operate on.
  • @NAME(meshName) Specifies the name of the given mesh. Used for mouth-swapping, for example.
  • @PRP(propertyName, value) Specifies a property that will be added to the header of the given mesh.

Properties are a component of the new header used in Lost World and Forces. Some examples of properties include:

  • ShadowCa Specifies whether or not the given mesh should cast a shadow.
  • ShadowRe Specifies whether or not the given mesh should receive a shadow.

For example, if you wish to make a mesh called "MyMesh" cast a shadow, you'd simply append "@PRP(ShadowCa, true)" to the end of its name, so that the mesh's name is "[email protected](ShadowCa, true)" upon exporting the FBX/DAE file.

HedgeEdit

HedgeEdit is an open-source level editor similar to SonicGlvl, but with support for various 3D Sonic games in the series, rather than being primarily focused on Sonic Generations. It was written in C# by Radfordhound using HedgeLib, and is under the MIT License. You can download the latest version here.

File/Format specifications

BINA

NOTE: This section is highly based on my page on Sonic Colors formats: SCHG:Sonic_Colors

The BINA format is a generic container format used by Sonic Team in titles such as Sonic the Hedgehog (2006 game), Sonic and the Secret Rings, Sonic and the Black Knight, Sonic Colors, Sonic Lost World, and now in Sonic Forces.

Generally speaking, any file that follows the BINA Format is structured as follows:

  • BINA Header
  • Data (Entirely based on the type of data the file contains)
  • BINA String Table
  • BINA Offset Table (aka BINA Final Table or BINA Footer)

Each of the BINA-specific components are detailed below:

Header

struct Header
{
    char[4] BINASignature = "BINA";
    char[3] VersionNumber = "210";
    char EndianFlag; // "B" if the file is big-endian, "L" if the file is little-endian.
    uint FileSize;
    ushort Unknown1;
    ushort Unknown2;

    char[4] DATASignature = "DATA";
    uint DataLength = (FileSize - 16); // How long the file is from the beginning of DATASignature until the end.
    uint StringTableOffset; // The non-absolute offset to the BINA String Table explained below.
    uint StringTableLength; // The length of the BINA String Table explained below.

    uint OffsetTableLength; // The length of the BINA Offset Table explained below.
    ushort AdditionalDataLength = 0x18;
    ushort Padding = 0; // Just two nulls to pad-out AdditionalDataLength to 4 bytes.
    byte[AdditionalDataLength] AdditionalData; // Seems to always just contain nulls
}

String Table

Literally just an array of null-terminated strings (which are referenced via offsets by other values in the file). The only other thing noteworthy about them is that the last value in the array must be padded to a 0x4 offset.

Offset Table

This is the most complicated part of the BINA format by far, so brace yourselves!!
The BINA Offset Table, in concept, is actually very simple. It's literally just an array that lists the position of every offset in the file (not counting the offsets in the header, such as the offset to the string table).

However, in execution, BINA offset tables are quite complicated, as they use a few clever techniques to ensure the offset tables are as small as possible.

As an example, let's use some values (in hex) from an actual BINA offset table, namely the first 4 bytes in w5a01_obj_area01.gedit's offset table:

44 48 42 42


The first value in this table (44 in hex) is represented as the following in binary:

0100 0100


So basically, here's how this works. The first two bits represent how long this offset is, corresponding to one of the values in the table below:

00 = This offset is 0 bits long, you've reached the end of the offset table!
01 = This offset is 6 bits long.
10 = This offset is 14 bits long.
11 = This offset is 30 bits long.


In this case, since the first two bits are 01, the offset is 6 bits long, so we read the next 6 bits (00 0100) to get the value for the offset. Simple, right?

Now here's where things get a little tricky.

You see, these values aren't simply the positions of each offset. Rather, these values represent the distance between this offset and the last offset we've currently read (or the length of the BINA Header, aka 64 bytes, if we haven't actually read an offset yet).

Beyond that, due to how binary works, the last two bits in a value can only possibly be used to represent a value from 0-3, which wouldn't be very helpful here, as each offset, at a minimum, must be 4 bytes long (in Forces they're actually typically 8 bytes long but shh let's save that for later)!

So, to better take advantage of space, all of the values have actually been bit-shifted by two to the right (01 0100 is stored in the file like 00 0101, for example). Therefore, you also need to bit-shift these values back to the left by 2 to counter-act this.

Example

Phew, alright, we covered a lot just then! Still with us? Good!
Let's go over how you'd read each offset in the above example table.

The first value in our example offset table is 44 in hex and 0100 0100 in binary. Since the first two bits are 01, it means we need to read the next 6 bits (00 0100) to get our value. But remember, these values are actually all shifted by 2 to the right, so we need to shift it to the left by 2 to get the actual value: 0001 0000 (or 16).

Now we just add that to the position of the last offset we read, except in this case we haven't actually read an offset yet, so instead we use the length of the BINA header (again, 64 bytes). 64 + 16 = 80. So, the position of our first offset is 80! Now we just have to repeat this process for each offset in the table.

The next offset in this table is 0x48 (0100 1000). The first two bits are 01, so we use the next 6 bits (00 1000) as the value for the offset. Left-shift it by two as described above, and we get 0010 0000 (32). Add that to the position of the last offset we read (80), and presto, we get the position of our second offset (112)!

I think you get it now, so I'll stop there. Hopefully the BINA Offset Table now makes sense!

GEdit

The GEdit format is used for defining "set data" (stage object layouts). You can find each stage's gedit files in "wars_patch.cpk" (for some reason not in wars_0 or wars_1), inside the PAC file that has the stage's ID in its name under the "gedit" folder.

The GEdit format is a BINA format, therefore it contains a BINA Header, String Table, and Offset Table as described in the above section on the BINA format. The format-specific "Data" portion of the GEdit format works as follows:

struct Header
{
    ulong Padding1 = 0;
    ulong Padding2 = 0;
    
    ulong ObjectOffsetTableOffset;
    ulong ObjectCount;

    ulong ObjectCount2 = ObjectCount; // Not sure why the object count is in here twice? Lol
    ulong Padding3 = 0;

    ulong[ObjectCount] ObjectOffsetTable; // An array of offsets to each object entry
    ObjectEntry[ObjectCount] Objects;
}

class ObjectEntry
{
    ulong Padding1 = 0;
    ulong ObjectTypeOffset; // Points to the object's type in the BINA String Table.

    ulong ObjectNameOffset; // Points to the object's name in the BINA String Table.
    ushort ObjectID;
    ushort Unknown1;
    ushort ParentObjectID; // Set to 0 if the object has no parent
    ushort ParentUnknown1; // Set to 0 if the object has no parent

    float[3] Position;
    float[3] Rotation; // In radians, not degrees.

    // These seem to have no effect in-game, so they might just be
    // values used by the dev team's level editor.
    float[3] Position2;
    float[3] Rotation2; // Also in radians
    
    // See the following section for more information on tags
    ulong TagsOffsetTableOffset;
    ulong TagCount; // ?
    ulong UnknownCount1 = TagCount; // Seems to always be the same as TagCount?
    ulong Padding3 = 0;

    // Parameters are hard-coded for each object type. Since there are too
    // many object-specific parameters to list on one page, see our Forces
    // object templates on the HedgeLib repository for type-specific parameter
    // specificatons. (https://goo.gl/e1tYXT)
    ulong ParametersOffset;
    
    ulong[TagCount] TagsOffsetTable; // An array of offsets to each tag
    Tag[TagCount] Tags;
}

Tags

Tags are a special type of data that can be assigned to an object.

They act very similarly to the object's parameters, except tags actually have types that are included in the set data, as well as a pre-defined structure that can be used to read them even if their type is not known.

class Tag
{
    ulong Padding1 = 0;
    ulong TypeOffset; // Points to the tag's type in the BINA String Table
    ulong DataLength;
    ulong DataOffset;

    // The actual data in this array has to be parsed differently based on the tag's type.
    // See the following section for currently-known tags.
    byte[DataLength] Data;
}


While the data itself can be read due to this structure even if the type of tag being read is unknown, actually parsing it unfortunately still requires type-specific code or templates. Here are the specifications for every currently-known type:

RangeSpawning

class RangeSpawning
{
    float RangeIn; // How close the player needs to get to the object before the object will spawn
    float RangeOut; // How far the player needs to move away from the object once it has spawned before the object will de-spawn.
}
Sonic Community Hacking Guide
General
Sonic Music Hacking Guide | SonED2 Manual
Game-Specific
Sonic the Hedgehog (16-bit) | Sonic the Hedgehog (8-bit) | Sonic Eraser | Sonic CD (prototype 510) | Sonic CD | Sonic CD (PC) | Sonic CD (2011) | Sonic 2 (Nick Arcade prototype) | Sonic 2 (Simon Wai prototype) | Sonic 2 (16-bit) | Sonic 2 (8-bit) | Sonic 2 & Knuckles | Dr. Robotnik's Mean Bean Machine | Sonic Triple Trouble | Sonic 3 | Sonic & Knuckles | Sonic 3 & Knuckles | Sonic & Knuckles Collection | Sonic Crackers | Sonic 3D: Flickies' Island | Chaotix | Sonic R PC | Sonic Jam 6 | Sonic Advance | Sonic Advance 2 | Sonic Advance 3 | Sonic Battle | Sonic Adventure | Sonic Adventure DX: Director's Cut | Sonic Adventure DX: PC | Sonic Adventure (2010) | Sonic Adventure 2 | Sonic Adventure 2: Battle | Sonic Adventure 2 (PC) | Sonic Heroes | Shadow the Hedgehog | Sonic Riders | Sonic the Hedgehog (2006) | Sonic & Sega All-Stars Racing | Sonic Unleashed (Xbox 360/PS3) | Sonic Colours | Sonic Generations | Sonic Lost World | Sonic Forces
Legacy Guides
The Nemesis Hacking Guides The Esrael Hacking Guides
ROM: Sonic 1 | Sonic 2 | Sonic 2 Beta | Sonic 3

Savestate: Sonic 1 | Sonic 2 Beta/Final | Sonic 3

Sonic 1 (English / Portuguese) | Sonic 2 Beta (English / Portuguese) | Sonic 2 and Knuckles (English / Portuguese)
Move to Sega Retro
Number Systems (or scrap) | Assembly Hacking Guide | Subroutine Equivalency List | 68000 Instruction Set | 68000 ASM-to-Hex Code Reference | VDP Documentation | Official Sega 32X Documentation | Official Mega-CD Documentation | Official Sega Mega Drive Documentation