Skip to content

Commit cd72733

Browse files
authored
Merge pull request #45 from MHFU-ModKit/fix/port-hit-reload
fix(framework): a re-exported hit table applies as a new export
2 parents b63e982 + 5238ac7 commit cd72733

2 files changed

Lines changed: 82 additions & 13 deletions

File tree

‎framework/lua/lib/mhfu_port.lua‎

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,10 @@ P.act_set = act_set
174174
local Port = {}
175175
Port.__index = Port
176176

177+
-- What a redefine keeps: the spawn event that bound `ent` will not fire again (ent = 0 would stop
178+
-- the brain), and the hit tables in the game belong to the entity, not to the declaration.
179+
local KEEP = { "ent", "_hit_id", "_hit_last", "_hit_cap", "_atk_caps" }
180+
177181
--- Declare a ported monster.
178182
--
179183
-- name identifier, also the hot-reload key
@@ -190,16 +194,14 @@ Port.__index = Port
190194
--
191195
-- Returns a port handle. Safe to call again (hot reload): the injector is armed once per boot.
192196
function P.define(spec)
193-
-- a redefine keeps the bound entity: the spawn event that bound it will not fire again, and
194-
-- ent = 0 would stop the brain. The rest of the state resets.
195197
local prev = P.ports[spec.name]
196198
local self = setmetatable({
197199
name = spec.name,
198200
species = spec.species,
199201
clips = spec.clips or {},
200202
moves = spec.moves or {},
201203
replace = spec.replace or {},
202-
ent = prev and prev.ent or 0,
204+
ent = 0,
203205
clip = nil, -- currently latched executor a1, nil = hands off
204206
move = nil, -- currently scripted move name
205207
pinned = nil, -- {x, y, z} while the coordinate lock is on
@@ -214,6 +216,7 @@ function P.define(spec)
214216
_native_armed = false,
215217
_req = nil, -- a play() issued through the seam, until it lands
216218
}, Port)
219+
if prev then for _, k in ipairs(KEEP) do self[k] = prev[k] end end
217220
-- declared pairs the engine enters on its own get the port's clip too (the action hook paints
218221
-- them), so the mapping holds whoever picked the move; first name wins for a pair declared twice
219222
local names = {}
@@ -511,7 +514,8 @@ end
511514
-- in the quest, but beside a native of the same species this re-skins the native too.
512515
--
513516
-- Applied once the entity is live in-area, re-checked every tick against one record and one grid
514-
-- byte, and re-applied with a log line if either changed (an overlay reload shows up here).
517+
-- byte, and re-applied with a log line if either changed (an overlay reload shows up here) or a
518+
-- re-export registers a new content id.
515519
--
516520
-- Attacks go the other way: a handler spawns an attack by id, and ATTACK_RECORD `id` at
517521
-- attack_tables.records names a volume set (VOLUME_SET), reached through the overlay's pointer
@@ -541,9 +545,7 @@ P._hit = P._hit or {} -- port name -> table, from the generated module
541545
--- x states} | nil }. Keyed by PORT name so the data module and the brain module
542546
--- can load in either order; the tick joins them.
543547
function P.hit(name, tbl)
544-
P._hit[name] = tbl
545-
local port = P.ports[name]
546-
if port then port._hit_id = nil end -- a re-export applies on the next tick
548+
P._hit[name] = tbl -- a new id applies on the next tick (hit_tick)
547549
local nsets = 0
548550
if tbl.attack_sets then for _ in pairs(tbl.attack_sets) do nsets = nsets + 1 end end
549551
log("[port:%s] hit tables registered: %s volume(s), %s grid state(s), %d attack "
@@ -776,18 +778,20 @@ local function hit_apply(port, tbl)
776778
end
777779

778780
--- Called from the tick for a live, in-area port. Applies on the first
779-
--- opportunity and whenever the live bytes stop matching.
781+
--- opportunity, on a new export id and whenever the live bytes stop matching.
780782
local function hit_tick(port)
781783
local tbl = P._hit[port.name]
782784
if not tbl then return end
783785
if port._hit_id == tbl.id and hit_intact(port, tbl) then return end
784-
local why = port._hit_id == tbl.id and "live table changed under us"
785-
or (port._hit_id and "new export" or "first contact")
786+
-- _hit_id is what is in place (nil after a failed apply, so it retries); _hit_last names
787+
-- what got in last, for the log
788+
local id = tostring(tbl.id)
789+
local why = port._hit_last == nil and "first contact"
790+
or (port._hit_last == id and "live table changed under us" or "new export")
786791
local ok, what = hit_apply(port, tbl)
787792
if ok then
788-
port._hit_id = tbl.id
789-
log("[port:%s] HIT TABLES APPLIED (%s): %s id=%s", port.name, why, what,
790-
tostring(tbl.id))
793+
port._hit_id, port._hit_last = tbl.id, id
794+
log("[port:%s] HIT TABLES APPLIED (%s): %s id=%s", port.name, why, what, id)
791795
else
792796
port._hit_id = nil
793797
if (port._hit_fail or 0) % 20 == 0 then
@@ -822,6 +826,7 @@ P._once.events = true
822826
mhfu.on_quest_targets_building(function(quest)
823827
if quest == 0 then return end
824828
for name, port in pairs(P.ports) do
829+
port._hit_last = nil -- the quest's first hit apply logs as first contact
825830
for _, victim in ipairs(port.replace) do
826831
if mhfu.quest_has(quest, victim) then
827832
local ok = mhfu.quest_replace_monster(quest, victim, port.species)

‎framework/tests/test_mhfu_port.py‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,12 @@ def memory(lua: Any) -> None:
101101
mhfu.write_u16 = function(x, v) wr(x, v); wr(x + 1, v >> 8) end
102102
mhfu.read_u32 = function(x) return mhfu.read_u16(x) | mhfu.read_u16(x + 2) << 16 end
103103
mhfu.write_u32 = function(x, v) mhfu.write_u16(x, v); mhfu.write_u16(x + 2, v >> 16) end
104+
mhfu.read_f32 = function(x)
105+
return (string.unpack("<f", string.pack("<I4", mhfu.read_u32(x))))
106+
end
107+
mhfu.write_f32 = function(x, v)
108+
mhfu.write_u32(x, (string.unpack("<I4", string.pack("<f", v))))
109+
end
104110
mhfu.mem_valid = function(x) return x >= {RAM.start} and x < {RAM.stop} end
105111
mhfu.get_screen_state = function() return 17 end
106112
mhfu.get_area_index = function() return 99 end
@@ -351,6 +357,64 @@ def test_native_seam(lua: Any) -> None:
351357
lua.execute(NATIVE)
352358

353359

360+
HIT_RELOAD = """
361+
local QUEST
362+
mhfu.on_quest_targets_building = function(fn) QUEST = fn end
363+
local P = require("mhfu_port")
364+
local HV = mhfu.addr.HIT_VOLUME
365+
-- a host hurtbox set of 3 records and one attack set of 2, each sentinel-terminated
366+
local SET, VT, ASET = ENT + 0x4000, ENT + 0x5000, ENT + 0x6000
367+
local row = mhfu.addr.SPECIES_TABLE + 75 * mhfu.addr.SPECIES.STRIDE
368+
mhfu.write_u32(row + mhfu.addr.SPECIES.HURTBOX_SET, SET)
369+
mhfu.write_u32(VT, ASET)
370+
for i = 0, 2 do mhfu.write_u16(SET + i * HV.SIZE, 1) end
371+
mhfu.write_u16(SET + 3 * HV.SIZE, 0xFFFF)
372+
for i = 0, 1 do mhfu.write_u16(ASET + i * HV.SIZE, 1) end
373+
mhfu.write_u16(ASET + 2 * HV.SIZE, 0xFFFF)
374+
local zin = P.define{ name = "zinogre", species = 75 }
375+
zin.ent = ENT
376+
-- a hot-reloaded <name>_hit.lua re-runs this; 4 volumes over a cap of 3 are truncated
377+
local function export(id, radius)
378+
local v = { 2, 0, 0, 1, 0, radius, 0, 0, 0, 0, 0, 0 }
379+
P.hit("zinogre", { species = 75, id = id, volumes = { v, v, v, v },
380+
attack_tables = { volumes = VT, n_sets = 1 },
381+
attack_sets = { [0] = { cap = 2, volumes = { v } } } })
382+
end
383+
local function radius() return mhfu.read_f32(SET + HV.RADIUS) end
384+
385+
export("599b6933", 100.0); ticks(2)
386+
assert(count("APPLIED %(first contact%).*id=599b6933") == 1 and radius() == 100.0)
387+
export("73527c5b", 200.0); ticks(2)
388+
assert(count("APPLIED %(new export%).*id=73527c5b") == 1 and radius() == 200.0)
389+
export("73527c5b", 200.0); ticks(2)
390+
assert(count("APPLIED") == 2, "the same id is not re-applied")
391+
export("599b6933", 100.0); ticks(1)
392+
assert(count("APPLIED %(new export%).*id=599b6933") == 1 and radius() == 100.0)
393+
mhfu.write_f32(SET + HV.RADIUS, 5.0); ticks(1)
394+
assert(count("APPLIED %(live table changed under us%)") == 1 and radius() == 100.0)
395+
assert(count("first contact") == 1)
396+
397+
-- a brain-mod reload redefines the port: what is in place stays in place
398+
local before = zin
399+
zin = P.define{ name = "zinogre", species = 75 }
400+
for _, k in ipairs({ "ent", "_hit_id", "_hit_last", "_hit_cap", "_atk_caps" }) do
401+
assert(zin[k] ~= nil and zin[k] == before[k], k)
402+
end
403+
ticks(2)
404+
assert(count("APPLIED") == 4, "not re-applied after a redefine")
405+
406+
-- the next quest loads the overlay afresh: its first apply is first contact again
407+
QUEST(1)
408+
mhfu.write_f32(SET + HV.RADIUS, 5.0); ticks(1)
409+
assert(count("APPLIED %(first contact%).*id=599b6933") == 2 and radius() == 100.0)
410+
"""
411+
412+
413+
def test_hit_reload(lua: Any) -> None:
414+
memory(lua)
415+
lua.execute(HIT_RELOAD)
416+
417+
354418
def test_fits_the_file_buffer() -> None:
355419
"""lua_host reads a script through one buffer and skips a bigger one at boot."""
356420
import re

0 commit comments

Comments
 (0)