Explore FH2Mods
Developer space

Documentation / 05

Game internals

Technical notes on the Azimuth game build.

By Trinculo54 · MIT license · Snapshot a5195aa · 27 Oct 2026

On this page

Notes on the game

What's been worked out about Forza Horizon 2 on PC while building the mods so far. Addresses are offsets from the game's base address (RVAs), since wait_for_game() gives you the base. Everything here was found in the one build everyone plays; treat it as a starting point and check before you patch.

The build and Azimuth

Everyone plays the Xbox One release forza_durango_release_final.exe, built on 24 November 2015. Its PE header has timestamp 0x565501ED and image size 0x03A18000, which is how the platform recognises it in memory. Azimuth doesn't start it as a process. It maps the exe into azimuth.exe itself, at the preferred base 0x140000000, when the player presses Play, and translates the Xbox APIs underneath it. So your mod, the game and Azimuth all share one process.

Azimuth's settings are in data\azimuth.ini, a UTF-16 file. The [launcher] section holds the player's name (player_name) and graphics settings, and [title-1a96545b] folder= names the game folder (usually FH2). That folder's Mount subfolder is what the game sees as game:\. 1A96545B is FH2's title ID, and Fast & Furious is 5A9771A4. Saves are under data\titles\1A96545B\saves\player-1\User_...\revisions\<n>\data\. Azimuth checks those files' hashes, and Extra Options can turn that check off, but mods should never need to touch saves.

Files and Extra Options

The game reads its data from game:\media. Most of it is in zips, whose directories are listed in media\zipmanifest.xml; the game relies on those offsets, so don't edit the zips. Extra Options adds an override folder: a file at media\X.zip\path\file can be replaced by mediaoverride\X\path\file, and a loose file at media\path\file by mediaoverride\path\file. The priority is mediaoverride, then mediapc, then the original. Extra Options loads through winmm.dll, and its settings are in FH2_ExtraOptions.ini next to azimuth.exe. Gamerpic.png in the same folder is the player's avatar when CustomGamerpic=1.

The override folder

The game finds every file through its file system's resolver, which a vtable slot at game+0x2E6EF28 points to (game+0x11F49B0). The file system is a list of volumes, each mounted at a path with a priority; the game's own zips and folders are volumes under game:\media\. The game also has a folder volume class (constructor game+0x11E05A0, vtable game+0x2EAB918, its RTTI locator just before it) that serves the files in one folder. Mounting two of those, for game:\mediaoverride\ and game:\mediapc\, over game:\media\ at priorities above everything else (0x7FFFFFFE and 0x7FFFFFFD) is what makes mediaoverride work: the file system's vtable +0x1E8 mounts a volume (no hash-manifest check when its last three arguments are 0) and +0x1F8 sorts the volumes again. The folder volume's "does this file exist" method (+0xE8) is handed archive selectors such as <*>0 too, which aren't file names, so they have to be answered "no".

Extra Options does this from its winmm.dll. Azimuth 1.0.1 runs without Extra Options for now, so from 2.7.3 the platform does the same when Extra Options' winmm.dll isn't loaded: it swaps the resolver pointer as soon as Azimuth maps the game (before the game's code is executable) and mounts both folders on the first lookup. platform.log says so ("media overrides: ..."). It checks the game's bytes against the exe on disk first and leaves everything alone if they differ. When Extra Options is loaded the platform doesn't touch the resolver: Extra Options refuses all of its patches if any place it patches was changed first, and its EnableMediaOverrides setting decides. Azimuth 1.0.1's log lists the folder under "modifications" (the game folder's Mount\mediaoverride (loose files)); that's only a note of what's in the game folder, not Azimuth reading it.

Extra Options also patches game code: around 84 byte patches and 47 hooks, listed with their RVAs in patches.json in its repo. It applies them all at once, while Azimuth loads its TLS helper just after mapping the game, and only after checking that every one of those places looks as it expects. If any doesn't, it applies none of them. That's why the platform's hooks wait for wait_for_code, and refuse game code that no longer matches the exe on disk.

Text

See string-tables.md. The title screen's "PRESS (A) TO START" is two strings in Main.str: 0xB197 (PRESS) and 0x18F0 (TO START). TO START is its own text box, centred vertically on the button, so the line can grow sideways only about as far as the screen edge.

The game database

media\Stripped\gamedbRC.slt is a plain SQLite database, about 6 MB, with around 290 tables. The game opens it as game:\media\Stripped\gamedbRC.slt through its own file system, the one mediaoverride is mounted over, and reads it at startup. Tables worth knowing: Data_Car (every car, with DisplayName, BaseCost, PerformanceIndex, 0-60 times and handling values), Data_Engine, Events, EventCars, CarMeets, AIPlayers and DriverModels. Text columns hold string-table references like _&2149394699 rather than text, so names and descriptions are changed through the string tables. The exe also mentions media\db\gamedb.slt, which isn't in the shipped game.

Drivatars

The built-in Drivatars are in media\DrivatarGlobalCache\DefaultCache.bin. The file starts with u32 4 (version) and u32 125 (count), followed by 125 records of 0x2658 bytes each. They use only 25 different gamertags, five records each. A record's gamertag is UTF-16 in its first 32 bytes (15 characters and a NUL), and the Drivatar's id, 1 to 25 and the same for all five copies, is the u32 at +0x2650. The game reads the file once at startup (code at 0xA2D0E0) into a manager object, with the record count at +0x4D58 and a std::vector of records at +0x4D78. WheresMyBuddy replaces it through mediaoverride.

A championship copies the Drivatars it picked into the save, as SaveStates::ChampionshipDrivatarsState. The Storm Island championships appear to use the same class (saved as IslandChampionshipDrivatarsState). Its vtable is at 0x2D44F20, and slot 2 is Serialize(archive) at 0xA68870. The record count is at +0x1C of the object and the std::vector of records at +0x20. Records saved there are byte-for-byte copies of DefaultCache.bin records, id included. WheresMyBuddy swaps that vtable slot to rename them after loading.

Car meets and some road-trip traffic aren't Drivatars. They're named from AIPlayers.str, the 25 gamertags plus "HRZN Ben", through the game database's AIPlayers table (SELECT Name, DriverModelId FROM AIPlayers WHERE Id=%d).

Radio

FMOD Studio 1.x is linked into the game, and its low-level API is exported from the exe by name, for example FMOD::System::createSound at 0x1997740, FMOD::Sound::getSubSound at 0x199F510 and FMOD::Sound::getUserData at 0x199FBE0. Look them up through the export table rather than hard-coding them. The radio is the RadioStreamFmod class (vtable 0x2E76220), which plays songs from FMOD sound banks. FH2 Universal Radio redirects its createSound call at 0x1018A13 and its getSubSound call at 0x10221F2 to hand the game a stream of its own. Song samples are one of four classes, with vtables MusicSample 0x2E74EF8, DJSample 0x2E76570, IdentSample 0x2E76590 and RadioSample 0x2E74ED8. Each has a pointer to its properties at +0x08: sound name at +0x00, event name +0x20, display name +0x40, artist +0x60, length in ms +0x8C, markers +0x90. The radio station list is in RadioInfo_EN.xml.

The front end is XAML pages in media\UI.zip (a plain zip), drawn by AVUI, a C++ take on WPF with Binding, styles, templates and converters. Pages are under Scenes/Anthem/, shared styles under Resources/Anthem/. Each page is a C++ class too, which binds the XAML's PART_ elements and supplies the properties its bindings ask for. Which page opens when is decided by the state machine in media\stateflow.zip (ui_screens.xml and friends). Pause > Settings is A_PauseOptions.xaml, class PauseOptionsMenu, a 5 x 2 grid of tiles. Extra Options adds rows of its own to A_HUDOptions.xaml and A_VideoOptions.xaml, recognising them by their x:Class.

What the platform's Mods page uses, all in the 24 November 2015 build:

  • Properties. Bindings look properties up by name with AVUI::Type::FindProperty(type, name) at 0x157FF60. It walks the list at type+0x40 (nodes {descriptor, next}), comparing the descriptor's char* name at +0x40 with the AVUI string's UTF-16 text (at +0x28), then tries the base type at type+0x80. A descriptor has its getter at +0x28, called (out, desc, object), which boxes the value into *out (ints with 0x4702D0, bools with 0x336BA0); its setter at +0x30, (desc, object, value), with an int value at value+0x28; and its value type at +0x48 (bool is 0x15C9550(0)). The HUD page's EnableDrivatarLiveries (0xD58660 returns it) is a typical On/Off one. A page tells its bindings a property changed with 0xCF9C60(page, desc). Float properties, which the game's Slider binds its Value to (the audio volumes, the controller dead zones), have the same layout with a float at value+0x28; 0xED6E90 returns one, and 0x3F5990 boxes a float. The slider's look is A_Style_Slider_Options in Resources/Anthem/Shared_OptionsScreens.xaml, whose number is a NumUnitConverter (FractionDigits, ShowUnit, Scale; ConverterParameter=fracpercentage shows 0..1 as 0..100). Extra Options' drift camera sliders on the HUD page are built the same way.

  • The Settings page. It's made once while the game boots and reused. 0xDC8A60 wires its tiles with 0xD9FC10(page, "PART_Button_X", &pmf), where pmf is {handler, 0} and a name that isn't on the page is skipped. Its controller grid is a fixed 5 x 2 (set at 0xDC873E): the navigation manager is at page+0x410, with the column count at +0x9C, and the tile grid is rebuilt from that each time the page is wired. A tile's handler (HUD's is 0xDC6C40) fires a state-machine event.

  • Opening a page. A page is made from a scene record, {id, name, L"Anthem\\A_X" (std::wstring at +0x28), factory at +0x48}. The HUD page's is built by 0xB3A3E0 (id 0x49). Opening it builds the record twice, once to look it up and once inside the factory (0xB0B990), and the factory's copy goes into the new page at +0x228; that's the one its XAML is loaded from. Changing its path makes the same page class load a different file, and new files in mediaoverride\UI\ are found like replaced ones.

  • Message boxes. 0xD09D00(page, &title, &message, items) shows A_MessageBox.xaml over page and returns it. Title and message are references to CLocStrings (vtable 0x2EB0E30, text from the virtual at +0x38). The items are a collection made by 0xC65230, holding items made by 0xC65AB0(&item, &id, &label) (id at +0x68) and added through the collection's vtable +0x40 (collection, index, item). The popup takes the collection's interface at +0x38. 0xDD8820(popup, 6) sets its help bar. 0xDD86F0(popup, 2, std::function<void()>, true) sets what happens when an item is chosen; the function is the MSVC layout ({space[0x18], impl*}) and taken by value. 0xE01A60(popup) is the chosen item.

  • The MODS tab's pages. Every page of the MODS tab, and every mod's page, is the HUD options page loading another file: the Marketplace tiles' Car Packs handler (0xDBD720) fires the car_packs event, which the platform's pause.xml sends to a node showing the HUD page, and the scene record is pointed at A_ModPage_<nn>. A page leaves only by back, to pause_menu.enterback. Installed mods holds every view at once (the list, each mod's settings, each mod's pages), switched with FH2Mod_View_<k> properties.

  • The Achievements tiles. PART_Button_Achievements (handler 0xDBD610, wired at 0xDC7E5D) and the Storm Island tab's PART_Button_Panama_Achievements (0xDBF150, which first checks the tile has the focus with 0xDC17D0) both call 0xCB47B0(page, &out, 0xFE), a UI command that opened the console's achievements app, at 0xDBD635 and 0xDBF185. A page on that tile hooks those two calls.

  • Tile text. The pause menu's icon tiles use A_ControlTemplate_IconTile (Shared_TiledMenus.xaml): the icon, the title, and a notification badge from local:BindingProxy.A (bound to the page's NotificationBadge... properties). A tile's Tag takes a binding too (the Marketplace tab's badge is one), so a copy of that template with one more FZTextBlock bound to the tile's Tag shows a line of its own; the pause menu's resources have A_Style_FZTextBlock_TileClubMemberCount, the club tile's orange count, before the menu items. Bindings on the pause menu page find the platform's properties through the same FindProperty hook as the options pages', from inside a template too (RelativeSource FindAncestor, AncestorType={x:Type Page}, as the game's own templates do). A Color property is typed and boxed as the game's TeamColor (its descriptor at 0x140C09BD0): type 0x1415C8D80(0), value boxed by 0x140BEA4E0(out, const float rgba[4]), red, green, blue and alpha from 0 to 1 (a packed 0xAARRGGBB is unpacked that way at 0x14195B21A). The wristbands' colours are in media\GameTunableSettings.zip's PlayerXPLevels.xml, as signed ARGB ints: ColourNum for the band, TextColour for text on it.

  • The pad in the menus. Pages get the pad as keys, WPF's numbering, from a table at 0x142254390 (game control pressed, released, key): A is Space (18), B is B (45), X is X (67), Y is Y (68), the d-pad the arrows (23-26) and the number pad's 8/2/4/6 together, LB PageUp (19) and RB PageDown (20). The table also has the shifts (116, 117) and the controls (118, 119), which aren't the bumpers. On the HUD options page they reach the button handler at vtable +0x578 (0xCEC3D0).

Finding more

Most of this came from the exe's own strings and RTTI. The exe keeps MSVC type names like .?AVChampionshipDrivatarsState@SaveStates@@, which lead to vtables. SQL in the exe shows which tables of the game database it reads, and how. When you find something, a pull request adding it here helps the next person.

Documentation attribution & MIT license
MIT License

Copyright (c) 2026 Trinculo54

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.