Skip to content

Scripting

fed edited this page Aug 26, 2022 · 15 revisions

Introduction

This is the documentation for H2-Mod's scripting API.
This documentation is complementary to IW6x' scripting API so be sure to check out their wiki first if you are not familiar with it.

Example script

Because this is singleplayer there are no notifies like "connected" or "spawned_player" like there are in multiplayer, instead all scripts are loaded once the player has spawned which is accessible through the global player variable:

-- Script loaded and player spawned

-- Let the game's GSC scripts initialize first
game:ontimeout(function()
    player:setorigin(vector:new(10000, 10000, 10000)
end, 0)

Note that the player spawns as soon as the map loads but not after the loading screen cinematic ends so your script will start even if you aren’t yet "in-game".
To fix this wait for another server frame before running your code:

game:ontimeout(function()
    game:ontimeout(function()
        player:setorigin(vector:new(10000, 10000, 10000)
    end, 0)
end, 0)

-- This should also work fine:

game:ontimeout(function()
    player:setorigin(vector:new(10000, 10000, 10000)
end, 100)

Extra features

H2-Mod's scripting API includes features that are either not documented or don't exist on IW6x

Entity damage callbacks

You can 'hook' the game's entity damage function in order to modify the damage using the game:onentitydamage function:

game:onentitydamage(function(_self, inflictor, attacker, damage, mod, weapon, dir, hitloc)
    if (_self == player) then
        -- If the victim is you (the player) set the damage to 0
        return 0
    end
end)

Arrays

Arrays are supported and can be accessed similarly to GSC:

local ents = game:getentarray()

for i = 1, #ents do
    print(ents[i])
end

If the array is associative you can get the list of its keys using the getkeys method:

local _end = player:getplayerangles():toforward() * 10000000
local trace = game:bullettrace(player:geteye(), _end, true, player)
local keys = trace.getkeys()

for i = 1, #keys do
    print(keys[i], trace[keys[i]])
end

Structs

Finding struct field IDs

GSC structs are also supported similarly to arrays. The difference is that struct fields are accessed not using the field name but using the field name's string id which is created at compile time, so if you wanted to access a specific field of a struct you would first need to find that its id by looking at the game's decompiled gsc scripts which you can find here.
The same goes for function names and GSC script file names. Example:

In maps/favela_escape.gsc the first function we find is _ID616.

_ID616()
{
    if ( _func_039( "mission_select_cam" ) == "1" )
    {
        maps\favela_escape_mission_select_cam::_ID50320();
        return;
    }

    if ( _func_039( "beautiful_corner" ) == "1" || _func_039( "beautiful_corner_demo" ) == "1" )
    {
        maps\favela_escape_beautiful_corner::_ID616();
        return;
    }

    _func_0DB( "fx_cast_shadow", 0 );
    level._ID49010 = 0.5;
    level._ID29811 = 1;
    level._ID10497 = 0.75;
    level._ID10527 = 1.75;

    // [...]
}

If we compare this to MW2's source GSC script, which you can find here we can find that the first function (_ID616) is called main.
We can also find out that these lines in MW2:

level.friendly_baseaccuracy = 0.5;
level.respawn_friendlies_force_vision_check = true;

// so burning vehicles don't block friendly progression up the skinny streets
level.destructible_badplace_radius_multiplier		= 0.75;
//level.destructible_explosion_radius_multiplier		= 0.85;
level.destructible_health_drain_amount_multiplier	= 1.75;  // burn faster

Correspond to these in MW2CR:

level._ID49010 = 0.5;  // id for 'friendly_baseaccuracy' is 49010
level._ID29811 = 1;    // id for 'respawn_friendlies_force_vision_check' is 29811
level._ID10497 = 0.75; // id for 'destructible_badplace_radius_multiplier' is 10497
level._ID10527 = 1.75; // id for 'destructible_health_drain_amount_multiplier' is 10527

Setting struct fields

So if you wanted to change level.friendly_baseaccuracy in lua you would have to do this:

level.struct[49010] = 1.0

-- or alternatively define the field id value

friendly_baseaccuracy = 49010
level.struct[friendly_baseaccuracy] = 1.0

Note that gsc-tool does not contain built-in function and method names do you will see stuff like _func_039 instead of getdvar. You can find some function names here.
Note: you cannot create new struct fields but only modify or read existing ones, same thing for arrays

Functions

Executing functions

It is also possible to call functions that are defined inside of the game's GSC scripts using the game:scriptcall(file, name, entity, ...args) function.
Example:

-- 'main' is a known name so you don't need to use '_ID616' but for other 
-- names you must use the '_ID<id>' you find in decompiled scripts.
game:scriptcall("maps/favela_escape", "main", level)

You can also get an array of functions that are included in a GSC script:

local functions = game:getfunctions("maps/favela_escape")
functions.main(level)

Or "include" them and use them as methods:

game:include("maps/favela_escape")
level:main()
-- Calling these functions on the `game` object is the same as calling them on `level`
game:main()

Singleplayer missions are annoying if you want to create, for example, a custom gamemode so to 'disable' the mission you can use various methods.
Conveniently the game has a mode that the developers used to record cinematics like the ones you see in the main menu.
To enable this you have to set the dvar beautiful_corner to 1. This will load the map's essentials (sound, fx, anims, ...) but nothing else.
Lua scripts are loaded before the GSC scripts so to disable a map's mission you can do something like this:

game:setdvar("beautiful_corner", 1)

-- [...]

It may still be necessary to remove certain elements from the map like triggers that will trigger even if the mission isn't "running":

-- This will delete all of the map's triggers
local ents = game:getentarray()
for i = 1, #ents do
    if (ents[i].classname and ents[i].classname:match("trigger")) then
        ents[i]:delete()
    end
end

Function detours

There may still be other things that prevent you from creating your awesome gamemode, for example, enabling beautiful_corner deletes all of the enemy spawners which can make spawning enemies difficult. To prevent this you can use the game:detour function:

-- In file '_ID43797' at function '_ID44261'
-- This will replace the funciton that deletes spawners with an empty function that does nothing instead
local _ID44261_hook = game:detour("_ID43797", "_ID44261", function() end)

-- Disable hook
_ID44261_hook.disable()
-- Enable hook
_ID44261_hook.enable()