Repository navigation
Game
Bartholomew Ho edited this page Nov 9, 2026
·
1 revision
The game itself: its thread, its frame, and whether it's ready. Use Game.Tick or Game.Run to run your code on the game's thread, where the other FH2 classes (Player, Garage, Profile...) are meant to be used.
Thread: the properties and Run/RunAsync can be used from any thread. Tick, Rewound and CarRelocated handlers run on the game thread. PhysicsStep handlers run on the game's physics thread.
Shows a notification each time you beat your top speed by 10 km/h, and adds a console command that repairs your car on the game's thread.
using FH2;
using FH2ModPlatform;
public sealed class TopSpeed : Mod
{
private float _best;
public override ModInfo Info => new("TopSpeed", "1.0.0");
protected override void OnInit()
{
Game.Tick += OnTick;
AddCommand("repair", "repairs your car", args =>
{
Game.Run(() => Player.Car?.Repair());
return "repairing";
});
}
private void OnTick()
{
var car = Player.Car;
if (car is null || car.SpeedKmh < _best + 10) return;
_best = car.SpeedKmh;
UI.Notify("New top speed", $"{_best:F0} km/h");
}
}Public Class TopSpeed
Inherits [Mod]
Private _best As Single
Public Overrides ReadOnly Property Info As ModInfo
Get
Return New ModInfo("TopSpeed", "1.0.0")
End Get
End Property
Protected Overrides Sub OnInit()
AddHandler Game.Tick, AddressOf OnTick
AddCommand("repair", "repairs your car",
Function(args)
Game.Run(Sub() Player.Car?.Repair())
Return "repairing"
End Function)
End Sub
Private Sub OnTick()
Dim car = Player.Car
If car Is Nothing OrElse car.SpeedKmh < _best + 10 Then Return
_best = car.SpeedKmh
UI.Notify("New top speed", $"{_best:F0} km/h")
End Sub
End Class| Member | What it does |
|---|---|
IsReady (property, get, bool) |
True once the SDK has checked the game's code and is hooked into the game's frame. Reading it starts the Game class if no mod has yet. |
IsDriving (property, get, bool) |
True while you're driving: your car is being simulated (not paused, not loading) and the HUD speedometer is up (not in the garage, a menu, a race intro...). Any thread. |
IsPaused (property, get, bool) |
True while the game is paused (the pause menu). Any thread. |
IsGameThread (property, get, bool) |
True when your code is running on the game's thread (inside Tick or Run). |
FrameCount (property, get, long) |
How many frames have ticked so far. Only frames while you drive are counted. |
Language (property, get, string?) |
The game's text language as a code: "EN", "DE", "FR", "ES", "MX", "IT", "BR", "JP", "KO", "CHT", "PL", "RU", "NL" (and "GB", "CZ", "HU"... which use English text). Null until the game has loaded its text. Any thread. |
LaunchLanguage (property, get, string) |
The language Azimuth starts the game in (from its data\azimuth.ini), as a Language code. This is what the game will most likely show before it has loaded its text. "EN" if it can't be told. Any thread. |
Base (property, get, nint) |
For advanced code of your own only. 0 until the game is loaded. |
| Member | What it does |
|---|---|
Run(Action action) |
Runs action on the game's thread at the next frame. If you call it from inside a Tick, Run, Rewound or CarRelocated handler, it runs straight away. |
RunAsync<T>(Func<T> func) → Task<T>
|
Runs func on the game's thread and gives you its result. If func throws, the task fails with that exception. Never await it from the game's thread. |
RunAsync(Action action) → Task
|
The same, for code that returns nothing. |
| Member | What it does |
|---|---|
Tick (event, Action) |
Once a frame, on the game's thread, while you drive. Keep it short: the game waits for it. |
PhysicsStep (event, Action<Vehicle, float>) |
Every car's physics step (yours, Drivatars', traffic), on the game's physics thread. You get the car and the step's length in seconds. This is where to push a car: set Vehicle.Velocity here. |
Rewound (event, Action<long>) |
You rewound: your car is back where it was at the moment given (an Environment.TickCount64 value), so undo what happened since. On the game's thread, when the car drives on. |
CarRelocated (event, Action) |
Your car was put somewhere new: a race started, you quit one, changed car or fast travelled. On the game's thread, when the car drives on. |
-
Tickonly fires while you drive. Nothing ticks on the title screen or while the game is paused. - Work you pass to
Runalso waits for the next frame you drive. So aRunAsynctask only finishes once you're driving. - Never wait for a
RunAsynctask from the game's thread (insideTick,Run, an ANNA entry). It needs the game's thread to finish, so it would never finish. - If a
Tick,Run,PhysicsStep,RewoundorCarRelocatedhandler throws, the error goes toplatform.log. The handler stays subscribed. -
PhysicsStepruns very often (every car, 60+ times a second). Keep it short, and use only the car you're given. Inside it, theVehiclemembers you'd normally use on the game thread work too. -
RewoundandCarRelocatedfire when the car drives on again, before that frame'sTickhandlers. - When your hosted mod is reloaded, its
Tick,PhysicsStep,RewoundandCarRelocatedhandlers and its queuedRunwork are removed for you. - The
Gameclass starts the first time a mod uses it. It checks the game's code first. On a game build the SDK doesn't know,IsReadystays false,Ticknever fires, andplatform.logsays what didn't match.PhysicsStepcan also be switched off on its own;platform.logsays so ("Game.PhysicsStep off"). - FH2 classes that need the game thread throw an
InvalidOperationExceptionwhen used from another thread. The message tells you to useGame.TickorGame.Run.
Playing
Getting started
Writing mods
The FH2 classes
- Game
- Player
- Vehicle
- World
- Garage, CarCatalog, Profile
- UI and Hud
- Camera
- Audio
- ANNA
- DebugOverlay
- Controller
- GameInput
- GameWindow
- Menus and UiElement
- SettingsTile
- ButtonIcons and Picture
- MapView, PhotoCamera, GarageCamera
More