Theme
Play mode selection
Which PvP mode this session is in, read from the game's own mode manager.
The mode is the mode object, and the object is read from CreateGameMode - the description it is handed to build the mode from has a pointer into the game's own mode table as its first field, one entry per mode, stable across sessions (Mercenaries was 38DD178 in all three, the campaign 38DC7F0 every time). That call is hooked (scripthook_playmode.c), its identity is checked against the build before the hook is armed, its argument is read off the watcher thread
- never inside the hook - and an object that is not in the framework's table is logged and left undecided rather than guessed at (
ShPlayModeFingerprinthands the number out for exactly that case; that is how Guerrilla was found).
GameModeManager::SetCurrentGameMode is hooked too, and its argument is logged with every mode, but it is not what names the mode: measured, it is 3 for Ghost War and 2 for the three modes that share it, but it is also 0 for the campaign, Narco Road *and* 2 for Fallen Ghosts with the same campaign object. The same mode content can come with either number, so the argument is recorded and the object decides.
It has to be that call, because nothing else in the engine says it. Measured, in the main menu and in the Ghost War lobby: the GameFlow state name, the GameFlow sub object class hashes, the drawn scene set (empty in the front end), the UI state bits, the input context, the entity counts and the archives read are all identical - PvE is four player co-op, so other humans prove nothing, and every mode reads the same 23 archives because the engine indexes all of them at startup. The mode name in memory is already there in the main menu because it is the button's own label. The objects are reflected (32 byte entries of crc32(name), index and function, and that hash is plain CRC-32), but 312 front end methods resolved against a 24000 name dictionary came back with nothing but the scene interface, and the mode manager's own method names match no method table entry at all. The named lifecycle methods were hooked as a last resort: they fire, with a real dispatcher chain in the stack, but with the same signature for all three modes - page navigation, not a mode choice - and no mode id or name appears in their arguments either.
What this costs and what it does not promise:
NONEuntil the game has set a mode, so asking early is safe and says "not yet" rather than guessing.ShPlayModeHookArmedsays whether it ever will on this build.- The mode is measured, not documented: an object that is not in the framework's table is left
NONEwith a log line carrying it (and the argument the game used), rather than guessed at. - A different build whose layout differs will not arm the hook, and the answer stays
NONE. - The mode is known from the moment the game sets it, in the lobby and in a match alike; nothing here needs a mouse, a resolution or a UI scale to be right.
| Export | Summary |
|---|---|
ShSelectedPlayMode | The mode this session is in, one of ShPlayMode. |
ShIsGhostWarMode | ShSelectedPlayMode() == the mode named. |
ShIsMercenariesMode | See ShIsGhostWarMode. |
ShIsGhostMode | See ShIsGhostWarMode. |
ShIsGuerrillaMode | See ShIsGhostWarMode. |
ShPlayModeFingerprint | The mode object's identity, as an RVA into the game's own mode table, or 0 while it is not known. |
ShPlayModeEvidence | What the answer rests on, for a log line or a menu: "GameModeType 2 with mode object 38DD178 (MERCENARIES)", or why there is none yet. |
ShPlayModeHookArmed | 1 when the game's mode manager is being read, so a caller can tell "not yet" from "not on this build" - and say so, rather than treating NONE as "no mode in particular". |
ShPlayModeBit | The bit a mode occupies in a blacklist mask (see the plugin blacklist group below), or 0 for a value that is not a mode. |
ShPlayModeName | A mode's name, spelled the way the framework logs it elsewhere: "Ghost War", "MERCENARIES", "campaign", "Ghost Mode", "Guerrilla", and "" for a value that is not a mode. |
ShSelectedPlayMode
c
SH_API int ShSelectedPlayMode(void);The mode this session is in, one of ShPlayMode.
NONE means not decided yet, which is also the honest answer while the mode object is still being read - the wait is a watcher tick, 200 ms at most - or when the object is one the framework does not know.
scripthook.h, line 289
ShIsGhostWarMode
c
SH_API int ShIsGhostWarMode(void);ShSelectedPlayMode() == the mode named.
scripthook.h, line 292
ShIsMercenariesMode
c
SH_API int ShIsMercenariesMode(void);Documented together with ShIsGhostWarMode just above.
scripthook.h, line 293
ShIsGhostMode
c
SH_API int ShIsGhostMode(void);Documented together with ShIsGhostWarMode just above.
scripthook.h, line 294
ShIsGuerrillaMode
c
SH_API int ShIsGuerrillaMode(void);Documented together with ShIsGhostWarMode just above.
scripthook.h, line 295
ShPlayModeFingerprint
c
SH_API int ShPlayModeFingerprint(void);The mode object's identity, as an RVA into the game's own mode table, or 0 while it is not known. This is the second half of the answer for the modes that share an argument: the table in scripthook_playmode.c maps each one to a mode, and an object that is not in it is logged by the framework - send that line in and it gets a row.
scripthook.h, line 303
ShPlayModeEvidence
c
SH_API int ShPlayModeEvidence(char *buf, int len);What the answer rests on, for a log line or a menu: "GameModeType 2 with mode object 38DD178 (MERCENARIES)", or why there is none yet. 1 when the mode is known.
scripthook.h, line 308
ShPlayModeHookArmed
c
SH_API int ShPlayModeHookArmed(void);1 when the game's mode manager is being read, so a caller can tell "not yet" from "not on this build" - and say so, rather than treating NONE as "no mode in particular".
scripthook.h, line 313
ShPlayModeBit
c
SH_API uint32_t ShPlayModeBit(int mode);The bit a mode occupies in a blacklist mask (see the plugin blacklist group below), or 0 for a value that is not a mode.
scripthook.h, line 317
ShPlayModeName
c
SH_API const char *ShPlayModeName(int mode);A mode's name, spelled the way the framework logs it elsewhere: "Ghost War", "MERCENARIES", "campaign", "Ghost Mode", "Guerrilla", and "" for a value that is not a mode. It is a translation key: pass it through ShLang before showing it.
scripthook.h, line 323