Modifying WarCraft 3 maps has a long iteration period due to closing WC3, changing your code and restarting WC3. JHCR (Jass hot code reload) tries to improve this cycle by replacing the slow actions of closing and starting WC3 with a hopefully faster compiler which pushes your code changes directly into an already running WC3.
But how could something like that even work? The answer is multi faceted. So let’s start with some very basic mockups.
Let’s say we want to reload a single function, how would that look like in code?
function foo takes nothing returns nothing
if function_was_reloaded("foo") then
// somehow execute the new body
// call BJDebugMsg("New Foo")
else
// old body of foo
call BJDebugMsg("Foo")
endif
endfunction
While this has many holes it’s conceptually correct already. But for the sake of easy implementation we split up the function into two functiosn, the original function but with a new name and a new function but with the old name:
function JHCR_foo takes nothing returns nothing
call BJDebugMsg("Foo")
endfunction
function foo takes nothing returns nothing
if function_was_reloaded("foo") then
// somehow execute the new body
// call BJDebugMsg("New Foo")
else
call JHCR_foo()
endif
endfunction
This way we don’t have to change other functions calling
foo
when we update it. Everything from a simple
call-statement over trigger-actions to such things as ExecuteFunc
still work just fine.
The harder part is now how to actually execute the new body of
foo
. To achieve this JHCR provides both a compiler to- and
an interpreter for a custom bytecode language which will be
superficially introduced in the next section.
The bytecode was carefully designed to strike a balance between size, parsing speed and executing speed as all of these factors play a big role in our limited WC3 environment.
For example the original foo
would be translated to this
bytecode:
fun 3 foo
lit string -2 Foo
bind string 1 -2
call -1 2 BJDebugMsg
ret nothing
But this is of course way too verbose and also not very fast to parse. So this format is only used for human consumption; to communicate with WC3 JHCR provides another format for this exact bytecode which looks like this:
| fun | | bind | |ret|
263.....21136-2.......3.....Foo201361........-2.......22-1.......2.....29139
| lit | | call |
I’ve annotated the different parts to show the opcode they represent.
This is way easier to parse programatically and takes up less space.
Every bytecode uses a fixed number and we encode numbers in a fixed
width with added padding to achieve a good tradeoff between size and
parsing speed. For fast parsing we use the fact that S2I("123asdf") == S2I(123)
so we can move in well
defined chunks over the input string.
There are plenty more opcodes ofcourse but we’ll save them for a future post.
As we’ve seen previously we took special care to preserve the original name of our function but this was mostly to integrate our system into the maps script. Now we will talk about integrating the maps script into JHCR.
Take the aboves bytecode as an example: we call the BJ-function BJDebugMsg
in there. How do we actually achieve
this? As JHCR takes both common.j
and
Blizzard.j
at compile time we assign each function an
unique id (BJDebugMsg
has id 2 in our
example). Now, to be able to call this function we generate a huge
if-then-else block for each id at compile time to call out to different
functions. If we limit ourself to only three functions, that is DisplayTimedTextToPlayer
, BJDebugMsg
and foo
we can look at the
code JHCR generates below.
function JHCR_Auto_call_predefined takes integer JHCR_reg,integer JHCR_i,integer JHCR_ctx returns nothing
if (JHCR_i < 2) then
call DisplayTimedTextToPlayer (JHCR_Table_get_player (JHCR_Context_bindings[JHCR_ctx],1),JHCR_Table_get_real (JHCR_Context_bindings[JHCR_ctx],2),JHCR_Table_get_real (JHCR_Context_bindings[JHCR_ctx],3),JHCR_Table_get_real (JHCR_Context_bindings[JHCR_ctx],4),JHCR_Table_get_string (JHCR_Context_bindings[JHCR_ctx],5))
else
if (JHCR_i < 3) then
call BJDebugMsg (JHCR_Table_get_string (JHCR_Context_bindings[JHCR_ctx],1))
else
call foo ()
endif
endif
endfunction
This is ofcourse quite a mouthfull but it all follows the same
pattern. The first parameter is the register the return-value of the
called function is stored in. but since all our functions return nothing
it wont be used here. The next parameter is
the id of the function we want to call. As you can see we do a bunch of
comparisons to get to the correct function. The next parameter is a
struct called context
in which we store a bunch of
information for the interpreter, most importantly here the parameters we
want passed to the function we want to call. Those parameters are passed
via the bind
opcode which you can see just before the
call
opcode in our small example above.
We also need to be able to read and write globals from our
interpreter and we deploy a similar technique as we did for functions.
For each available type in JASS (handle
, integer
, unit
, etc.) we
generate two functions to read and write a specific global variable of
that type. As with functions we assign each global variable a per-type
unique id on which we do a bunch of comparisons to set or read the
correct value. Let’s again only look at a very small input script with
only two global variables bj_MAX_PLAYERS
and
bj_forLoopAIndex
where bj_MAX_PLAYERS
is declared constant. JHCR will
generate the following functions:
function JHCR_Auto_get_global_integer takes integer JHCR_i returns integer
if (JHCR_i < 2) then
return bj_MAX_PLAYERS
else
return bj_forLoopAIndex
endif
endfunction
function JHCR_Auto_set_global_integer takes integer JHCR_i,integer JHCR_v returns nothing
if (JHCR_i < 2) then
return
else
set bj_forLoopAIndex = JHCR_v
endif
endfunction
Note the then-case in JHCR_Auto_set_global_integer
:
since bj_MAX_PLAYERS
is constant we actually
can’t set it. But both can of course be read. A similar approach is used
for global arrays.
To actually reload changed functions JHCR read the new script file
and checks for each function if the hash of the function has changed. If
that happens the bytecode like above is created and written to a file to
be loaded by the Preload
-native. For example
the changed foo
function would generate the following
preload-file:
function foo takes nothing returns nothing
call BJDebugMsg("New foo")
endfunction
function PreloadFiles takes nothing returns nothing
call SetPlayerTechMaxAllowed (Player (0),1,1)
call BlzSetAbilityTooltip ('Agyv',"263.....21136-2.......7.....New foo201361........-2.......22-1.......2.....29139",0)
call SetPlayerTechMaxAllowed (Player (0),2,0)
endfunction
The generated bytecode is of course very similar to the previously shown since we only changed one string literal after all. Now what happens with this next is topic of a future post. We will end this little post here even though it only scratches the surface of what JHCR has to do. I hope you enjoyed this quick overview of JHCR.
lep . blog