Skip to content

h7 library

The global h7 table holds the engine's API. It is shared by every mod and by exec scripts.

Parameters called class take a class name ("H7AdventureHero" or "MMH7Game.H7AdventureHero") or a class object; func takes a function path ("MMH7Game.H7Calendar.NextDay", optionally prefixed with "Function ") or a function object.

Engine

h7.version

h7.version → string

The engine version, e.g. "0.1.0". Use it to check for features your mod needs.

Logging

All logging goes to <Game>\H7ModEngine\logs\engine.log (the previous run is kept in engine.prev.log). Each line starts with a timestamp and the name of the mod that wrote it, or [lua] for code that runs outside any mod (exec scripts).

h7.log

h7.log(...: any)
print(...: any)

Writes its arguments, converted with tostring and separated by tabs, as one line:

h7.log("Hero", hero, "has", 12, "movement points")
-- [19:32:14.962] [MyMod] Hero  H7AdventureHero sce_sephinroth.TheWorld.PersistentLevel.H7AdventureHero_0   has 12  movement points

print is the same function.

h7.warn

h7.warn(...: any)

Like h7.log, with the line prefixed WARNING.

h7.error

h7.error(...: any)

Like h7.log, with the line prefixed ERROR. It only logs; it does not raise an error or stop your code. To stop with an error, use Lua's error("message").

h7.log_lines

h7.log_lines(after?: int) → lines: string[], newest: int

The most recent engine log lines, for showing the log in the game (the DebugConsole mod uses it). Lines are numbered from 1 since the game started; the engine keeps the last 2000 in memory.

Parameter
after Return only lines numbered above this (default 0: every kept line)

Returns lines, a sequence of strings as written to the log but without the timestamp (e.g. [MyMod] message; one entry may contain newlines, such as an error with its stack trace), and newest, the number of the newest line. Pass newest back as after to get only the lines added since.

local last = 0
h7.every(1, function()
    local new
    new, last = h7.log_lines(last)
    for _, line in ipairs(new) do
        if line:find("ERROR", 1, true) then --[[ ... ]] end
    end
end)

Finding objects

h7.first

h7.first(class: string | Class) → Object?

The first live instance of class or one of its subclasses, or nil if there is none. Class default objects (Default__...) and archetypes (templates stored in content packages) are skipped.

Parameter
class Class name or class object

Errors: unknown class '<name>' if a class name doesn't exist.

local controller = h7.first("H7AdventureController")
if controller then h7.log(controller:GetLocalPlayer(true):GetName()) end

"First" means lowest in the engine's object table, which usually but not always is the one created first. When there can be several instances (heroes, players, armies), use h7.find_all and pick the one you want.

h7.find_all

h7.find_all(class: string | Class, include_templates?: bool) → Object[]

Every live instance of class or its subclasses, as a sequence (empty when there are none).

Parameter
class Class name or class object
include_templates true to include class default objects and archetypes. Default false

Errors: unknown class '<name>'.

for _, hero in ipairs(h7.find_all("H7AdventureHero")) do
    h7.log(hero:GetName(), hero:GetLevel())
end

-- every creature archetype the game has loaded
local creatures = h7.find_all("H7Creature", true)

find_all scans the whole engine object table. Call it when you need it, not every frame, and cache the result while it stays valid (until the next map_loaded, for objects in a map).

h7.find

h7.find(path: string) → Object?

The object with the given path name, or nil. The path may be prefixed with the object's class to tell apart objects with the same path.

Parameter
path "Package.Outer.Name", or "ClassName Package.Outer.Name"
local calendar_class = h7.find("Class MMH7Game.H7Calendar")
local next_day = h7.find("Function MMH7Game.H7Calendar.NextDay")
local pixie = h7.find("Creature_Pixie.A_CreaturePixie")   -- an archetype in a content package

dumps\objects.txt (written at startup) lists every object's path.

h7.find_class

h7.find_class(name: string) → Class?

The class called name ("Package.Class" or the bare "Class"), or nil.

local hero_class = h7.find_class("H7AdventureHero")
h7.log(hero_class:get_super())   -- Class MMH7Game.H7EditorHero

h7.enum

h7.enum(name: string) → { [string]: int }

A table mapping each name of the UnrealScript enum name to its numeric value (0, 1, ...). Enum values read from properties and function results are plain integers; use this table to compare them or pass them.

Parameter
name The enum's name ("EUnitType") or path ("MMH7Game.H7StructsAndEnumsNative.EUnitType")

Errors: unknown enum '<name>'.

local UnitType = h7.enum("EUnitType")
if unit:GetEntityType() == UnitType.UNIT_HERO then ... end

Reflection

h7.properties

h7.properties(x: string | Object | Struct) → { name, type, type_name, owner, offset }[]

Every property of a class or struct, inherited ones included.

Parameter
x A class or struct name; an object (lists its class); a class, struct or function object; or a struct reference
Field
name Property name, as used from Lua
type The engine's property type, e.g. IntProperty, ObjectProperty, ArrayProperty, StructProperty
type_name The value's type as UnrealScript writes it (as in h7.signature): int, H7Creature, Vector, array<H7BaseCreatureStack>
owner The class or struct that declares it
offset Byte offset inside the object or struct

Passing a function object lists its parameters and local variables. Properties of types the engine doesn't support are left out.

Errors: unknown class/struct '<name>'.

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

h7.functions

h7.functions(x: string | Object | Struct) → { name, type, owner, flags }[]

Every function of a class (or struct), inherited ones included. x takes the same values as in h7.properties.

Field
name Function name
type The function object's class ("Function")
owner The class that declares it. A function overridden in a subclass appears once per declaring class
flags The engine's function flags (bit field: 0x400 native, 0x800 event, 0x2000 static, ...)

Errors: unknown class/struct '<name>'.

h7.signature

h7.signature(func: string | Function) → string

The function's declaration in UnrealScript syntax: static / native / event qualifiers, return type, full path and parameters with out and optional.

Errors: function '<path>' not found (use Package.Class.Function); expected a Function object.

h7.log(h7.signature("MMH7Game.H7GameProcessor.CalculateDamage"))
-- function void MMH7Game.H7GameProcessor.CalculateDamage(out H7CombatResult Result, int resultIdx, bool forecast)

Check the signature before hooking a function: native functions can be hooked but give the hook no arguments and can't be blocked (see h7.hook).

h7.to_table

h7.to_table(value: any) → table | any

A plain Lua copy of an engine value, independent of the engine:

value Result
Struct reference Table of its fields
Array reference Sequence of its elements
Object reference Table of the object's properties
Anything else value itself

Structs and arrays inside are copied recursively; objects inside stay object references (they are not expanded, since object graphs loop). Use it to log a value, or to keep hook arguments after the hook returns.

h7.hook_post("MMH7Game.H7GameProcessor.CalculateDamage", function(proc, args)
    last_result = h7.to_table(args.Result)    -- args.Result itself is invalid after the hook
end)

Hooks

A hook runs a Lua function whenever a game function is called, from UnrealScript or from the engine's C++ code. See Hooks in the guide for what hooks see and how to keep them fast.

h7.hook

h7.hook(func: string | Function, fn: function(self, args), when?: "pre" | "post") → id: int

Adds a hook on func and returns its id (an integer, for h7.unhook).

Parameter
func Function path or function object
fn The hook function, see below
when "pre" (default): before the function runs; "post": after it, like h7.hook_post

A pre-hook is called as fn(self, args):

Argument
self The object the function was called on
args The function's parameters by name, readable and writable (a struct reference). nil for native functions

Return true (the boolean) to block the call: the original function doesn't run and its caller gets a zero return value (0, false, "", None). Post-hooks still run. Any other return value lets the call proceed. Changing args changes what the function receives.

Errors: the function isn't found; could not hook <path>.

-- No hero may visit obelisks
h7.hook("MMH7Game.H7Obelisk.OnVisit", function(obelisk, args)
    return true
end)

Errors inside fn are logged with a stack trace and the call continues as if the hook had returned nothing; the hook stays installed.

Hooks on native functions (native in h7.signature) receive args = nil and can't block; the engine logs a note when you add a pre-hook to one. Latent and iterator functions only report calls made from C++.

h7.hook_post

h7.hook_post(func: string | Function, fn: function(self, args, ret)) → id: int

Adds a hook that runs after func has run. Same as h7.hook(func, fn, "post"). fn is called as fn(self, args, result):

Argument
self The object the function was called on
args The parameters after the call, out parameters included (nil for native functions)
result The return value. Not passed for functions that return nothing

Return a non-nil value to replace the return value; return nothing to leave it. A replacement of the wrong type is logged as an error and the original value is kept.

h7.hook_post("MMH7Game.H7CombatArmy.GetKilledCreaturesXP", function(army, args, xp)
    if xp > 0 then return math.floor(xp * 1.5 + 0.5) end
end)

Several hooks on one function all run in the order they were added: every pre-hook, then (unless blocked) the function, then every post-hook. When two post-hooks replace the result, the later one wins and receives the earlier one's value as result.

h7.unhook

h7.unhook(id: int)

Removes the hook id. Unknown ids are ignored. Hooks are removed automatically when their mod unloads.

Events and timers

See Events and timers for the list of events and how timers are scheduled.

h7.on

h7.on(event: string, fn: function(...)) → id: int

Calls fn(...) every time event fires, with the event's arguments. Returns the handler's id. Handlers run in the order they were added. Any event name is accepted, so a misspelt name simply never fires.

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

h7.off

h7.off(id: int)

Removes the event handler id. Unknown ids are ignored.

h7.after

h7.after(seconds: number, fn: function()) → id: int

Calls fn() once, seconds of game time from now (on the first frame at or after that time). Returns the timer's id.

h7.after(5, function() h7.log("5 seconds later") end)

h7.every

h7.every(seconds: number, fn: function()) → id: int

Calls fn() every seconds of game time until cancelled. The first call is seconds from now. At most one call per frame: a timer shorter than a frame runs once per frame. seconds of 0 or less makes a one-shot timer, like h7.after.

h7.cancel

h7.cancel(id: int)

Cancels the timer id. Unknown ids (including timers that already fired) are ignored. A timer may cancel itself.

local n, id = 0, nil
id = h7.every(1, function()
    n = n + 1
    if n == 10 then h7.cancel(id) end
end)

Creating objects

See Creating objects in the guide for how copies interact with the garbage collector, lazily loaded references, localisation and save games.

h7.clone

h7.clone(source: Object, name: string, outer?: Object, flags?: int) → Object, created: bool

Creates a copy of source: an object of the same class with every property copied from it.

Parameter
source The object to copy, usually an archetype (a template such as Creature_Pixie.A_CreaturePixie)
name Name of the new object
outer The object or package that contains it. Default: the transient package. Use h7.package for stable paths
flags Extra object flags to add, e.g. h7.flags.RootSet
Returns
object The copy
created true if it was created now; false if outer already held an object called name of the same class, which is returned instead

The copy keeps the source's Public, Standalone and Archetype flags, so a copy of an archetype is an archetype too. Returning an existing copy makes mod code safe to run again after a hot reload.

Errors: <path> already exists with another class; could not create <class> '<name>': <reason> (for example when h7.can_create() is false).

local pkg = h7.package("MyMod")
local sprite = h7.clone(h7.find("Creature_Pixie.A_CreaturePixie"), "A_CreatureFrostSprite", pkg, h7.flags.RootSet)
sprite.mHitpoints = 20

h7.new_object

h7.new_object(class: string | Class, options?: table) → Object

Creates a new object of class. options is a table with any of:

Field
name Object name. Default: a generated unique name
outer Containing object or package. Default: the transient package
template Object to copy property values from. Default: the class defaults
flags Object flags (replaces the default Public + Standalone)
archetype true to add the Archetype flag
replace true to reconstruct an existing object with the same name in place (same address, properties reset) instead of failing

Errors: unknown class '<name>'; <path> already exists (pass replace = true to reconstruct it); could not create <class> '<name>': <reason>.

local copy = h7.new_object("H7Creature", { name = "A_MyCreature", outer = h7.package("MyMod"),
                                           template = h7.find("Creature_Pixie.A_CreaturePixie"), archetype = true })

h7.package

h7.package(name: string) → Package

The top-level package called name, created if it doesn't exist. Objects inside it have stable paths (MyMod.A_CreatureFrostSprite), which is what save games store.

Errors: could not create package '<name>': <reason>.

h7.name

h7.name(str: string) → string

Adds str to the engine's name table (if it isn't there yet) and returns it. Writing a name property does this for you; call it yourself when a name must exist before the game looks it up.

Errors: could not create name '<str>'.

h7.can_create

h7.can_create() → bool

true when the engine found and verified the game functions object creation needs. When false, the reason is in the log and h7.clone, h7.new_object, h7.package (for a new package) and h7.name (for a new name) fail.

h7.flags

h7.flags.Public | .Standalone | .Transient | .Archetype | .RootSet → integer

Object flags for h7.clone and h7.new_object. Combine them with |.

Flag Value Meaning
Public 0x0000000400000000 Visible outside its package
Standalone 0x0008000000000000 Kept while its package is loaded, even when unreferenced
Transient 0x0000400000000000 Never saved
Archetype 0x0000000000000400 A template other objects are created from
RootSet 0x0000000000004000 Never garbage collected

Mods

h7.mods

h7.mods() → { name, version, author, description, category, enabled, loaded, hidden, settings }[]

Every installed mod, in load order.

Field
name, version, author, description, category From the manifest (empty strings when not set)
enabled Whether the mod is turned on (manifest enabled, overridden by the player's choice in H7ModEngine.ini)
loaded Whether it is loaded. enabled but not loaded: it has no main.lua, or is about to load on the next frame. A mod whose main.lua raised an error still counts as loaded (see the log)
hidden Manifest hidden: not listed in the Mods menu
settings How many settings it declares

h7.set_mod_enabled

h7.set_mod_enabled(name: string, on: bool)

Turns the mod name on (on truthy) or off. The choice is saved in H7ModEngine.ini and the mod loads or unloads at the start of the next frame, after which mod_toggled fires. A mod may disable itself.

Errors: unknown mod '<name>'.

h7.mod_settings

h7.mod_settings(name: string) → setting[]

The settings the mod name declares, in manifest order, each with its current value:

Field
key, label, description As declared (label defaults to key)
type "bool", "number", "choice" or "text"
value Current value
default Declared default
min, max, step Numbers only, when declared
options Choices only: the allowed values
reload Whether changing it restarts the mod
control Numbers only, when declared: "slider", "input" or "arrows"

Errors: unknown mod '<name>'.

h7.set_mod_setting

h7.set_mod_setting(name: string, key: string, value: any) → any

Changes a setting of mod name, exactly like the Mods menu does: the value is validated (numbers are clamped to min..max and snapped to step), saved in H7ModEngine.ini, and setting_changed fires if it changed. Returns the value as stored. A mod changes its own settings with mod.settings.key = value.

Errors: unknown mod '<name>'; mod <name> has no setting '<key>'; invalid value for <name>.<key> (<type>) (e.g. a string for a number, or a value not in options).

h7.reload

h7.reload()

At the start of the next frame, unloads every mod, scans the mods folder again (picking up new mod folders) and loads every enabled mod; then mods_loaded fires.

Console

h7.exec

h7.exec(command: string) → string

Runs an Unreal console command through the local player controller (PlayerController.ConsoleCommand) and returns its output. Which commands work depends on the game's (shipping) build.

Errors: no PlayerController exists yet (very early during startup).

h7.clipboard

h7.clipboard() → string
h7.clipboard(text: string) → bool

Without an argument, returns the text on the Windows clipboard ("" when it holds no text). With text, puts it on the clipboard and returns whether that worked. The debug console uses it for Ctrl+V / Ctrl+C.

Networking

Network functions return at once (except h7.net.responder_*, h7.net.local_ip and h7.crypto.*, which are synchronous); the work runs on background threads and fn is called with the result on the game thread on a later frame, as part of your mod (its log lines and errors carry your mod's name). When your mod unloads before the result arrives, fn is not called. Verified in game (JSON, h7.net.local_ip, HTTPS GET) and outside it (build.bat nettest online, which also covers POST, errors and UPnP); UPnP from inside the game is not verified yet.

h7.http.request

h7.http.request(options: table, fn?: function(res: table)) → id: int

Sends an HTTP or HTTPS request. options:

Field
url Required; http:// or https:// (anything else raises an error)
method "GET" (default), "POST", "PUT", "DELETE", ...
headers Table of header name → value
body A string, or a table that is sent as JSON (with Content-Type: application/json unless you set one)
timeout Seconds for each phase (resolve, connect, send, receive); default 10

res fields: ok (a response arrived), status (e.g. 200; 0 when ok is false), body (string, up to 16 MB), headers (lower-case names; repeated headers joined with ", ") and error (a message when ok is false). HTTP errors such as 404 are ok = true. Uses Windows' own HTTP stack (system proxy and certificates).

h7.http.request({ url = "https://example.com/games/42", method = "DELETE", headers = { Authorization = "Bearer " .. token } },
    function(res) h7.log("deleted: " .. res.status) end)

h7.http.get

h7.http.get(url: string, fn?: function(res: table), headers?: table) → id: int

Shorthand for a GET h7.http.request.

h7.http.post

h7.http.post(url: string, body: string | table, fn?: function(res: table), headers?: table) → id: int

Shorthand for a POST h7.http.request; a table body is sent as JSON.

h7.http.cancel

h7.http.cancel(id: int)

Forgets request id: its fn won't be called. The request itself still completes in the background. Unknown ids are ignored. Works for h7.net.upnp_* ids too.

h7.json.encode

h7.json.encode(value: any, pretty?: bool) → string

Encodes nil, booleans, numbers, strings, h7.json.null and tables. A table whose keys are exactly 1..n is an array, as is one marked with h7.json.array; others are objects with keys in sorted order (number keys become strings), so the same data always gives the same text. pretty indents by two spaces. Raises an error for functions, userdata, NaN, infinity and nesting deeper than 100 (cyclic tables).

h7.json.decode

h7.json.decode(text: string) → value: any, error?: string

Parses JSON. Objects become tables, arrays become sequences marked as arrays, null becomes h7.json.null, whole numbers become integers. Invalid JSON returns nil and a message with the position.

local data, err = h7.json.decode(res.body)
if not data then return h7.warn("bad reply: " .. err) end

h7.json.array

h7.json.array(t?: table) → table

Marks t (or a new empty table) so it encodes as a JSON array even when empty, and returns it.

h7.json.null

h7.json.null → table

The value JSON null decodes to, and encodes from. Compare with ==.

h7.net.local_ip

h7.net.local_ip() → string?

This PC's LAN address (the one Windows uses to reach the internet), e.g. "192.168.1.20"; nil when offline. No packets are sent.

h7.net.upnp_add

h7.net.upnp_add(port: int, protocol: string, fn?: function(res: table), description?: string) → id: int

Asks the router (UPnP IGD) to forward port (protocol "UDP" or "TCP") to this PC's same port. The engine talks to the router itself (SSDP discovery from this PC's LAN address, then SOAP), not through Windows' UPnP component, which can miss the router on PCs with virtual network adapters. If the port is already forwarded to this PC (the game's own networking maps UDP 27015 as "Heroes VII ..."), nothing is changed and res.existing is true: don't remove such a mapping later. res: ok, existing, external_ip (the router's public address), internal_ip, error (e.g. the port is forwarded to another PC, or no router answered). Usually takes about a second, up to ~8 s when no router answers. description is shown in the router's list (default "Heroes VII (H7ModEngine)"). Verified in game on the user's router (2026-11-05). The game itself only listens on UDP (27015); a TCP mapping of the same port is for h7.net.responder_start, which the OnlineLobby mod maps alongside.

h7.net.upnp_remove

h7.net.upnp_remove(port: int, protocol: string, fn?: function(res: table)) → id: int

Removes the router's mapping of port/protocol. res: ok, error.

h7.net.set_lan_host

h7.net.set_lan_host(result: OnlineGameSearchResult, ip: string, port: int) → bool

Makes a server-browser entry point at host ip:port the way a LAN search result does, so the game's own join (JoinOnlineGame → GetResolvedConnectString → start ip:port) connects there. result is an element of an array<OnlineGameSearchResult>, typically H7MultiplayerGameManager.SearchResults after :add(). The engine builds the game's native session record (0x38 bytes on v2.2.1) and stores it in PlatformData; the game deletes it when the session ends. Raises an error on game builds whose record layout doesn't match. Used by the OnlineLobby mod. Verified in game: a join attempt to an unreachable host times out cleanly (2026-11-05).

h7.net.responder_start

h7.net.responder_start(port: int, key: string) → ok: bool, error?: string

Answers the online lobby's TCP challenge on port from a background thread (H7Lobby host verification). The listener accepts connections on every interface and keeps answering while the game window is in the background and the game thread is paused (protocol and threat model: docs/ONLINE.md in the repository). One connection at a time, each answered at most once:

Bytes
in 43 "H7LOBBY 1 " + 32 lowercase hex characters (the nonce) + "\n"
out 65 h7.crypto.hmac_sha256(key, nonce) + "\n"

Anything else, or nothing within 2 s, is closed without a reply. Synchronous; returns true, or false and the reason ("TCP port 27015 is already in use", ...). Calling it again while it runs on the same port only swaps the key; another port moves it. The responder belongs to the calling mod and stops when that mod unloads (hot reload never leaves the port held). Use it with the lobby's protocol only: key is the challenge key derived from your registration secret (hmac_sha256(token, "H7LOBBY challenge key")), never the secret itself. Windows may ask the player once whether the game may accept connections. Verified offline (build.bat nettest); not yet in game.

h7.net.responder_stop

h7.net.responder_stop()

Stops the challenge responder and frees its port (waits at most ~2 s for a connection being answered). Harmless when it isn't running.

h7.net.udp_ports

h7.net.udp_ports() → { {ip: string, port: int}, ... }

The IPv4 UDP sockets the game process has open (ip is "0.0.0.0" for all addresses). A hosting game's listen socket is among them once the lobby map has loaded; it is on 27015 unless another program holds that port, in which case UE3 takes the next free one. Synchronous.

h7.net.firewall_check

h7.net.firewall_check(protocol: string, port: int, fn: function(res: table)) → id: int

Whether Windows Firewall lets inbound protocol ("UDP" or "TCP") traffic on port reach the game. It evaluates the enabled inbound rules of the active network profiles like Windows does (a matching block rule wins; without a rule the profile's default inbound action decides) and calls fn on a later frame with:

Field
ok The check ran (false: error says why)
enabled Windows Firewall is on for an active profile
verdict "allowed", "blocked", "no rule" (blocked by default) or "off"
rule The deciding rule's name, when there is one
profiles Active profiles, e.g. "private"

Third-party firewalls are not seen. Also logged as Firewall: inbound ....

h7.crypto.sha256

h7.crypto.sha256(data: string) → string

SHA-256 of data (any bytes) as 64 lowercase hex characters. Windows' own cryptography (CNG); synchronous.

h7.crypto.hmac_sha256

h7.crypto.hmac_sha256(key: string, message: string) → string

HMAC-SHA256 of message under key (both any bytes; a hex string is used as its characters, not decoded), as 64 lowercase hex characters.

local ckey = h7.crypto.hmac_sha256(token, "H7LOBBY challenge key")

h7.crypto.random_hex

h7.crypto.random_hex(bytes: int) → string

bytes (1..1024) cryptographically secure random bytes as 2 * bytes lowercase hex characters, for secrets and tokens (math.random is not suitable).

h7.game_content

h7.game_content(fn: function(content: table)) → id: int

Describes the community content the game loads from the player's Documents\My Games\Might & Magic Heroes VII\MMH7Game\CookedPC folder (*.upk, *.umap). Scanned once on a background thread (about 70 ms for ~600 packages) and cached; fn gets:

Field
modded true when any package is there
packs { {name, files, version}, ... } sorted by name. Each entry of CookedPC\mods is one pack ("mods/Conflux"); every other folder is one pack ("Data", "Visuals"); loose top-level packages are their own pack. version is 8 hex digits hashed from the pack's file names, sizes and package headers: identical copies match, another release of the same mod doesn't (packages carry no version string)
fingerprint 16 hex digits over all packs; "" when unmodded
root The scanned folder
error Set if the Documents folder couldn't be found

LocalShaderCache* packages are machine-generated and skipped.

h7.game_content(function(c)
    h7.log(("%s, %d pack(s)"):format(c.modded and "modded" or "base", #c.packs))
end)

Configuration

h7.config

h7.config(section: string, key: string, default?: any) → string | any

Reads key from [section] of H7ModEngine.ini. The file is read on every call, so a value the player changes while the game runs is seen at once. Values are strings; convert them yourself (tonumber).

Returns default (nil if not given) when the key is missing or empty.

local key = h7.config("Console", "Key", "Tilde") -- the DebugConsole's key

Mod settings are better declared in the manifest (settings, shown in the Mods menu); use h7.config for options that belong in the ini itself.