Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions docs/formats/p3rd_ai_script.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,11 @@ clip that hands to the next by `after`:

- **Clips.** The default path. A branch on something the build cannot know (a bearing, a counter,
a wait) is tried each way and the path that spawns most attacks and plays fewest clips is taken;
the others are named in `P3_OWN_BRANCH`. A looping clip, or one the handler leaves at a frame it
tests, gets the `length` the donor played it for.
the others are named in `P3_OWN_BRANCH`. A clip the donor starts past its first frame gets that
`start`; one the handler leaves at a frame it tests gets that frame as `stop`. Both are clip
frames, so they hold at any blend and cursor rate (a blend holds the cursor 2 to 5 AI frames,
enraged it runs 2.8 a frame). Only a step that ended on no frame of its clip (a pair, a loop,
a test the cursor never changed) gets the `length`, in AI frames, the donor played it for.
- **Attacks.** A record's START and LEN against the cursor at the spawn give the frames; a node
that outlives a clip change is split per clip. The record's volume set is matched to the host
record whose volumes overlap it most on the port's rig (the same kind first, then power); below
Expand All @@ -180,7 +183,7 @@ clip that hands to the next by `after`:
| `P3_OWN_ATTACK` | warning | a donor record with no host record and no spare, or more attacks on a clip than the move player holds: left out |
| `P3_OWN_SPARE` | info | a donor record with no host record took a spare one: its id, set, volumes, power, kind, active frames |
| `P3_OWN_PARTIAL` | warning | the walk gave up, or a clip it could not resolve |
| `P3_OWN_BRANCH`, `P3_OWN_MOTION`, `P3_OWN_START`, `P3_OWN_NEXT` | info | a branch taken one way; code movement; a clip the donor starts past its first frame; a pair the handler hands over to |
| `P3_OWN_BRANCH`, `P3_OWN_MOTION`, `P3_OWN_NEXT` | info | a branch taken one way; code movement; a pair the handler hands over to |
| `P3_OWN_SPLIT`, `P3_OWN_GROUP`, `P3_OWN_ACTIVE` | info | a node split per clip; records that land one hit between them in the donor; a window longer than the host record's life |

### Spare attack records
Expand Down
10 changes: 8 additions & 2 deletions framework/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ for while the monster's notice runs (it knows the player, not yet in combat) wai
and the combat entry; the call's `force` starts it at once (`mhfu.move_play(ent, spec, true)`).
`mhfu.monster_state(ent)` says whether it noticed the player, is in combat, runs its notice.

`start` and `stop` are clip frames: the clip starts at `start` (the executor's a3, so every part
agrees and an attack before it never spawns) and the move ends when the timing part's cursor
reaches `stop`, as the clip's end or `length` would end it. Neither depends on the blend (2 to 5
AI frames held at `start`) or on the cursor's rate (2.0 a frame, 2.8 enraged); `length` counts
AI frames from the dispatch and does.

## Monster events

The same AI step finds what a mod may answer, one AI frame after the engine's change at most,
Expand All @@ -131,10 +137,10 @@ the engine counts the flinch and applies the damage, then enters the move's carr
## Own moves from a manifest

A port's own moves sit in a registry by slot (`mhfu.em_move`): `mhfu.addr.EM_MOVES.MOVES_COUNT`
(96) of them, and the turn keys they share (`EM_MOVES.KEYS_COUNT`, 5632), in one 19.9 KB
(96) of them, and the turn keys they share (`EM_MOVES.KEYS_COUNT`, 5632), in one 20.3 KB
partition block. The seam's
brain, in C on the AI step, plays one through the move player when asked (`mhfu.em_play`), when the
move before it ends on its clip, its length or a wall and names it as its `after`, or when a rule
move before it ends on its clip, its length, its stop or a wall and names it as its `after`, or when a rule
says (`play_move`, and `from_move` to fire while one plays). `mhfu_port.lua` does all of it from the
port's generated `<name>_moves.lua`: `port:move("stamp")` (`ports/README.md` "Moves and
behaviour"). Its `tip` goes to `mhfu.em_tip`: each tip joint takes its carrier's pose after the
Expand Down
4 changes: 2 additions & 2 deletions framework/include/mhfu/em_vhook.h
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,8 @@ int mhfu_em_tip(const uint8_t (*pairs)[2], int n);
/* --- own moves ------------------------------------------------------------------------------
* A port's own moves, by slot, in partition memory. The brain, in C on the wrapped AI step, plays
* one through the move player that same AI frame: one asked for (mhfu_em_play), else the AFTER of
* one of ours that ended on its clip, its length or a wall (forced as that one was), else a
* rule's. A slot with an AFTER ends into its carrier (its back pair) with the AI script's wake
* one of ours that ended on its clip, its length, its stop or a wall (forced as that one was),
* else a rule's. A slot with an AFTER ends into its carrier (its back pair) with the AI script's wake
* cleared (MOVE.HOLD_SCRIPT), so the host brain does not cut in before the next. A slot's FORCE is
* the call's, never the slot's.
*
Expand Down
16 changes: 14 additions & 2 deletions framework/include/mhfu/move.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,16 @@
* crosses its frame, and an attack with an end frame is ended there (or when the move ends
* first) through the node's own end state; without one it lives as long as its record says. The
* move ends when its clip does (the carrier hands off to the brain, or
* the move enters its back pair), after LENGTH frames, or when anything else changes the pair, a
* reaction first of all. With SKIP the host AI step does not run while the clip plays, except
* the move enters its back pair), when the timing part's cursor reaches STOP, after LENGTH frames,
* or when anything else changes the pair, a reaction first of all.
*
* START and STOP are clip frames, so they hold whatever the blend and the cursor's rate are: the
* clip starts at START (the executor's a3) and an attack before it never spawns, one at it spawns
* on the first playing frame. LENGTH counts AI frames from the dispatch instead, the blend's
* (2 to 5, the cursor held at START) among them, at the cursor's SPEED (2.0 a frame calm, 2.8
* enraged).
*
* With SKIP the host AI step does not run while the clip plays, except
* on a frame with a hit pending, so the brain cannot cut in; the host still takes the damage.
* While a move plays, the host's animation events for the entry its clip sits in (attacks and
* effects at the host clip's frames) are skipped, unless HOST_ATTACKS keeps them.
Expand Down Expand Up @@ -72,6 +80,10 @@ typedef struct {
* AI script does not pick between this move and the one that
* follows (em_vhook sets it for a move with an AFTER) */
uint8_t _pad;
uint16_t start; /* clip frame the clip starts at (the executor's a3); 0 = the
* row's own, frame 0 on em75 */
uint16_t stop; /* the move ends when part's cursor reaches this clip
* frame; 0 = none */
} mhfu_move_t;

/* MOVE_STATE.STATE */
Expand Down
8 changes: 5 additions & 3 deletions framework/lua/meta/mhfu.d.lua
Original file line number Diff line number Diff line change
Expand Up @@ -652,7 +652,9 @@ function mhfu.move_block() end
---A move for `mhfu.move_play`; every field but `entry` is optional.
---@class mhfu.Move
---@field entry integer executor entry: the clip
---@field length? integer AI frames from the clip's dispatch, default 0 = until the clip ends
---@field length? integer AI frames from the clip's dispatch (the blend's among them), default 0 = until the clip ends or `stop`
---@field start? integer clip frame the clip starts at, default 0 = its first; an attack before it never spawns
---@field stop? integer clip frame the move ends at, when `part`'s cursor reaches it, whatever the blend and the cursor's rate; default 0 = none
---@field carrier? integer[] { main, sub } the native pair the move rides, default { 0, 2 }
---@field back? integer[] { main, sub, mode } entered at the end, default none
---@field skip? boolean the host AI step skips while the clip plays, except on a hit
Expand Down Expand Up @@ -711,7 +713,7 @@ function mhfu.move_block() end
-- it ends and names it (`after`), or when a rule says (EmRule.play_move).

---Own move slot 0..mhfu.addr.EM_MOVES.MOVES_COUNT-1 plays `spec`; `spec.after` is the slot played when it ends on its clip, its
---length or a wall. False for a bad slot or no room left for its turn keys.
---length, its stop or a wall. False for a bad slot or no room left for its turn keys.
---@param slot integer
---@param spec mhfu.OwnMove
---@return boolean
Expand Down Expand Up @@ -739,7 +741,7 @@ function mhfu.em_moves_status() end

---An own move for `mhfu.em_move`: a `mhfu.Move` and the slot after it.
---@class mhfu.OwnMove: mhfu.Move
---@field after? integer own move slot played when this one ends on its clip, its length or a wall
---@field after? integer own move slot played when this one ends on its clip, its length, its stop or a wall

---What `mhfu.em_moves_status()` returns.
---@class mhfu.OwnMovesStatus
Expand Down
5 changes: 5 additions & 0 deletions framework/mods/lua_host/bind_move.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ static void spec_of(lua_State *L, int t, mhfu_move_t *mv)
{
mhfu_move_init_spec(mv, (uint16_t)opt_field(L, t, "entry", 0));
mv->length = (uint16_t)opt_field(L, t, "length", 0);
int start = opt_field(L, t, "start", 0), stop = opt_field(L, t, "stop", 0);
luaL_argcheck(L, start >= 0 && start <= 0xFFFF && stop >= 0 && stop <= 0xFFFF, t,
"start and stop are clip frames, 0..65535");
mv->start = (uint16_t)start;
mv->stop = (uint16_t)stop;
mv->part = (uint8_t)opt_field(L, t, "part", 0);
mv->spawner = (uint32_t)opt_field(L, t, "spawner", 0);
lua_getfield(L, t, "skip");
Expand Down
4 changes: 2 additions & 2 deletions framework/src/core/em_vhook.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -426,8 +426,8 @@ static int g_qry_wrapped; /* MONSTER_VTABLE.QUERY carries the charge
* It enters the pair mhfu_em_request asked for, keeps the pair's dwell, the hunter's distance and
* the monster events, then plays or enters at most one thing: an own move asked for
* (mhfu_em_play), else the move of the rule on the flinch whose reaction the last host step
* replaced, else the AFTER of the registry's move that just ended on its clip, its length or a
* wall, else the first rule that holds. Last, the rules on the
* replaced, else the AFTER of the registry's move that just ended on its clip, its length, its
* stop or a wall, else the first rule that holds. Last, the rules on the
* flinch arm the reaction replacement for the host step that follows. An own move goes to the
* move player here, and its step, called right after, enters the carrier the same AI frame. While
* the tail cut waits for its drop it enters and plays nothing (cut_waits). Runs inside the
Expand Down
22 changes: 14 additions & 8 deletions framework/src/core/move.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ static_assert(offsetof(mhfu_move_t, spawner) == MHFU_MOVE_SPAWNER, "MOVE layout"
static_assert(offsetof(mhfu_move_t, host_attacks) == MHFU_MOVE_HOST_ATTACKS, "MOVE layout");
static_assert(offsetof(mhfu_move_t, force) == MHFU_MOVE_FORCE, "MOVE layout");
static_assert(offsetof(mhfu_move_t, hold_script) == MHFU_MOVE_HOLD_SCRIPT, "MOVE layout");
static_assert(offsetof(mhfu_move_t, start) == MHFU_MOVE_START, "MOVE layout");
static_assert(offsetof(mhfu_move_t, stop) == MHFU_MOVE_STOP, "MOVE layout");
static_assert(MHFU_MOVE_ATTACKS_COUNT == MHFU_MOVE_MAX_ATTACKS, "MOVE layout");
static_assert(sizeof(mhfu_move_state_t) == MHFU_MOVE_STATE_SIZE, "MOVE_STATE layout");
static_assert(offsetof(mhfu_move_state_t, state) == MHFU_MOVE_STATE_STATE, "MOVE_STATE layout");
Expand Down Expand Up @@ -106,10 +108,10 @@ static void enter(uint32_t ent, uint32_t main_state, uint32_t sub_state, uint32_
{
mhfu_call(MHFU_ENTER_ACTION, ent, main_state, sub_state, mode);
}
/* a2 a3 as the hub handlers pass them */
static void execute(uint32_t ent, uint32_t entry)
/* a2 as the hub handlers pass it (the row's blend); a3 the clip's start frame, 0 the row's */
static void execute(uint32_t ent, uint32_t entry, uint32_t start)
{
mhfu_call(MHFU_ACTION_EXECUTOR, ent, entry, 0, 0);
mhfu_call(MHFU_ACTION_EXECUTOR, ent, entry, 0, start);
}
/* natives pass *COLLISION_WORLD_PTR as the unused first argument */
static uint32_t spawn(uint32_t spawner, uint32_t ent, uint32_t id)
Expand All @@ -122,11 +124,11 @@ static void end_node(uint32_t node, uint32_t vtable)
}
#else
extern "C" void mhfu_host_enter(uint32_t ent, uint32_t m, uint32_t s, uint32_t mode);
extern "C" void mhfu_host_execute(uint32_t ent, uint32_t entry);
extern "C" void mhfu_host_execute(uint32_t ent, uint32_t entry, uint32_t start);
extern "C" uint32_t mhfu_host_spawn(uint32_t spawner, uint32_t ent, uint32_t id);
extern "C" void mhfu_host_end(uint32_t node, uint32_t vtable);
static void enter(uint32_t e, uint32_t m, uint32_t s, uint32_t mode) { mhfu_host_enter(e, m, s, mode); }
static void execute(uint32_t e, uint32_t entry) { mhfu_host_execute(e, entry); }
static void execute(uint32_t e, uint32_t entry, uint32_t start) { mhfu_host_execute(e, entry, start); }
static uint32_t spawn(uint32_t sp, uint32_t e, uint32_t id) { return mhfu_host_spawn(sp, e, id); }
static void end_node(uint32_t node, uint32_t vtable) { mhfu_host_end(node, vtable); }
extern "C" void *mhfu_host_at(uint32_t addr);
Expand Down Expand Up @@ -293,7 +295,7 @@ static void dispatch(uint32_t ent)
{
uint32_t now = pair_of(ent);
if (now != carrier()) { finish(MHFU_MOVE_END_PAIR, now); return; }
execute(ent, S->move.entry);
execute(ent, S->move.entry, S->move.start);
for (int k = 0; k < MHFU_ENTITY_CLIP_BLOCKS_COUNT; k++) {
S->node[k] = mhfu_mem_read_u32(clip_block(ent, k) + MHFU_CLIP_BLOCK_NODE);
S->clip_end[k] = mhfu_mem_read_f32(clip_block(ent, k) + MHFU_CLIP_BLOCK_END);
Expand Down Expand Up @@ -332,6 +334,8 @@ static uint32_t play(uint32_t ent)
if (now != carrier()) { finish(MHFU_MOVE_END_PAIR, now); return 0; }
int p = S->move.part;
float prev = S->peak[p];
const float before = (float)S->move.start - 1.0f; /* an attack before START never spawns */
if (prev < before) prev = before;
for (int k = 0; k < MHFU_ENTITY_CLIP_BLOCKS_COUNT; k++) {
uint32_t b = clip_block(ent, k);
float cur = mhfu_mem_read_f32(b + MHFU_CLIP_BLOCK_PHASE);
Expand All @@ -343,7 +347,8 @@ static uint32_t play(uint32_t ent)
finish(MHFU_MOVE_END_LOST, now);
return 0;
}
attacks(ent, prev, mhfu_mem_read_f32(b + MHFU_CLIP_BLOCK_PHASE));
const float cursor = mhfu_mem_read_f32(b + MHFU_CLIP_BLOCK_PHASE);
attacks(ent, prev, cursor);
int wall = steer(ent);
if (wall == MHFU_STEER_STUCK) {
/* as the Tigrex charge ends on such a wall; the AI script waits for the hub after it */
Expand All @@ -354,7 +359,8 @@ static uint32_t play(uint32_t ent)
return 0;
}
int done = !(mhfu_mem_read_u16(b + MHFU_CLIP_BLOCK_FLAGS) & 1) || wall == MHFU_STEER_WALL;
int timed = S->move.length && S->frames >= S->move.length;
int timed = (S->move.length && S->frames >= S->move.length)
|| (S->move.stop && cursor >= (float)S->move.stop);
if (done || timed) {
if (S->move.back_main != MHFU_MOVE_NO_PAIR || timed) {
int back = S->move.back_main != MHFU_MOVE_NO_PAIR;
Expand Down
8 changes: 4 additions & 4 deletions framework/tests/em_host.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,17 @@ void mhfu_host_enter(uint32_t e, uint32_t m, uint32_t s, uint32_t mode)
if (cell) { cell[0] = (uint8_t)m; cell[1] = (uint8_t)s; }
if (m == 0 && s == 2) mhfu_mem_write_u8(e + MHFU_ENTITY_SCRIPT_WAKE, 1); /* the hub wakes the AI script */
}
void mhfu_host_execute(uint32_t e, uint32_t entry)
void mhfu_host_execute(uint32_t e, uint32_t entry, uint32_t start)
{
record('X', e, entry, 0, 0);
record('X', e, entry, 0, start);
for (uint32_t k = 0; k < MHFU_ENTITY_CLIP_BLOCKS_COUNT; k++) {
uint32_t b = e + MHFU_ENTITY_CLIP_BLOCKS + k * MHFU_CLIP_BLOCK_SIZE;
uint32_t node = 0x5000u + entry;
float zero = 0;
float phase = (float)start;
uint16_t playing = 1;
memcpy(at(b + MHFU_CLIP_BLOCK_NODE, 4), &node, 4);
memcpy(at(b + MHFU_CLIP_BLOCK_END, 4), &g_clip_end, 4);
memcpy(at(b + MHFU_CLIP_BLOCK_PHASE, 4), &zero, 4);
memcpy(at(b + MHFU_CLIP_BLOCK_PHASE, 4), &phase, 4);
memcpy(at(b + MHFU_CLIP_BLOCK_FLAGS, 2), &playing, 2);
}
}
Expand Down
14 changes: 10 additions & 4 deletions framework/tests/move_host.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ static uint32_t g_calls[64][5];
static int g_n;
static int g_lands = 1; /* enter-action writes the pair cells */
static float g_clip_end = 228; /* what the executor installs on every part */
static int g_blend; /* blend frames the executor installs (CLIP_BLOCK.BLEND) */

/* BREAK_TABLE, where the game has it */
static uint8_t g_breaks[MHFU_BREAK_TABLE_COUNT * MHFU_BREAK_ROW_SIZE];
Expand Down Expand Up @@ -82,17 +83,20 @@ void mhfu_host_enter(uint32_t e, uint32_t m, uint32_t s, uint32_t mode)
uint8_t *cell = at(e + MHFU_ENTITY_MAIN_STATE, 2);
if (g_lands && cell) { cell[0] = (uint8_t)m; cell[1] = (uint8_t)s; }
}
void mhfu_host_execute(uint32_t e, uint32_t entry)
/* the executor: every part's clip at cursor `start`, held for the blend as CLIP_SET does */
void mhfu_host_execute(uint32_t e, uint32_t entry, uint32_t start)
{
record('X', e, entry, 0, 0);
record('X', e, entry, 0, start);
for (uint32_t k = 0; k < MHFU_ENTITY_CLIP_BLOCKS_COUNT; k++) {
uint32_t b = e + MHFU_ENTITY_CLIP_BLOCKS + k * MHFU_CLIP_BLOCK_SIZE;
uint32_t node = 0x5000u + entry;
float zero = 0;
float phase = (float)start;
int32_t blend = g_blend ? g_blend + 1 : 0;
uint16_t playing = 1;
memcpy(at(b + MHFU_CLIP_BLOCK_NODE, 4), &node, 4);
memcpy(at(b + MHFU_CLIP_BLOCK_END, 4), &g_clip_end, 4);
memcpy(at(b + MHFU_CLIP_BLOCK_PHASE, 4), &zero, 4);
memcpy(at(b + MHFU_CLIP_BLOCK_PHASE, 4), &phase, 4);
memcpy(at(b + MHFU_CLIP_BLOCK_BLEND, 4), &blend, 4);
memcpy(at(b + MHFU_CLIP_BLOCK_FLAGS, 2), &playing, 2);
}
}
Expand Down Expand Up @@ -150,8 +154,10 @@ void host_set(int lands, float clip_end)
{
memset(g_breaks, 0, sizeof(g_breaks));
g_lands = lands; g_clip_end = clip_end; g_n = 0; g_step = 0; g_nraised = 0; g_cut = 0;
g_blend = 0;
memset(g_react, 0, sizeof(g_react));
}
void host_blend(int frames) { g_blend = frames; }
int host_has_step(void) { return g_step != 0; }
int host_calls(uint32_t *out) { memcpy(out, g_calls, sizeof(g_calls)); int n = g_n; g_n = 0; return n; }
uint32_t host_frame(uint32_t ent) { return g_step ? g_step(ent) : 0xFFFFFFFFu; }
Expand Down
13 changes: 13 additions & 0 deletions framework/tests/test_em_brain.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,14 @@ class Move(ctypes.Structure):
("force", ctypes.c_uint8),
("hold_script", ctypes.c_uint8),
("_pad", ctypes.c_uint8),
("start", ctypes.c_uint16),
("stop", ctypes.c_uint16),
]


assert ctypes.sizeof(Move) == a.MOVE.size


class Op(ctypes.Structure):
_fields_ = [("op", ctypes.c_uint8), ("arg", ctypes.c_uint8), ("value", ctypes.c_int16)]

Expand Down Expand Up @@ -433,6 +438,14 @@ def test_play_asked_starts_that_frame(g: Game) -> None:
assert g.lib.mhfu_em_play(ENT, 1, 0) == 0 # an empty slot


def test_a_slots_start_and_stop_reach_the_move(g: Game) -> None:
assert g.own(0, 46, start=6, stop=14) and g.lib.mhfu_em_play(ENT, 0, 0)
g.frames(2)
assert ("X", ENT, 46, 0, 6) in g.calls()
g.until(lambda: g.move("STATE", "B") == DONE)
assert g.move("END", "B") == BACK and g.move("END_FRAME") == 4 # (14 - 6) / SPEED


def test_after_follows_through_the_carrier(g: Game) -> None:
"""A slot with an AFTER ends into its carrier, and the next plays on the following frame."""
g.own(0, 46, after=1)
Expand Down
Loading
Loading