This post covers pre-GameMode initialization for event-based mods, using MenuMode 4, the PostLoadGame event, and JIP LN script runner, with minimal GameMode usage. I assume you already have installed latest versions of xNVSE, JIP LN, and JohnnyGuitar NVSE.

Back to the roots

First of all, I’d like to debunk the myth that MenuMode 4 is exclusive to NVSE+JIP. Pre-GameMode initialization has always been available in vanilla. When the game loads to the main menu, it runs in MenuMode 1013 (Pause) and starts all quests with the “Start Game Enabled” flag, running blocks 1013. This is a perfectly valid pre-GameMode vanilla script:

scn InitQuestScript

int DoOnce

begin MenuMode 1013
    if DoOnce == 0
        set DoOnce to 1
        ; do something
    endif
end

 

Although, it’s not very useful due to limited abilities of vanilla GECK functions, which are mostly intended to be used in GameMode and are often persistent.

 

You can do some basic checks, like if NVSE or JIP are loaded. If you add NVSE functions to the quest script, it will silently fail in vanilla environment. Instead, add a stage with a script, where you would run IsModLoaded or IsPluginInstalled. To pass results back, add constant global variables, assign them in your stage script, and then check them from the quest. Example:

scn InitQuestScript

int DoOnce

begin MenuMode 1013

    if DoOnce == 0
        set DoOnce to 1
        SetStage TestQuest 1 ; set NVSEVer to GetNVSEVersion
        if NVSEVer < 6
            ShowMessage NoNVSEWarning
        endif
    endif

end

This script will warn user about missing or old NVSE even before the main menu.

 

If you need this quest only to perform version checks, you can stop it after loading to GameMode:

begin GameMode
    StopQuest MainMenuQuest
end

Everything changed with NVSE

NVSE’s non-persistent nature allows to run a function once on game start, and its results would stay active between different game loads, using GameMode execution when it’s really needed. NVSE allows to get rid of the temporary variable (DoOnce) and NVSE stage scripts, neatly putting everything together:

scn InitQuestScript

begin MenuMode 1013
    if GetGameRestarted ; yay!
        if GetNVSEVersion < 6
            ShowMessage NoNVSEWarning
        endif
    endif
end

 

jazzisparis introduced a separate block for the pause mode, running only from the main menu, and assigned code 4 to it:

scn InitQuestScript

begin MenuMode 1013
    ; this will run
end

begin MenuMode 4
    ; this will, too
end

These blocks will run every time in the order they are added to the script. GetActiveMenuMode will return 1013 in both, though.

 

Another notable change was introduced in JIP LN 54.75. Remember that constant global variable NVSEVer? If it’s assigned from the main menu, it would be carried over to GameMode, and then reset after returning to the menu. JIP LN 54.75+ changes this behavior to keep constant variable’s value through the whole session.

 

Now we can refer to the main menu as MenuMode 4.

MenuMode 4

There is an important point to remember, using an autostarting quest. When its delay is set to default value 0, it will start in 5 seconds after the game starts loading. With a lightweight setup, it’s possible to load a game faster and never run the quest script. This behavior changes once any custom value is set; can be 0.1 or 5, tests did not show any difference. In this case, the quest starts immediately after slides start rolling. Thus, never forget to set custom non-zero quest delay. There is no practical evidence of bypassing MenuMode 4 with custom delays.

 

Learn to differentiate quest state in MenuMode 4 and GameMode. In the main menu, you actually have a running game – your character has a name, initialized actor values, all autostarting quests are running, you just see a special pause menu in front of you. If you run coc <something>, you will jump to that location with this character. When a savegame is loaded, all non-NVSE data is reset. Quests are restarted, quest variables are cleared, all character’s actor values are refreshed.

scn InitQuestScript

int iFlag

begin MenuMode 4
    if GetGameRestarted
        set iFlag to 42 ; this value is accessible until a save is loaded
    endif
end

begin GameMode
    print $iFlag ; will print 0, because all quest variables have been reset
end

 

On return to the main menu, you have a clean slate again: new default character, all quest variables are cleared, and iFlag is equal 0 because GetGameRestarted runs only once per session.

 

But non-persistent attributes, set with NVSE functions, may stay.  For instance, you can add a temporary auxiliary attribute to playerRef, and it will remain unchanged through the whole session, even though vanilla attributes will be reset:

begin MenuMode 4
    if GetGameRestarted
        playerRef.AuxiliaryVariableSetString "*myattribute" "value"
    endif
end

 

If you set temporary NVSE attributes in GameMode or modify base forms, changes will leak to other characters. Event handlers normally stay, too (there are exceptions). Either use this to your advantage, or be prepared to clean it up.

 

Keeping all that in mind, here is my process of MenuMode 4 initialization:

scn InitQuestScript

begin GameMode
    StopQuest InitQuest ; No zombies allowed!
end

begin MenuMode 4

    StopQuest InitQuest

    if GetGameRestarted == 0
        ; clean up temporary NVSE attributes and event handlers
        return
    endif

    ; do something once
end

 

What happens here:

  1. MenuMode 4 starts running.
  2. As we need it to run only once, we can stop this quest immediately. StopQuest is not equivalent to return, execution will continue until the end of the block.
  3. The GetGameRestarted == 0 block will not run during the first execution.
  4. Run necessary NVSE functions after it.
  5. Block doesn’t run again, because we just stopped the quest.
  6. On entering GameMode, the quest will restart, if it wasn’t stopped from GameMode before. If not needed, it can be stopped again, this state will be written into the savegame, and the quest will not autostart in GameMode again, unless you restart it manually.

 

GameMode in quest scripts can be used for one-time initialization, but it has a significant drawback. You can’t start it again without using some other mode or function. PostLoadGame provides more flexibility in controlling the mod state, see below.

 

Why StopQuest is placed before GetGameRestarted? As you already know, the quest will restart once user exits to the main menu. StopQuest will run again, execution will enter the GetGameRestarted == 0 block, which will run only if the block runs repeatedly. This is the moment when you can cleanup temporary changes or event handlers.

 

Alternatively, you can use the ExitToMainMenu event, which would do the same.

Controlling state using NVSE events

During game session, NVSE sends certain events to plugins and scripts, you can see them here. Some of them are available to scripts as well. For now, we are mostly interested in PostLoadGame, running right after savegame is loaded.

 

On PostLoadGame, character and quests are fully initialized. You can access inventory, perks, global variables, running and finished quests, etc. You cannot access non-persistent world objects and NPCs, parent cell, or 3D models around character as it’s not placed in any cell yet. But don’t think of it as an obstacle, you have enough tools to schedule any operations for GameMode.

 

PostLoadGame does not fire on starting new game, NewGame runs instead. As we normally need to run the same actions on both events, we would prefer to use the same UDF. A PostLoadGame callback requires one integer argument, while NewGame requires a UDF without arguments. To avoid adding a separate UDF for NewGame, we can hook it like this:

begin MenuMode 4

    StopQuest InitQuest

    if GetGameRestarted == 0
        return
    endif

    SetEventHandler "PostLoadGame" MyPostLoadGameHandler
    SetEventHandler "NewGame" ({} => call MyPostLoadGameHandler 1)

    ; This event handler can be set once
    SetEventHandler "OnAdd" OnAddPurifiedWater "first"::WaterPurified "second"::playerRef
end

 

What can we do in PostLoadGame?

  • Ensure persistent mod data is present and valid (user perks, running quests).
  • Perform version migration, if mod is updated mid-game.
  • Schedule GameMode execution, if you need access to the world or want to re-bind flushed event handlers.

 

Unfortunately, printing to the console does not work during this stage. For console output, schedule GameMode execution. Printing in loading menu became possible as of JIP LN 56.34.

 

Example:

begin function MyPostLoadGameHandler { iArg }

    ; We don't need GameMode for this
    if playerRef.HasPerk MyPerk == 0
        playerRef.AddPerk MyPerk
    endif

    if MyCurrentVersion < 2 ; non-constant global variable	
	; perform migration to version 2
	set MyCurrentVersion to 2
    endif

    if MyCurrentVersion < 3
	; perform migration to version 3
	set MyCurrentVersion to 3
    endif

    ; Schedules one-time script execution in GameMode.
    ; SetGameMainLoopCallback is a JIP LN alternative to it, but you'll have to unregister it.
    CallAfterSeconds 0 MyGameModeScript

end

 

begin function MyGameModeScript {}

    ; SetJohnnyOnCrosshairEventHandler is one of the event listeners, cleared on game load.
    ; PostLoadGame is too early for it, we must bind it in GameMode.
    SetJohnnyOnCrosshairEventHandler 1 MyCrosshairHandler 0

    if playerRef.GetInCell GSDocMitchellHouse
        ; we have access to the environment
    endif
end

 

As you can see, we can listen to the main game events:

  • Game start (MenuMode 4)
  • New game (NewGame)
  • Game load (PostLoadGame)
  • GameMode
  • Exit to the main menu (ExitToMainMenu or the GetGameRestarted == 0 block).

JIP LN Script Runner

JIP LN also allows to run batch scripts on the main game events. This feature is covered in this post.

 

It can be used with or instead of the MenuMode 4 workflow. For complex mods, I recommend to use gr_*.txt files to call initialization UDFs, where you would hook necessary events. This way, you’ll be still able to track form connections. Batch scripts are a good place to perform some sanity checks, in case you use some new xNVSE syntax.

 

gr_my_mod_init.txt

if GetNVSEVersionFull < 6.2
    MessageBoxEx "My cool mod requires xNVSE 6.20+."
endif
; Look, ma, no quests!
call MyInitScript

If user has NVSE 5.1.4, and MyInitScript uses lambdas, at least user will know why the mod does not work.

 

Batch scripts gr_*.txt run very early, at the end of JIP LN initialization and before any autostarting quest. This is the earliest point, available to scripts.

0 Comments
Inline Feedbacks
View all comments