lep / jhcr

A compiler to allow hot code reload in WarCraft 3
https://www.hiveworkshop.com/threads/jass-hot-code-reload.313811/
GNU Lesser General Public License v3.0
31 stars 2 forks source link

Use gamecache to load data via Preloader #7

Closed lep closed 1 year ago

lep commented 1 year ago

Based off of https://github.com/lep/jhcr/issues/6 I tested this under patch 1.33. But the FlushGameCache line doesn't work under 1.33. Does it really work under 1.26?

shuen4 commented 1 year ago

my fault i was expecting FlushGameCache to be similar to FlushStoredInteger to remove all save data types, so i didnt test again after changing it to FlushGameCache what actually happen: FlushStoredInteger deletes saved data FlushGameCache deletes all saved data and makes the handle no longer valid

function onEvent takes nothing returns nothing
    if HaveStoredInteger(GC, "test", "test") then
        call FlushStoredInteger(GC, "test", "test")
        call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 10, "previous onEvent failed")
    else
        call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 10, "previous onEvent successed")
    endif
    call StoreInteger(InitGameCache("test.w3v"), "test", "test", 0)
    set i = i + 1
    if i - i / 2 * 2 == 1 then // i % 2 == 1
        // crash this thread
        call I2S(1 / 0) 
    endif
    call FlushStoredInteger(GC, "test", "test")
endfunction

log:

successed
failed
successed
failed
successed
failed
// repeat N time

after change FlushStoredInteger to FlushGameCache

successed
failed
successed
successed
// repeat N time successed

going to try if adding set GC = InitGameCache("test.w3v") after FlushGameCache(GC) produce same log as using FlushStoredInteger

edit: it does produce same log as using FlushStoredInteger

shuen4 commented 1 year ago

test for modify gamecache file on 1.26a:

function init takes nothing returns nothing
    local gamecache GC = InitGameCache("test.w3v")
    call StoreString(GC, "test_missionKey", "test_valueKey", "test_value")
    call SaveGameCache(GC)
    // didnt test if this was necessary
    call ReloadGameCachesFromDisk()
    call BJDebugMsg(GetStoredString(InitGameCache("test.w3v"), "test_missionKey", "test_valueKey")) // test_value
    // copy Warcraft III\Save\Profile*\Campaigns.w3v
    // 1.32+ path (just guessing): Documents\Warcraft III\BattleNet\*\Campaigns\Classic\Campaigns.w3v
    // didnt test this too
    call ReloadGameCachesFromDisk()
    // and this
    set GC = InitGameCache("test.w3v")
    call StoreString(GC, "test_missionKey", "test_valueKey", "test_value123")
    // and this
    call SaveGameCache(GC)
    call BJDebugMsg(GetStoredString(InitGameCache("test.w3v"), "test_missionKey", "test_valueKey")) // test_value123
    // restore copied Campaigns.w3v
    call ReloadGameCachesFromDisk()
    call BJDebugMsg(GetStoredString(InitGameCache("test.w3v"), "test_missionKey", "test_valueKey")) // test_value
endfunction

~~pros: no OP limit in Preloader script (wont exceed anyways unless very very huge bytecode generated and interpreter would exceed this OP limit even Preloader script didnt)~~

cons: no longer possible for LAN reload OP limit exceed in JHCR_Init_parse would make partial reload (currently limited by bytecode reload limit (12 or 24 depend on patch) and probably wont cause this)

some features might possible to add if modify gamecache directly (if patch 1.33+ didnt cache gamecache): auto reload if detected modified (a global variable in map script to track reloaded count and a gamecache data to track how many time file generated and if detected global variable + 1 == gameacache data preform reload) warn the user if missing intermediate bytecode (global variable != gamecache data && global variable + 1 != gameacache data (currently possible but useless on patch 1.33+ due to Preloader caching))

shuen4 commented 1 year ago

by using TriggerSleepAction we can reset executed OP count but it may let other trigger running partial reloaded bytecode and passing bad value to nfunction therefore making game crashing

by using ExecuteFunc wont have such issue but it wont simple as using TriggerSleepAction some code about using ExecuteFunc to repeat a operation that usually would endup OP limit exceeded

// usual code
// OP limit = default
scope test initializer init
globals
    integer i
endglobals
private function test takes nothing returns nothing
    loop
        set i = i + 1
    endloop
endfunction
private function onEvent takes nothing returns nothing
    set i = 0
    call ExecuteFunc(SCOPE_PRIVATE + "test")
    call BJDebugMsg(I2S(i)) // OP limit exceeded  (forgot value but it was around 27000 ~ 30000)
endfunction
private function init takes nothing returns nothing
    local trigger t = CreateTrigger()
    call TriggerRegisterPlayerChatEvent(t, GetLocalPlayer(), "", false)
    call TriggerAddAction(t, function onEvent)
endfunction
endscope

// bypass OP limit
// OP limit = logically infinity but will cause stack overflow if nested too deep
scope test initializer init
globals
    integer i
endglobals
private function test takes nothing returns nothing
    loop
        exitwhen i == 9999999 // 1 more digit war3 will stack overflow due to nested ExecuteFunc
        set i = i + 1
        if i - i / 9374 * 9374 == 0 then // on 300000 opcode limit thread OP limit exceeded at 9375th loop
            call ExecuteFunc(SCOPE_PRIVATE + "test") // change to call AbilityId(SCOPE_PRIVATE + "test") to find the number
        endif
    endloop
endfunction
private function onEvent takes nothing returns nothing
    set i = 0
    call ExecuteFunc(SCOPE_PRIVATE + "test")
    call BJDebugMsg(I2S(i)) // war3 hang ~1s then display 9999999
endfunction
private function init takes nothing returns nothing
    local trigger t = CreateTrigger()
    call TriggerRegisterPlayerChatEvent(t, GetLocalPlayer(), "", false)
    call TriggerAddAction(t, function onEvent)
endfunction
endscope

// bypass a little OP limit
// OP limit = default ^ 2
scope test initializer init
globals
    integer i
endglobals
private function test takes nothing returns nothing
    loop
        set i = i + 1
        exitwhen i == 999999999
    endloop
endfunction
private function onEvent takes nothing returns nothing
    set i = 0
    loop
        exitwhen i == 999999999
        call ExecuteFunc(SCOPE_PRIVATE + "test")
    endloop
    call BJDebugMsg(I2S(i)) // war3 hang 3 min then OP limit exceeded (forgot to check the value)
endfunction
private function init takes nothing returns nothing
    local trigger t = CreateTrigger()
    call TriggerRegisterPlayerChatEvent(t, GetLocalPlayer(), "", false)
    call TriggerAddAction(t, function onEvent)
endfunction
endscope

// bypass a little more OP limit - just testing, the above one should already sufficient
// OP limit = default ^ 3
scope test initializer init
globals
    integer i
endglobals
private function test takes nothing returns nothing
    loop
        exitwhen i == 999999999
        set i = i + 1
    endloop
endfunction
private function test1 takes nothing returns nothing
    loop
        exitwhen i == 999999999
        set i = i + 1
        call ExecuteFunc(SCOPE_PRIVATE + "test")
    endloop
endfunction
private function onEvent takes nothing returns nothing
    set i = 0
    loop
        exitwhen i == 999999999
        call ExecuteFunc(SCOPE_PRIVATE + "test1")
    endloop
    call BJDebugMsg(I2S(i)) // war3 hang 5min then display 999999999
endfunction
private function init takes nothing returns nothing
    local trigger t = CreateTrigger()
    call TriggerRegisterPlayerChatEvent(t, GetLocalPlayer(), "", false)
    call TriggerAddAction(t, function onEvent)
endfunction
endscope

by default 1.26a OP limit is 300000

lep commented 1 year ago

There are many interesting possibilities when using gamecache. The greatest point to me is to remove the dumb difference when compiling jhcr. But also that -- ignoring the op limit -- we can now load bigger chunks without worrying about running out of ability ids. I'm aware of all the stuff like directly modifying a GC file or techniques to circumvent the op limit. I say one step at a time. Some parts i wrote with re-entrance in mind (i think) but not the reloading itself. I don't think it would be too bad but i haven't taken a closer look yet. I propose that i merge this as is, that is just using GC to transfer stuff. I tested it under latest bnet patch and you tested it under 1.26a. That's good enough for me right now.