• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[Lua] Simple script reloading

Status
Not open for further replies.
Level 13
Joined
Nov 7, 2014
Messages
571
warning: I've only tested this on version 1.31, it might not work on later versions (Reforge).

Introduction

When the game loads a map, at some point, it also loads and runs the map script. After the script is loaded, any further changes to it are not reflected until the map is restarted. This is very inconvenient because map restarts are not instantaneous. Many scripting languages, including Lua, have built-in functions for loading new scripts. One such function is the 'load' function (the 'loadfile' function is not available), it loads a string (chunk) and returns a function which, when called, executes the Lua code inside. Script reloading is simply loading a changed script and executing it.

Loading a Lua script from a file

We can use TriggerHappy's method for reading strings from a file. It relies on the Preloader and BlzGetAbilityTooltip native functions.

my-preload-file.pld:
JASS:
function PreloadFiles takes nothing returns nothing
    call BlzSetAbilityTooltip ('Agyv', "<put-Lua-script-here-but-make-sure-it-is-valid-Jass-string>", 0)
endfunction

loading the Lua script:
Lua:
    -- the absolute path to the preload file is: 'c:\users\<user-name>\documents\warcraft iii\custommapdata\my-preload-file.pld'
    -- documents = My Documents
    --
    local preload_file = 'my-preload-file.pld'

    Preloader(preload_file)
    local text = BlzGetAbilityTooltip(FourCC('Agyv'), 0)

Detecting script changes

See the 'watch-wc3-script-for-changes.lua' in the attached zip file. I've used Lua (and a tiny C program) for this, but there are better ways to do it (some OS API that notifies us when the file has changed, or not using Lua =)).

Running the watch script


I use a '.bat' script similar to this to start the watch script:
Code:
@echo off

set watch_script="<path-to>\watch-wc3-script-for-changes.lua"
set script_file="<path-to-the-script-file-we-want-to>\reload.lua"
set preload_file="c:\users\<user-name>\documents\warcraft iii\custommapdata\my-preload-file.pld"

lua %watch_script% %script_file% %preload_file%

Reloading the script in-game

Lua:
local sprintf = string.format

local function writefln(fmt, ...)
    print(sprintf(fmt, ...))
end

local intcc = FourCC

local function reload_script()
    local script_file = 'reload.lua'
    local preload_file = 'my-preload-file.pld'

    Preloader(preload_file)
    local text = BlzGetAbilityTooltip(intcc('Agyv'), 0)

    local fn
    local err
    local ok

    fn, err = load(text, script_file, 't', _ENV)
    if err ~= nil then
        writefln('syntax error: %s', err)
    else
        clear_all()
        ok, err = pcall(fn)
        if not ok then
            writefln('error: %s', err)
        end
    end
end

...
    -- I use the Esc key to reload the script
    local t = CreateTrigger()
    TriggerRegisterPlayerEvent(t, Player(0), EVENT_PLAYER_END_CINEMATIC)
    TriggerAddAction(t, reload_script)
...

If you are using the World Editor for writing Lua watch out for those '%' signs.

Undoing side effects

One good thing of restarting the map after a script change is that it gives us a "clean slate". Imagine we have the following script:
Lua:
CreateUnit(Player(0), FourCC('Hpal'), 0.0, 0.0, 270.0)
If we reload it, we would get a Paladin on the map. If we then change it to:
Lua:
CreateUnit(Player(0), FourCC('Ofar'), 0.0, 0.0, 270.0)
And reload it, we would get a Far Seer on the map, but the Paladin would still be there as well.

A simple but annoying way around this is to wrap the native CreateUnit (in this case) with something like this:
Lua:
local native_CreateUnit = CreateUnit
local clear_CreateUnit
do
    local xs = {}

    _ENV.CreateUnit = function(a1, a2, a3, a4, a5)
        local x = native_CreateUnit(a1, a2, a3, a4, a5)
        xs[#xs+1] = x
        return x
    end

    clear_CreateUnit = function()
        for a = #xs, 1, -1 do
            RemoveUnit(xs[a])
            xs[a] = nil
        end
    end
end

local function clear_all()
    clear_CreateUnit()
end

local function reload_script()
...
    clear_all()
    ok, err = pcall(fn)
...

We call the clear_all/clear_CreateUnit function, which effectively removes our side effects, before running the new version of the script.

I don't like Lua, can I reload Jass instead?

Yes, see Jhcr by Lep.
 

Attachments

  • lua-simple-script-reloading.zip
    2.7 KB · Views: 107

LeP

LeP

Level 13
Joined
Feb 13, 2008
Messages
539
Yes, lua is suited so much better for this than jass (but i started jhcr like years ago, actually).
I can't test it unfortunately as i don't have a lua-wc3 installed atm but from looking at the reload lua script i t seems like you just dump the whole lua file into the preload file. Have you tested how much data you can fit into one call to BlzSetAbilityTooltip?
What i do in jhcr is to check the hash of each function and see if it changed to last iteration.

Otherwise i would encourage everyone to test this kind of developtment; it's very nice with this instant feedback.
 
Level 13
Joined
Nov 7, 2014
Messages
571
Have you tested how much data you can fit into one call to BlzSetAbilityTooltip?
I threw 102462 bytes at it and it seemed to work. My guess is that blizzard converts the preload file from Jass to Lua and then executes it (is Jass always converted to Lua and executed?). I don't know if Lua has a string literal limit.

@mori also has a live reload implementation here.
Static typing and live reloading O.O? It also solves the "side effects/nuances" issue? Well that's awesome.
 

~El

Level 17
Joined
Jun 13, 2016
Messages
556
There's no string limit in Lua, nor for literals nor for strings created programmatically.

I have a WC3 Lua script which is around 600 kb in size and it still loads perfectly fine. I throw it all into a single string literal, just escaping newlines and whatnot.

It also solves the "side effects/nuances" issue? Well that's awesome.

What do you mean by side-effects/nuances? That method still requires you to jump through some hoops to make code which works well with live-reload. You need to be aware of live-reloading when making any kind of global state or registering callbacks.
 
Level 13
Joined
Nov 7, 2014
Messages
571
What do you mean by side-effects/nuances?
I meant that we were referring to same problem (for me in my first post in the "Undoing side effects" section) and you here.

That method still requires you to jump through some hoops to make code which works well with live-reload. You need to be aware of live-reloading when making any kind of global state or registering callbacks.
The setup that I use is the following: I put the 'config' and 'main' functions (required by the game), the functions that do the script reloading, as well as the native wrapping functions in the same 'main' file:
Lua:
-- native wrapping functions

local native_BlzCreateFrame = BlzCreateFrame
local native_BlzCreateSimpleFrame = BlzCreateSimpleFrame
local native_BlzCreateFrameByType = BlzCreateFrameByType
local clear_Frames
do
    local xs = {}

    _ENV.BlzCreateFrame = function(a1, a2, a3, a4)
        local x = native_BlzCreateFrame(a1, a2, a3, a4)
        xs[#xs+1] = x
        return x
    end

    _ENV.BlzCreateSimpleFrame = function(a1, a2, a3)
        local x = native_BlzCreateSimpleFrame(a1, a2, a3)
        xs[#xs+1] = x
        return x
    end

    _ENV.BlzCreateFrameByType = function(a1, a2, a3, a4, a5)
        local x = native_BlzCreateFrameByType(a1, a2, a3, a4, a5)
        xs[#xs+1] = x
        return x
    end

    clear_Frames = function()
        for a = #xs, 1, -1 do
            BlzDestroyFrame(xs[a])
            xs[a] = nil
        end
    end
end

local function wrap_native(native_name, dtor)
    local xs = {}

    local native_fn = _ENV[native_name]
    _ENV[native_name] = function(...)
        local x = native_fn(...)
        xs[#xs+1] = x
        return x
    end

    local function clear_fn()
        for a = #xs, 1, -1 do
            dtor(xs[a])
            xs[a] = nil
        end
    end

    return clear_fn
end

--  @Note: we store the native function 'CreateTrigger' in 'native_CreateTrigger' before
-- wrapping it, but we need to remember to use 'native_CreateTrigger' in the remaining lines of this file,
-- instead of the now wrapped plain 'CreateTrigger'
--
local native_CreateTrigger = CreateTrigger
local clear_Trigger = wrap_native('CreateTrigger', function(x) TriggerClearConditions(x); TriggerClearActions(x); DestroyTrigger(x) end)

local clear_Timer = wrap_native('CreateTimer', function(x) TimerStart(x, 0.0, false, nil); DestroyTimer(x); end)
local clear_CreateUnit = wrap_native('CreateUnit', RemoveUnit)
local clear_TextTag = wrap_native('CreateTextTag', DestroyTextTag)

local function clear_all()
    clear_Frames()
    clear_Trigger()
    clear_Timer()
    clear_CreateUnit()
    clear_TextTag()
end
All other code is in the file that is getting reloaded. The call to the 'clear_all' function, in effect, restarts the map, and is done before the call to the reloaded script, which "recreates the world" by calling the wrapped native functions (creating triggers with callbacks and whatnot).
 
Status
Not open for further replies.
Top