Skip to content
Open
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
25 changes: 22 additions & 3 deletions framework/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,12 @@ 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 20.3 KB
partition block. The seam's
A port's own moves sit in a registry by slot (`mhfu.em_move`): `mhfu.addr.OWN_MOVES` (128) of
them, and the turn keys (`EM_KEYS`, 2944), travel samples (`EM_SAMPLES`, 1280) and lunges
(`EM_LUNGES`, 12) they share, in one 24 KB partition block. These are consts in
`addresses.toml`: raising one is that one line, every offset after it is laid out from it. A run
a pool holds already, anywhere in it, word for word (a lunge byte for byte), is shared, not
appended: `mhfu_port.moves.Pools` counts the same way, so a module its check passes fits. 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, 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
Expand All @@ -152,6 +155,22 @@ hunter and away turns apply on every frame of the move unless the steer has a `w
to }`: then only while the clip frame (part 0's cursor) is in `[from, to)`, as the donor turns in
a cursor window.

A move can travel by code, as MHP3rd's airborne steps do, where the clip carries nothing.
`steer.travel` is the monster's own (x, z) every `mhfu.addr.STEER_KEY_STEP` clip frames from the
move's `start`: cumulative, x toward its left and z forward in its frame when the move began, as
16-bit samples of 1/`STEER_TRAVEL_SCALE` units (8 hex digits a sample in the moves module,
`mhfu.steering`), at most `STEER_SAMPLES` a move. Each AI frame the move player adds the curve's
change since the clip cursor's last frame (straight lines between samples, so any cursor rate
and the blend's hold are right) to POSITION, turned by that YAW and scaled by the monster's size.
It does so before the engine's root motion, wall push-out and floor in the same frame: walls
undo it as they undo root motion, and the floor keeps the height. `steer.lunge = { window = {
lo, hi }, near, span, base, range }` travels toward the hunter across the clip frames (lo, hi]: by
the hunter's XZ distance `d` when the move began, nothing under `near`, else `base + range *
min((d - near) / span, 1)` units along the bearing to the hunter then, in world axes and not
scaled. A fixed turn with a `window` turns `total` across it by the cursor instead of over
`frames` AI frames. All of it is data in the registry and in `mhfu.move_play`'s steer; a move
without it writes what it always did.

An AI script plays own move k with `enter 16 k 0` (`mhfu.addr.OWN_MOVE_MAIN`): the enter-action
stub enters the slot's carrier (the hub `(0,2)` for an empty or out-of-range slot, the game never
sees main 16), files a forced play, and the brain starts the move at the next AI step; the script
Expand Down
22 changes: 14 additions & 8 deletions framework/include/mhfu/em_vhook.h
Original file line number Diff line number Diff line change
Expand Up @@ -182,19 +182,21 @@ int mhfu_em_tip(const uint8_t (*pairs)[2], int n);
* pair: the wake the hub raises inside the run is cleared by the script's tick after it, and the
* move player clears the one its own entry of the hub raises. mhfu_em_status_t.own_enters counts
* these enters, own_last gives the slot of the last that played. */
#define MHFU_EM_MOVES MHFU_EM_MOVES_MOVES_COUNT
#define MHFU_EM_KEYS MHFU_EM_MOVES_KEYS_COUNT
#define MHFU_EM_MOVES MHFU_OWN_MOVES
#define MHFU_EM_NO_MOVE 0xFFu
#define MHFU_EM_NO_LUNGE 0xFFu /* mhfu_em_own_t.lunge_at: no lunge */
/* The pools' sizes, MHFU_EM_KEYS, _SAMPLES and _LUNGES, are consts in addresses.toml. */

/* struct EM_OWN in addresses.toml */
typedef struct {
mhfu_move_t move;
mhfu_steer_t steer;
uint16_t key_at, key_count; /* its turn keys in mhfu_em_moves_t.keys */
uint16_t sample_at, sample_count; /* its travel samples in mhfu_em_moves_t.samples */
uint8_t stuck_main, stuck_sub, stuck_mode;
uint8_t after; /* slot played when this one ends; MHFU_EM_NO_MOVE */
uint8_t lunge_at; /* its lunge in mhfu_em_moves_t.lunges, or NO_LUNGE */
uint32_t valid;
uint32_t _pad;
} mhfu_em_own_t;

/* struct EM_MOVES in addresses.toml */
Expand All @@ -203,17 +205,21 @@ typedef struct {
uint32_t req_ent, req_slot; /* a play asked for, taken at req_ent's AI step */
uint32_t tag_slot, tag_started; /* the slot of the move player's move, if ours */
uint32_t plays, chained;
uint32_t key_top;
uint32_t key_top, sample_top, lunge_top; /* pool entries in use */
uint32_t req_force; /* REQ_SLOT starts while the notice runs */
mhfu_em_own_t moves[MHFU_EM_MOVES];
uint16_t keys[MHFU_EM_KEYS];
mhfu_travel_sample_t samples[MHFU_EM_SAMPLES];
mhfu_steer_lunge_t lunges[MHFU_EM_LUNGES];
mhfu_steer_spec_t scratch; /* the steering handed to the move player */
uint32_t req_force; /* REQ_SLOT starts while the notice runs */
} mhfu_em_moves_t;

/* Empties every slot and the turn keys. */
/* Empties every slot and the pools: turn keys, travel samples, lunges. */
void mhfu_em_moves_clear(void);
/* Slot `slot` plays mv, steered by s (its keys copied); `after` is a slot or MHFU_EM_NO_MOVE.
* 0 for a bad slot or no room left for the keys. */
/* Slot `slot` plays mv, steered by s; `after` is a slot or MHFU_EM_NO_MOVE. Its keys, travel
* samples and lunge are copied into the pools unless a pool holds that run already, anywhere,
* word for word (a lunge byte for byte): then the slot points there. 0 for a bad slot or no room
* left in a pool for what is new. */
int mhfu_em_move(int slot, const mhfu_move_t *mv, const mhfu_steer_spec_t *s, uint8_t after);
/* Plays slot on entity at its next AI step, forced past the notice wait (MOVE.FORCE) or not; 0
* while nothing is wrapped or the slot is empty. */
Expand Down
2 changes: 1 addition & 1 deletion framework/include/mhfu/fatigue.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ extern "C" {
#endif

#define MHFU_FATIGUE_PAIRS MHFU_FATIGUE_CFG_PAIRS_COUNT /* costs by pair */
#define MHFU_FATIGUE_OWN MHFU_EM_MOVES_MOVES_COUNT /* costs by own-move slot */
#define MHFU_FATIGUE_OWN MHFU_OWN_MOVES /* costs by own-move slot */

/* struct FATIGUE_PAIR in addresses.toml */
typedef struct {
Expand Down
47 changes: 36 additions & 11 deletions framework/include/mhfu/steer.h
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
/* SPDX-License-Identifier: MIT */
/* SPDX-FileCopyrightText: 2026 sp00ktober */
/* A big monster's own move, steered one AI frame at a time: its turn, and whether a wall ends it.
* The engine carries the travel itself (the clip's root motion) and pushes the monster out of
* walls (WALL_COLLIDE, after the AI step); this reads that frame's wall result and turns YAW.
* Leaf functions: no engine or library call. */
/* A big monster's own move, steered one AI frame at a time: its turn, whether a wall ends it, and
* travel the clip does not carry. The engine carries the clip's root motion itself and pushes the
* monster out of walls (WALL_COLLIDE, after the AI step); this reads that frame's wall result,
* turns YAW and moves POSITION, which that push-out then corrects as it does root motion.
* Leaf functions: no engine call. */
#ifndef MHFU_STEER_H
#define MHFU_STEER_H

#include <stdint.h>

#include "addresses.gen.h"
#include "mhfu/types.h"

#ifdef __cplusplus
Expand All @@ -22,7 +24,7 @@ enum {
MHFU_STEER_STILL = 0, /* no turn */
MHFU_STEER_HUNTER = 1, /* toward the hunter, at most `rate` a frame (TURN_TOWARD's step) */
MHFU_STEER_AWAY = 2, /* away from the hunter, the same way */
MHFU_STEER_FIXED = 3, /* `total` spread evenly over `frames` frames */
MHFU_STEER_FIXED = 3, /* `total` spread evenly over `frames` frames, or across the window */
};

enum {
Expand All @@ -39,7 +41,9 @@ typedef struct {
uint16_t frames; /* FIXED: frames to spread `total` over, 1..0x7FFF */
uint16_t dir; /* direction of travel against YAW, YAW units: 0 forward, 0x8000 back */
uint16_t win_from, win_to; /* HUNTER, AWAY: turn only while the clip frame is in
* [win_from, win_to); both 0: the whole move */
* [win_from, win_to); both 0: the whole move. FIXED, in
* mhfu_steer_move: `total` is spread across the window by the
* clip cursor and `frames` is not read; win_to 0: over `frames` */
} mhfu_steer_t;

/* atan2(dx, dz) in YAW units, within one unit. */
Expand All @@ -65,8 +69,18 @@ int mhfu_steer_frame(uint32_t entity, const mhfu_steer_t *p, uint16_t frame);

/* --- a move's steering, as the move player runs it (move.cpp) ------------------------------- */

#define MHFU_STEER_KEY_STEP 2 /* clip frames between turn keys: mhfu_port.travel.KEY_STEP */
#define MHFU_STEER_KEYS 256
/* MHFU_STEER_KEY_STEP, _KEYS, _SAMPLES and _TRAVEL_SCALE are consts in addresses.toml */

/* struct TRAVEL_SAMPLE in addresses.toml: x, z in 1/MHFU_STEER_TRAVEL_SCALE world units */
typedef struct {
int16_t x, z;
} mhfu_travel_sample_t;

/* struct STEER_LUNGE in addresses.toml */
typedef struct {
uint16_t lo, hi; /* clip frames, the window (lo, hi]; hi 0: no lunge */
float near, span, base, range;
} mhfu_steer_lunge_t;

/* struct STEER_SPEC in addresses.toml */
typedef struct {
Expand All @@ -76,6 +90,11 @@ typedef struct {
uint8_t _pad[3];
uint16_t keys[MHFU_STEER_KEYS]; /* YAW every KEY_STEP clip frames, the last at the clip's end:
* the port's turns module (mhfu_port.travel.Turn) */
uint16_t sample_count; /* 0: no travel by code */
mhfu_steer_lunge_t lunge;
mhfu_travel_sample_t samples[MHFU_STEER_SAMPLES]; /* the monster's own (x, z) at clip frame
* start + k * KEY_STEP, cumulative, in its frame at the first
* playing frame (x its left, z forward); held past the last */
} mhfu_steer_spec_t;

/* struct STEER_STATE in addresses.toml */
Expand All @@ -84,22 +103,28 @@ typedef struct {
mhfu_steer_spec_t now;
uint16_t base; /* YAW the curve adds to */
uint8_t wall; /* the last frame's MHFU_STEER_* */
uint8_t _pad;
float cursor; /* the cursor the step saw last */
uint16_t yaw0, yaw; /* YAW on the first playing frame, and as the step left it */
float lunge_x, lunge_z; /* the lunge's whole length, world axes, set on the first frame */
} mhfu_steer_state_t;

/* No turn, no walls; a class-2 wall ends into (0,6) mode 1, as the Tigrex charge does. */
void mhfu_steer_init_spec(mhfu_steer_spec_t *s);
/* The curve's YAW at clip frame `cursor`, the keys joined by straight lines and held past the
* last; 0 without keys. Exact on the keys, where the engine's cursor sits at speed 2. */
uint16_t mhfu_steer_curve(const volatile mhfu_steer_spec_t *s, float cursor);
/* sin and cos of YAW `yaw`, within 1e-6. */
void mhfu_steer_sincos(uint16_t yaw, float *s, float *c);
/* One AI frame of a move (move.cpp's seam), `frame` its playing frames from 1: frame 1 takes
* NEXT; a cursor that went back (a loop) carries the curve's turn over; YAW = base + curve, the
* base turned by the spec's mode (HUNTER and AWAY only while `cursor`, the clip frame, is in
* the window). Returns the wall result of the frame before, from frame 2. */
* the window; FIXED with a window across it by the cursor). Then POSITION moves by the change
* of the travel curve and of the lunge between the cursor of the frame before (frame 1: `start`,
* the clip frame the move began at) and this one: the curve's turned by YAW on frame 1 and
* scaled by ENTITY.SIZE_SCALE, the lunge along the bearing to `hunter` on frame 1, unscaled.
* Returns the wall result of the frame before, from frame 2, and then moves nothing. */
int mhfu_steer_move(void *entity, const mhfu_vec3_t *hunter, volatile mhfu_steer_state_t *st,
uint32_t frame, float cursor, float loop_start, float end);
uint32_t frame, float start, float cursor, float loop_start, float end);

#ifdef __cplusplus
} /* extern "C" */
Expand Down
6 changes: 4 additions & 2 deletions framework/lua/lib/mhfu_port.lua
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,8 @@ P.is_own = is_own
-- an own move (port:move) and takes the registry slot of its `slot` (the manifest's
-- module numbers its own moves, which the brain splice's `enter` names), else the
-- first free one by name. Its `steer` goes to the move player as it is (mhfu.MoveSteer:
-- turn, rate, `window = { from, to }` the clip frames a hunter or away turn keeps to).
-- turn, rate, `window = { from, to }` the clip frames a turn keeps to or a fixed turn
-- spreads across, `travel` the move's own travel by code, `lunge` toward the hunter).
-- Default: the manifest's (P.behaviour)
-- rules port:rule{} specs installed with the port; default: the manifest's
-- vars, signals name -> index of the board's counters and signals (port:var, port:fire); default:
Expand Down Expand Up @@ -747,7 +748,8 @@ function Port:_arm_native()
if n ~= nil then
made = made + 1
if not mhfu.em_move(k, self:_seam_move(n)) then
log("[port:%s] own move '%s' not registered: no room for its turn keys", self.name, n)
log("[port:%s] own move '%s' not registered: no room for its turn keys, travel or lunge",
self.name, n)
end
end
end
Expand Down
19 changes: 16 additions & 3 deletions framework/lua/meta/mhfu.d.lua
Original file line number Diff line number Diff line change
Expand Up @@ -669,12 +669,22 @@ function mhfu.move_block() end
---@field curve? string YAW while the clip plays: the entry's string in `<port>_turns.lua`
---@field turn? "still"|"hunter"|"away"|"fixed" also: toward or away from the hunter at `rate`, or `total` over `frames`
---@field rate? integer YAW units an AI frame toward or away, default 64 (the Tigrex charge's)
---@field window? integer[] { from, to }: clip frames, 0 <= from < to, in which "hunter" and "away" turn (the donor turns inside a window); default the whole move
---@field window? integer[] { from, to }: clip frames, 0 <= from < to, in which "hunter" and "away" turn (the donor turns inside a window), or across which "fixed" spreads `total` by the clip cursor; default the whole move
---@field total? number degrees for "fixed", positive the way YAW grows
---@field frames? integer AI frames "fixed" spreads `total` over
---@field frames? integer AI frames "fixed" spreads `total` over, without a `window`
---@field walls? boolean a wall ahead ends the move; a class-2 one enters `stuck`
---@field dir? number degrees of the travel against YAW, which sectors count as ahead; default 0
---@field stuck? integer[] { main, sub, mode } entered on a class-2 wall, default { 0, 6, 1 }
---@field travel? string the move's own travel by code, as the port's moves module writes it: 8 hex digits a sample, x then z as 16 bits each in 1/mhfu.addr.STEER_TRAVEL_SCALE world units, cumulative, every mhfu.addr.STEER_KEY_STEP clip frames from the move's `start`, at most mhfu.addr.STEER_SAMPLES; each AI frame moves the monster by the curve's change since the cursor's last frame, in its frame at the first playing frame (x its left, z forward), scaled by its size, held past the last sample
---@field lunge? mhfu.MoveLunge travel toward the hunter, by the hunter's distance when the move begins

---A move's lunge (mhfu.MoveSteer.lunge): over the clip frames `window` (lo, hi] the monster moves 0 units while the hunter's XZ distance `d` is under `near`, else `base + range * min((d - near) / span, 1)` along the bearing to the hunter at the first playing frame, in world axes, spread by the clip cursor and not scaled by the size.
---@class mhfu.MoveLunge
---@field window integer[] { lo, hi }: clip frames, 0 <= lo < hi
---@field near? number default 0
---@field span? number default 0, which counts as the whole range
---@field base? number default 0, negative away from the hunter
---@field range? number default 0

---What `mhfu.move_status()` returns.
---@class mhfu.MoveStatus
Expand Down Expand Up @@ -713,7 +723,8 @@ 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, its stop 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, travel
---samples or lunge.
---@param slot integer
---@param spec mhfu.OwnMove
---@return boolean
Expand Down Expand Up @@ -751,6 +762,8 @@ function mhfu.em_moves_status() end
---@field plays integer own moves started
---@field chained integer of them by `after`
---@field keys integer turn keys in use
---@field samples integer travel samples in use
---@field lunges integer lunges in use

-- fatigue (bind_em.cpp, include/mhfu/fatigue.h) ---------------------------------------------------
-- MHFU has no fatigue and its AI script cannot count. A stamina for the wrapped species' monster is
Expand Down
2 changes: 2 additions & 0 deletions framework/mods/lua_host/bind_em.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,8 @@ int lb_em_moves_status(lua_State *L)
SF_INT("plays", r->plays);
SF_INT("chained", r->chained);
SF_INT("keys", r->key_top);
SF_INT("samples", r->sample_top);
SF_INT("lunges", r->lunge_top);
#undef SF_INT
return 1;
}
Expand Down
Loading
Loading