Skip to content

H7ModEngine modding API

H7ModEngine lets you write mods for Might & Magic Heroes VII (v2.2.1) in Lua 5.4. A mod can read and change any game object, call any UnrealScript function, and hook game functions to watch, change or block what they do. Mods reload automatically while the game runs, so you can edit and see the result without restarting.

Installation

  1. Copy dinput8.dll into <Game>\Binaries\Win64\.
  2. Copy the H7ModEngine folder into the game folder (<Game>\H7ModEngine\).
  3. Start the game as usual (through Ubisoft Connect).

<Game>\H7ModEngine\ then holds everything the engine uses:

Folder Contents
mods\ One folder per mod
logs\engine.log Everything the engine and your mods log (the previous run is in engine.prev.log)
data\ Saved mod data (mod.save)
lib\ Shared Lua libraries any mod can require
exec\ Drop a .lua file here to run it once in the running game (for testing)
dumps\ objects.txt / names.txt: an index of every engine object, written at startup
H7ModEngine.ini Configuration

Your first mod

Create <Game>\H7ModEngine\mods\MyFirstMod\main.lua:

h7.log("Hello from " .. mod.name)

h7.on("map_loaded", function(map)
    h7.log("Entered map: " .. map)
end)

Start the game and open logs\engine.log:

[19:32:14.962] [MyFirstMod] Hello from MyFirstMod
[19:32:14.962] Loaded mod MyFirstMod
[19:32:14.963] [MyFirstMod] Entered map: h7logoentry
[19:32:19.583] [MyFirstMod] Entered map: councilhub

Now edit main.lua while the game runs and save it: within a second the engine unloads the mod and runs it again. Errors don't crash the game; they are logged with a stack trace:

[19:40:01.120] ERROR in MyFirstMod/main.lua: ...\mods\MyFirstMod\main.lua:3: attempt to call a nil value ...

A real gameplay mod is just as short. This one gives the human player 500 gold every new day:

h7.hook_post("MMH7Game.H7Calendar.NextDay", function(calendar)
    local controller = h7.first("H7AdventureController")
    local player = controller and controller:GetLocalPlayer(true)
    if not player then return end
    player:GetResourceSet():ModifyCurrency(500, true)
    h7.log("+500 gold for " .. player:GetName())
end)

Mod folder and manifest

mods\
  MyMod\
    mod.lua      -- manifest (optional)
    main.lua     -- runs when the mod loads (required)
    util.lua     -- extra files, loaded with mod.require("util")

mod.lua returns a table. Every field is optional; without a manifest the folder name is the mod name.

return {
    name = "MyMod",              -- default: folder name
    version = "1.0",
    author = "You",
    description = "What the mod does.",
    load_after = { "OtherMod" }, -- load after these mods (if they are installed)
    enabled = true,              -- false: off until a player turns it on
    category = "Combat",         -- shown (and filterable) in the Mods menu
    hidden = false,              -- true: not listed in the in-game Mods menu
    settings = { ... },          -- values players can change in the Mods menu, see Settings below
}

Load order. Mods load alphabetically by name, except that a mod is moved after every mod it lists in load_after. After all mods have loaded, the mods_loaded event fires.

Turning mods on and off. Players use the Mods entry in the main menu (the bundled ModManager mod), which lists every mod with its description and an Enabled/Disabled toggle. A change applies at once: the mod loads or unloads on the next frame. The choice is stored in H7ModEngine.ini (see Configuration) and overrides the manifest's enabled. Mods can do the same with h7.set_mod_enabled(name, on); the mod_toggled event reports each change.

Settings. A mod can offer values that players change in the Mods menu: selecting a mod shows its description and, below it, its settings as controls. Declare them in the manifest:

settings = {
    { key = "multiplier", label = "Damage multiplier", default = 1.25, min = 0.25, max = 4, step = 0.25,
      description = "All combat damage is multiplied by this." },
    { key = "gold", label = "Gold per day", default = 500, min = 0, max = 100000, control = "input" },
    { key = "log", label = "Log hits", default = true },
    { key = "who", label = "Applies to", options = { "Human player", "Everyone" }, default = "Human player" },
    { key = "greeting", label = "Greeting", type = "text", default = "Hello" },
}
Field
key Name used in code (required)
label, description Shown in the Mods menu (label defaults to key)
type "bool", "number", "choice" or "text"; inferred from default (and options) when left out
default Value until the player changes it
min, max, step Numbers: values are clamped to min..max and rounded to a multiple of step (counted from min)
options Choices: the allowed values, in the order the menu cycles through them
reload true: the mod restarts when the value changes (for values only read at load time)
control Numbers: how the Mods menu shows it. "slider" (default with min and max), "input" (a box the player types the number into; default without a range) or "arrows" (steps of step)

Read the current values from mod.settings (mod.settings.multiplier); it always has the latest value, so read it when you need it rather than copying it once. Assigning (mod.settings.log = false) changes the setting like the menu does. pairs(mod.settings) lists them. Values are stored in H7ModEngine.ini, section [Mod.<name>]. When a value changes the setting_changed event fires (mod, key, value). Other mods and exec scripts use h7.mod_settings(name) (a list of the declarations with their current value) and h7.set_mod_setting(name, key, value), which returns the stored (clamped) value and raises an error for an unknown key or an invalid value. In the menu, numbers get a slider (drag or click it) or a number box (click it, type, Enter saves, Escape cancels, clicking elsewhere saves; the value is clamped to the range), on/off settings a check box, choices arrows that step through the values; text settings are shown but can only be changed in code or the ini. Every change is saved at once. The panel has room for five settings; further ones are only listed as being in the ini.

Each mod has its own globals. Global variables you create stay inside your mod; reading a global you didn't create falls through to the shared environment (h7, string, math, table, ...). Two mods can both define function on_unload() without clashing.

The mod table is available in every mod:

Field
mod.name, mod.version, mod.author From the manifest
mod.dir Full path of the mod folder
mod.require(name) Runs <mod folder>\<name>.lua in the mod's environment once and returns its result (or true); later calls return the cached result
mod.save(table) / mod.load() Persistent data, see Saving data
mod.settings The mod's settings: read the current values, assign to change them (see Settings above)

Unloading and hot reload. About once a second the engine checks each loaded mod's folder (including subfolders) for changed .lua files. When one changed, the mod is unloaded and its main.lua runs again. Unloading calls the mod's global on_unload() if it defines one, then removes everything the mod registered: hooks, event handlers and timers. You don't have to clean those up yourself.

Hot reload only covers mods that are already loaded. To pick up a new mod folder or a mod you just enabled, call h7.reload() (for example from the exec folder) or restart the game.

Shared libraries. The standard require("name") loads <Game>\H7ModEngine\lib\name.lua. Use it for code several mods share.

Finding what to change

Heroes VII's gameplay is written in UnrealScript, and every class, property and function in it is reachable from Lua. To find the right one:

  • Read the UnrealScript sources. Decompiled sources for the game (the MMH7Game package has about 1400 classes) are the best map: they show what each function does and who calls it. Search them for the behaviour you want to change, then hook the function that computes it.
  • Browse the game classes on the documentation site: every class the game loads with its properties, functions (with hook paths), structs, enums and default values.
  • Search dumps\objects.txt. One line per engine object, written at startup: 12345 00007ff4fd9f4f40 Function MMH7Game.H7Calendar.NextDay.
  • Ask the engine. From a mod or the exec folder:
h7.log(h7.signature("MMH7Game.H7GameProcessor.CalculateDamage"))
-- function void MMH7Game.H7GameProcessor.CalculateDamage(out H7CombatResult Result, int resultIdx, bool forecast)

for _, f in ipairs(h7.functions("MMH7Game.H7Calendar")) do h7.log(f.owner, f.name) end
for _, p in ipairs(h7.properties("MMH7Game.H7Calendar")) do h7.log(p.type, p.name) end

Hook targets used by the example mods (all verified in game):

To change Hook Notes
Start of a new day MMH7Game.H7Calendar.NextDay Called from H7AdventureController.SetNextPlayer when the last player ends the turn
Damage of an attack MMH7Game.H7GameProcessor.CalculateDamage Also runs for damage previews (forecast = true) and heals
Experience after combat MMH7Game.H7CombatArmy.GetKilledCreaturesXP H7EditorHero.AddXp is native and can't be hooked usefully
Hero movement points MMH7Game.H7EditorHero.GetMovementPoints Caravans are H7EditorHeros too

Engine objects

Engine objects (actors, heroes, players, armies, ...) appear in Lua as references. A reference points at the live object: reading a property always returns its current value.

local hero = h7.first("H7AdventureHero")

h7.log(hero)                     -- H7AdventureHero sce_sephinroth.TheWorld.PersistentLevel.H7AdventureHero_0
local player = hero:GetPlayer()  -- call an UnrealScript function
h7.log(player:GetName())

Properties are read and written by name, exactly as declared in UnrealScript:

local stack = h7.first("H7CreatureStack")
h7.log(stack.mHumanDamageMultiplier)
stack.mHumanDamageMultiplier = 2

Writing a property that doesn't exist is an error; reading one returns nil.

Functions are called with : and take their parameters in declaration order:

local resources = player:GetResourceSet()
resources:ModifyCurrency(500, true)     -- ModifyCurrency(int modAmount, bool updateGui)
  • The function's return value comes back first, followed by the values of its out parameters, in order. Struct and array results come back as plain Lua tables.
  • Missing arguments (or nil) are passed as zero: 0, false, "", no object. UnrealScript default values are not applied, so pass optional parameters explicitly when their default isn't zero.
  • If a property and a function share a name, call the function with obj:call("Name", ...).
  • Calls run the function exactly as the game would, including any hooks on it.

Helper methods on every object (lowercase, so they never clash with game members):

Method Returns
get_name() Object name, e.g. H7AdventureHero_0
get_full_name() Class and path, e.g. H7AdventureHero sce_sephinroth.TheWorld.PersistentLevel.H7AdventureHero_0 (also what tostring gives)
get_path_name() Path without the class
get_class() The object's class (an object)
get_outer() The object that contains it
get_default() On a class: its default object (Default__ClassName)
get_super() On a class, struct or function: its parent
get_address() Memory address as an integer (useful as a table key)
is_valid() false once the game has destroyed the object
is_a(class) Whether the object is of class or a subclass (class object or name)
call(name, ...) Calls a function by name

Two references to the same object are equal (==). An object can be destroyed while your mod still holds a reference (for example when a map unloads); using it then raises "engine object ... is no longer valid". Don't keep object references across maps, or check is_valid() first.

Values and types

UnrealScript Lua
int, byte, enums integer (enums are their numeric value; see h7.enum)
float number
bool boolean
string string
name string (a name the engine doesn't know yet is added to its name table)
object, class, interface object reference, or nil for none
struct live struct reference: v.X reads and v.X = 1 writes the field in place
array<T> live array reference (see below)
fixed array (int X[4]) a Lua table copy; assign a table to write it
delegate {object = ..., func = "Name"}; pass the same shape to a delegate parameter, e.g. widget:AddEventListener("CLIK_click", {object = cntl, func = "Options"})

Assigning an object to an object property checks its type. A struct property can be assigned a struct reference of the same type or a Lua table of fields: obj.Location = {X = 0, Y = 0, Z = 100}.

Arrays are live views of the engine array:

local obelisks = player.mVisitedObelisks   -- array<H7Obelisk> property of H7Player
h7.log(#obelisks)                          -- length
local first = obelisks[1]                  -- 1-based, like Lua
obelisks[1] = other                        -- replace an element
obelisks:add(value)                        -- append; returns the new length
obelisks:remove(2)                         -- remove element 2, shifting the rest down

Assigning a Lua table (or another engine array) to an array property replaces its contents: obj.mNames = {"a", "b"}. The same works for array parameters of a function call; elements are converted like properties, so an array of structs takes a table of tables:

-- GFxObject.Invoke(string Member, array<ASValue> args)
local AS = h7.enum("ASType")
widget:Invoke("setTextColor", { { Type = AS.AS_Number, n = 0x9be37a } })

Leaving an array argument out passes an empty array.

An array returned by a function is a plain Lua table copy instead (player:GetHeroes()), since the engine's copy is gone once the call returns. UnrealScript's private/protected don't apply: mods can read and write every property.

h7.to_table(value) copies a struct, array or object's properties into a plain Lua table: handy for logging or for keeping a snapshot.

Looking things up

Function Returns
h7.first(class) The first live instance of class (or a subclass), or nil
h7.find_all(class [, include_templates]) All live instances. Class defaults and archetypes (templates stored in content packages) are skipped unless include_templates is true
h7.find(path) The object with this path: "MMH7Game.H7Calendar", or with its class to be exact: "Class MMH7Game.H7Calendar", "Function MMH7Game.H7Calendar.NextDay"
h7.find_class(name) A class by "Package.Class" or bare "Class" name
h7.enum(name) A table of an enum's values: h7.enum("EMyEnum").SOME_VALUE → integer
h7.properties(x) / h7.functions(x) Lists of {name, type, owner, offset} / {name, type, owner, flags}, including inherited members (type is the engine type, e.g. IntProperty; owner the declaring class). x is a class or struct name, an object, or a struct reference
h7.signature(func) The function's declaration as text, e.g. function string Engine.Controller.GetHumanReadableName()

class can be a name ("H7AdventureHero" or "MMH7Game.H7AdventureHero") or a class object. find_all scans every engine object, so call it when you need it rather than every frame.

Creating objects

Mods can create new engine objects, most usefully copies of game data (archetypes: the templates the game spawns creatures, heroes, towns and buildings from). A copy starts with every property of its source; change what you need and add it to the game's lists. This is how a mod adds a new creature, hero or a whole faction.

local pkg = h7.package("MyMod")                 -- a package of your own: gives copies stable names
local src = h7.find("Creature_Pixie.A_CreaturePixie")
local sprite, created = h7.clone(src, "A_CreatureFrostSprite", pkg, h7.flags.RootSet)
sprite.mNameInst = "Frost Sprite"               -- name shown in game (see below)
sprite.mHitpoints = 20
Function
h7.clone(source, name [, outer [, flags]]) A copy of source (same class, every property copied) called name inside outer (default: the transient package). Returns the object and true; if outer already holds an object called name of the same class it is returned with false instead, so the same code can run again after a hot reload. Archetypes stay archetypes. flags adds object flags
h7.new_object(class, {name=, outer=, template=, flags=, archetype=, replace=}) A new object of class, with properties from template (or the class defaults). Fails if outer already has an object called name, unless replace = true
h7.package(name) The top-level package name, created if it doesn't exist
h7.name(str) Adds str to the engine's name table and returns it
h7.can_create() false if the engine couldn't verify the game functions it needs (see the log); the functions above then fail
h7.flags Object flags: Public, Standalone, Transient, Archetype, RootSet

Things to know:

  • Keep copies alive. Outside the editor the garbage collector frees anything nothing references, on every map change. Pass h7.flags.RootSet (never freed) or make sure game data the game keeps references the copy.
  • Lazily loaded references. Many Heroes VII references (creature and hero models mVisuals, icons, town screens, H7Faction.mStartTown...) are DynLoadObjectProperty fields that the game loads from the object's package when first needed and releases again with Del* functions. A copy has no package, so it can never load them: call source:DynLoadObjectProperty("mVisuals") (or DynLoadObjectArrayProperty for arrays) on the source before cloning, and stop the game's Del* functions for your copies (and their sources) with a pre-hook. A creature without its model crashes the game when a battle starts.
  • Names and texts are localised by object name, which a copy doesn't share with its source: the game shows the copy's fallback text (usually English). Set the cached texts yourself, e.g. mNameInst on units, or hook the GetName of the class.
  • Save games store references by object path (MyMod.A_CreatureFrostSprite), so create your copies with the same names before a save is loaded (at the main menu), and never rename them.

Hooks

A hook runs your Lua function whenever a game function is called.

-- Before the function runs. Return true to block it.
local id = h7.hook("Package.Class.Function", function(self, args)
end)

-- After the function has run. Return a value to replace its return value.
local id = h7.hook_post("Package.Class.Function", function(self, args, result)
end)

h7.unhook(id)

h7.hook(func, fn, "post") is the same as h7.hook_post(func, fn). The function is named Package.Class.Function or passed as a function object.

Parameters. - self is the object the function was called on. - args gives access to the function's parameters by name: args.Result, args.resultIdx. You can also assign them (args.Amount = 10 in a pre-hook changes what the function receives). out parameters can be read and written the same way. - In a post-hook, args shows the values after the function ran, and result is its return value (absent for functions that return nothing). - args and any struct or array read from it are only valid while your hook runs. Copy what you need (h7.to_table) if you want to keep it.

Blocking. A pre-hook that returns true stops the original function from running. The caller then gets a zero return value (0, false, "", nil). Post-hooks still run.

Changing the result. A post-hook that returns a non-nil value replaces the function's return value. Returning nothing leaves it unchanged.

-- CombatXP: 50% more experience from combat
h7.hook_post("MMH7Game.H7CombatArmy.GetKilledCreaturesXP", function(army, args, xp)
    if xp > 0 then return math.floor(xp * 1.5 + 0.5) end
end)

-- DamageScale: read an argument and call methods on it
h7.hook_post("MMH7Game.H7GameProcessor.CalculateDamage", function(processor, args)
    local result, idx = args.Result, args.resultIdx
    if not result or result:IsHeal(idx) then return end
    result:SetDamage(math.floor(result:GetDamage(idx) * 1.25 + 0.5), idx)
end)

What a hook sees. - Hooks on UnrealScript functions see every call, whether it comes from other UnrealScript or from the engine's C++ code. - A hook applies to the function exactly as named. If a subclass overrides the function, calls on subclass objects run the override, so hook the override too (H7CreatureStack.ApplyDamage is separate from H7Unit.ApplyDamage). - Native functions (implemented in C++, native in h7.signature) can be hooked, but args is nil and they can't be blocked. - Latent and iterator functions (Sleep, foreach iterators) only report calls made from C++.

Several hooks on one function (from one or more mods) all run, in the order they were added.

Speed. Some functions run hundreds of times a second (for example during combat). Keep hooks on them cheap: check the cheapest condition first, and cache answers per object, as the HeroMovement example does:

local is_local = {}
h7.on("map_loaded", function() is_local = {} end)

h7.hook_post("MMH7Game.H7EditorHero.GetMovementPoints", function(hero, args, points)
    local key = hero:get_address()
    if is_local[key] == nil then
        local player = hero:GetPlayer()
        is_local[key] = player ~= nil and player:IsControlledByLocalPlayer()
    end
    if is_local[key] then return math.floor(points * 1.5 + 0.5) end
end)

Events and timers

local id = h7.on("map_loaded", function(map) ... end)
h7.off(id)
Event Arguments When
tick dt (seconds since the last frame) Every frame
map_loaded map name (lowercase), e.g. h7logoentry, councilhub (main menu), sce_sephinroth When a different map is active. Battles don't trigger it
mods_loaded number of mods After all mods have loaded (also after h7.reload())
mod_toggled mod name, enabled After a mod was loaded or unloaded by h7.set_mod_enabled (the Mods menu)
setting_changed mod name, key, new value After a mod setting changed (Mods menu, mod.settings, h7.set_mod_setting)

Timers run on game time:

local id = h7.after(5, function() h7.log("5 seconds later") end)   -- once
local id = h7.every(60, function() h7.log("every minute") end)     -- repeating
h7.cancel(id)

Events, timers and the hot-reload check run on the game's frame. They pause while the game window is in the background, because the game stops updating then.

Saving data

local data = mod.load()            -- {} the first time
data.launches = (data.launches or 0) + 1
mod.save(data)

mod.save(table) writes <Game>\H7ModEngine\data\<ModName>.lua; mod.load() reads it back. Numbers, strings, booleans and nested tables are saved; anything else (functions, engine objects) is dropped. The data belongs to the mod, not to a savegame.

Logging and debugging

Function Writes to logs\engine.log
h7.log(...), print(...) [MyMod] message
h7.warn(...) WARNING [MyMod] message
h7.error(...) ERROR [MyMod] message

Hooks, event handlers and timers run as the mod that registered them: their log lines carry its name, and any hook, handler or timer they create belongs to that mod too (and is removed when it unloads).

Errors inside hooks, event handlers and timers are caught and logged as ERROR in hook ... (MyMod) with a stack trace; the game keeps running and the hook stays active.

The debug console (the DebugConsole mod, shipped with the engine and off by default): turn it on in the Mods menu (category Developer), then press the key from H7ModEngine.ini ([Console] Key=, default Tilde, the key left of 1) in any screen. It shows the engine log live and runs what you type:

Input
h7.first("H7AdventureHero"):GetName() A Lua expression: its value is printed (tables expanded two levels; dump(v, depth) for more)
x = h7.first("H7Calendar") A statement; variables you set stay for the session
/stat fps A game console command (h7.exec); commands("cheat") lists the commands the current screen accepts
Several lines A one-time script: Enter on an unfinished chunk (for i = 1, 3 do) starts a new, indented line and runs the whole script once it is complete; Shift+Enter always adds a line, Ctrl+Enter always runs. If the last line is an expression its value is printed. Ctrl+V pastes a script from the clipboard

Suggestions appear under the input as you type: after /, the game's console commands (the exec functions of the viewport, its console and interactions, the player controller, its cheat manager, HUD, pawn and input, and the game info, read live; plus the engine commands from the game's console command list) with their parameters, which stay shown while you type the arguments; in Lua, globals and keywords, table fields, and an object's properties (after .) or functions with their signatures (after :). Up/Down choose, Tab accepts, Esc hides. Each row shows the name, its kind (function, method, property, table, value, keyword, command), its signature or type and the class that declares it. Signatures use one notation: (class: string | Class, include_templates?: bool) -> Object[]; ? marks an optional parameter (from Lua a skipped one is passed as zero, not its UnrealScript default), and a game function's out parameters are listed after -> too, since Lua gets them as extra return values. While you type a call's arguments (h7.first(, hero:GetPlayerOwner(, /Summon), the popup shows the signature with the current argument underlined, and a one-line description for h7 functions. Suggestions also work after calls that only look things up (h7.first("H7AdventureHero"):Get): h7.first, h7.find, h7.find_class, h7.find_all, h7.enum and methods named Get.../Is... are run while you type (other calls never are). The game's command list includes engine commands this build doesn't have (e.g. Stat GAME; Stat FPS works). These are never suggested: the mod ships the list of missing ones for v2.2.1, tests the list's Stat/Show entries once per install when it loads (each runs twice in one frame, on and off again, so nothing changes), and learns any other command the game answers with "Command not recognized".

Keys
Up/Down Suggestion, line of a script, or history (saved across sessions)
Tab Accept the suggestion; indent on an empty line
PgUp/PgDn, mouse wheel Scroll the output
Ctrl+V / Ctrl+C / Ctrl+U / Ctrl+L Paste / copy the input / clear the input / clear the output
Esc Hide the suggestions, then close the console

While it is open the game doesn't get the keyboard (the mouse still works). Hooks and timers created from the console belong to DebugConsole and are removed when it is turned off or reloaded. Its settings (Mods menu): how much of the screen it covers and the text size.

The exec folder runs Lua in the running game. Copy a .lua file into <Game>\H7ModEngine\exec\. Within about a second it runs, its output and return values go to the log (exec: => ...), and the file is renamed to .done. Exec scripts share the global environment, not a mod's.

-- exec\check.lua
local hero = h7.first("H7AdventureHero")
return hero, hero:GetMovementPoints()

h7.exec("command") runs a console command through the local player controller and returns its output.

Networking

Mods can talk to web services (h7.http), read and write JSON (h7.json) and open ports on the player's router (h7.net), and hash or generate secrets (h7.crypto). Network calls never block the game: they run in the background and call your function with the result on a later frame. If your mod unloads first, the function is not called.

h7.http.get("https://example.com/api/games", function(res)
    if not res.ok then return h7.warn("request failed: " .. res.error) end
    if res.status ~= 200 then return h7.warn("HTTP " .. res.status) end
    local games = h7.json.decode(res.body)
    h7.log(#games .. " games listed")
end)

-- A table body is sent as JSON.
h7.http.post("https://example.com/api/games", { name = "My game", slots = 2 }, function(res) end)

-- Let other players reach this PC on UDP port 27015 (most home routers support this).
h7.net.upnp_add(27015, "UDP", function(r)
    h7.log(r.ok and ("public address " .. tostring(r.external_ip)) or ("UPnP failed: " .. r.error))
end)

res has ok (false when no response arrived: no connection, DNS error, timeout), status, body, headers (lower-case names) and error. A 404 or 500 is ok = true with that status. Decoded JSON null is h7.json.null, and decoded arrays stay arrays when encoded again (even empty ones).

Verified in game (JSON, local_ip, an HTTPS GET from the main menu) and outside it with build.bat nettest online (HTTPS GET/POST, errors, dropped callbacks, UPnP map/unmap on a real router). UPnP from inside the game is not verified yet.

Configuration

<Game>\H7ModEngine\H7ModEngine.ini:

[Mods]
; Comma-separated mod names, written by the in-game Mods menu. They override the manifests' enabled field.
Enabled=DailyGold
Disabled=HelloWorld

; Mod settings, written by the Mods menu (one section per mod; see Settings)
[Mod.DamageScale]
multiplier=1.5

[Console]
; Key that opens the debug console (DebugConsole mod): an Unreal key name such as Tilde, F12, Insert
; (added with the default Tilde when missing; read while the game runs)
Key=Tilde

[Online]
; Lobby server for online multiplayer games (added with this default when missing); players who want to
; find each other must use the same one. Read it with h7.config("Online", "LobbyUrl").
LobbyUrl=https://h7-lobby.whaileee.workers.dev

[Dev]
; Write names.txt / objects.txt to dumps\ at startup (default 1)
DumpSDK=1
; Engine development only (default 0): raw layout snapshots used for engine calibration
DumpProbe=0
; Engine development only (default 0): the unpacked game executable (~100 MB)
DumpImage=0

Reference

The complete reference, with every parameter, return value and error, is in the API reference (in this repository: docs/reference/). A summary:

h7

h7.version Engine version string
h7.log/warn/error(...), h7.log_lines([after]) Logging; recent log lines (the debug console shows them)
h7.config(section, key [, default]) A value from H7ModEngine.ini (Configuration)
h7.clipboard([text]) Read the Windows clipboard's text, or put text on it
h7.first, h7.find_all, h7.find, h7.find_class, h7.enum, h7.properties, h7.functions, h7.signature Looking things up
h7.to_table(value) Plain Lua copy of a struct, array or object
h7.clone, h7.new_object, h7.package, h7.name, h7.can_create, h7.flags Creating objects
h7.hook(func, fn [, "pre" \| "post"]), h7.hook_post(func, fn), h7.unhook(id) Hooks
h7.on(event, fn), h7.off(id) Events
h7.after(seconds, fn), h7.every(seconds, fn), h7.cancel(id) Timers
h7.mods() {name, version, author, description, category, enabled, loaded, hidden, settings} for every installed mod (settings: how many it declares) (enabled but not loaded: it failed to load, see the log)
h7.set_mod_enabled(name, on) Turn a mod on or off; saved in the ini, applied on the next frame
h7.mod_settings(name), h7.set_mod_setting(name, key, value) A mod's settings with their values; change one (see Settings)
h7.reload() Unload and reload all mods (on the next frame)
h7.exec(command) Run a console command
h7.http.request{...}, h7.http.get, h7.http.post, h7.http.cancel(id) Networking: HTTP(S) requests in the background
h7.json.encode(value [, pretty]), h7.json.decode(text), h7.json.array([t]), h7.json.null JSON
h7.net.local_ip(), h7.net.upnp_add(port, protocol [, fn]), h7.net.upnp_remove(port, protocol [, fn]) This PC's LAN address; router port mapping
h7.net.set_lan_host(result, ip, port) Point a server-browser entry at a host (joinable through the game's own join)
h7.net.responder_start(port, key), h7.net.responder_stop() Answer the online lobby's TCP challenge (host verification) on a background thread
h7.net.udp_ports(), h7.net.firewall_check(protocol, port, fn) The game's open UDP sockets; whether Windows Firewall lets inbound traffic reach the game
h7.crypto.sha256(data), h7.crypto.hmac_sha256(key, msg), h7.crypto.random_hex(bytes) Hex SHA-256 / HMAC-SHA256; secure random hex (synchronous)
h7.game_content(fn) The community content packs in CookedPC (edition, per-pack version hashes, fingerprint)

Per mod: mod.name, mod.version, mod.author, mod.dir, mod.require(name), mod.save(table), mod.load(), and an optional global function on_unload().

Example mods (in mods\; all except HelloWorld ship disabled; turn them on in the Mods menu):

Mod Shows
DebugConsole The in-game Lua console: drawing on the Canvas every frame, typed text from Console.InputChar, reading the ini, listing exec functions by reflection
ModManager The main menu's Mods entry, the mod list and the settings controls: adding Flash clips to a game window and reading mouse input
OnlineLobby Online multiplayer through the community lobby (enabled by default): changing hook arguments, HTTP + JSON, UPnP, the lobby's host verification (h7.crypto, h7.net.responder_*), writing class defaults, reusing the map selection window as a game browser (with map pictures), joining through the game's own code, asking with the game's own popup
HelloWorld Events, timers, saved data, on_unload
DailyGold Post-hook on a game function; finding objects; calling functions
CombatXP Replacing a return value
DamageScale Reading hook arguments (including an out parameter) and calling methods on them
HeroMovement Filtering by owner; caching per object in a hot hook

Limitations

  • Built for Heroes VII v2.2.1 (64-bit). Other versions may not work.
  • Mods change the local game only. Multiplayer is untested and will likely desync when a mod changes gameplay.
  • Native functions: hooks get no arguments and can't block.
  • New mod folders need h7.reload() or a restart.
  • Flash objects (GFxObject) returned by GetObject/AttachMovie are freed by the engine's garbage collector when no UnrealScript object holds them. Keep the clip's name and call GetObject again when you need it, instead of storing the object.
  • GetObject only reaches display objects (named clips, buttons, labels) and objects created from Unreal (CreateObject/CreateArray); members holding arrays or objects created by the movie's own ActionScript return nil.
  • The game's Flash menus cache every translation they ask for (H7FlashMovieCntl.FlashLocalize) for the rest of the session, so changing a translation through a hook also changes it everywhere else that key is used.
  • Mods run on the game thread: a slow hook slows the game down.
  • In the Mods menu the selection goes back to the first row after a mod is enabled or disabled (the list is rebuilt; its rows can't be selected from Lua).