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
- Your first mod
- Mod folder and manifest
- Finding what to change
- Engine objects
- Values and types
- Looking things up
- Hooks
- Events and timers
- Saving data
- Logging and debugging
- Configuration
- Reference
- Limitations
- Copy
dinput8.dllinto<Game>\Binaries\Win64\. - Copy the
H7ModEnginefolder into the game folder (<Game>\H7ModEngine\). - 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 |
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)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.
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
MMH7Gamepackage 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
execfolder: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 (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 = 2Writing 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
outparameters, 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.
| 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 downAssigning 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.
| 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.
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...) areDynLoadObjectPropertyfields that the game loads from the object's package when first needed and releases again withDel*functions. A copy has no package, so it can never load them: callsource:DynLoadObjectProperty("mVisuals")(orDynLoadObjectArrayPropertyfor arrays) on the source before cloning, and stop the game'sDel*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.
mNameInston units, or hook theGetNameof 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.
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.
selfis the object the function was called on.argsgives access to the function's parameters by name:args.Result,args.resultIdx. You can also assign them (args.Amount = 10in a pre-hook changes what the function receives).outparameters can be read and written the same way.- In a post-hook,
argsshows the values after the function ran, andresultis its return value (absent for functions that return nothing). argsand 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.ApplyDamageis separate fromH7Unit.ApplyDamage). - Native functions (implemented in C++,
nativeinh7.signature) can be hooked, butargsisniland they can't be blocked. - Latent and iterator functions (
Sleep,foreachiterators) 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)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.
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.
| 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.
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.
<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=0The 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 |
- 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 byGetObject/AttachMovieare freed by the engine's garbage collector when no UnrealScript object holds them. Keep the clip's name and callGetObjectagain when you need it, instead of storing the object. GetObjectonly 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 returnnil.- 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).