Skip to content
Closed
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
47 changes: 45 additions & 2 deletions docs/PC.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ darkchronicle [options]
| `--frames N` | exit with status 0 after frame N of the main loop. N is a positive integer |
| `--screenshot PATH` | with `--frames`: write the last frame to PATH as a PNG on exit. A failed write exits with status 1 |
| `--screenshot-display N` | with `--screenshot`: also write N interpolated display frames of that frame, as `<stem>-display-<k>.png` for k = 0 to N-1 |
| `--screenshot-fps` | with `--screenshot`: draw the FPS counter into the image, from a presented frame at window size. With `--offscreen`, nothing is presented: the image is read back from the display image instead |
| `--screenshot-fps` | with `--screenshot`: draw the FPS counter and any menu pointer into the image, from a presented frame at window size. With `--offscreen`, nothing is presented: the image is read back from the display image instead |
| `--input SCRIPT` | drive the pads from an input script at start-up. A bad script exits with status 2 |
| `--display-per-tick N` | also render N interpolated display frames per clock tick, discarded. N is a positive integer |
| `--show-fps` | draw the FPS counter on presented frames, headless too |
Expand Down Expand Up @@ -667,6 +667,49 @@ pointer immediately. Ordinary dialogue choices share the message adapter;
the rendered confirmation replays and drawn-message unit tests cover that input
path, but campaign dialogue choices still need a dedicated replay.

### Character, Options and Help mouse

On the Character page, hovering a shown portrait (Max, Monica, the Ridepod,
Monster Transform or the travelling townsperson) moves the retail cursor there,
and clicking it confirms; a click beside the portraits does nothing. While a
character change loads, left clicks do nothing. The change question, the
townsperson's View Status/Send back question, their commands and their
questions take clicks on their drawn choices; when a question opens over a
list that still draws its cursor, the newest window takes the mouse. On
Cedric's repair board, hover and the wheel select an item and only a click on
an item confirms, so a stray click cannot spend his points. Waits that take
any button (the opening, the townsperson's speech) still take any click.

Retail mirrors the hand to point left at Max. The mouse pointer is never
mirrored: the shared pointer already kept the hotspot on the fingertip in either
facing, but a hand whose body jumps across the hotspot between neighbouring
portraits reads as pointing somewhere else, so it keeps its usual facing on
every target.

On the retail Options page, hovering a row's label moves the row cursor and keeps
its choice; hovering a choice button also moves the choice. Clicking a button
selects that setting. The PC SETTINGS label opens that page on click.

In Help, hovering an entry moves the cursor and a click opens it. On an entry's
picture pages the wheel turns pages (down for the next) through the retail
Left/Right keys; a click turns to the next page and right click returns, as Cross
and Circle do. Movies keep their retail controls. Keyboard and controller input
is unchanged on all three pages.

The wheel follows one rule on these pages: in a list longer than its window,
each notch scrolls the list one row and the cursor takes the row that comes under
the pointer, or stays in view (`MenuMouseScroll`; the ITEM page's bag scrolls the
same way). Elsewhere it moves the cursor, as in shops and the root menu; the
Character page's portrait map is not a list, so the wheel does nothing there. Options and
Help rows are hit where they are drawn while the list eases to a new scroll, and
only inside the list's scissor (`clip0`): a row sliding under the footer cannot
be clicked, and a visible row above the new scroll still can.

`tools/input/character-mouse.input`, `character-mouse-cedric.input`, `options-mouse.input` (`--jump town:10`)
and `help-mouse.input` (`--jump dungeon:0:1`) replay these at `--window 1920x1080`
with `--fast-load`; their comments give capture frames. `--screenshot-fps`
captures include the menu pointer.

### Mouse follow camera

Mouse motion directly turns the active walking camera around the character. It
Expand Down Expand Up @@ -918,7 +961,7 @@ The PAL scripts (`.stb`) hold the NTSC frame counts and convert them while they
- The FPS counter: `video.show_fps` turns it on (F3 and the `fps_toggle` host action flip it; headless only with `--show-fps`). It is drawn through `OverlayRecord`, as a display render's overlay, in the overlay's own font (the Dark Cloud Compendium Community Font baked into `overlay_font.inc`), so no canonical image holds it. The game's message font is not used: its texture lives only in the current mode's texture block (`ReLoadFontTexture` per mode) and `FontTex[0]` dangles across mode changes, so a badge in it would come and go with the modes. Its text is set by `video.fps_detail`; with all fields it reads `FPS 143.9 TICK 60.0/60 DRAWS 412`. TICK counts clock ticks per second, so it reads the tick rate whatever `mgFrameRate` is. DRAWS is the newest list's mesh plus 2D draws. `--log-fps` prints the same text on stderr once a second.
- Interpolation diagnostic: set `KZ_INTERPOLATION_TINT=1` to tint draws on the main presentation image (default off). Green is a matched transformed mesh; cyan is an unmatched or pose-excluded mesh carried by the display camera; red is newest-only geometry; blue is flat 2D or a screen effect; yellow is a depth-tested sprite with reprojected corners. Unchanged matched meshes remain green. This classifies geometry, not all its inputs: texture contents, lighting, fog parameters and sprite motion can still be tick-exact. Alpha tests, coverage, depth and blending are retained, so translucent passes can mix tints. Reusable intermediate render targets keep their original colours; their final composites are tinted on the main image. Canonical renders and ordinary screenshots never receive the tint. Use `--screenshot-display 8` to inspect the diagnostic at eight interpolation phases. Optional `KZ_INTERPOLATION_TRACE=<file>` appends per-draw CSV sections at alpha zero, including matching/cut reasons, texture IDs, camera IDs, target IDs, whether a draw is tinted on the main image, and changed fog/material/projection/local inputs. It writes only while tint is enabled; remove both environment variables after diagnosis.
- Outlined models render into a shared transparent image. Canonical clears and copies retain the retail draw rectangle; presentation scratch clears and replacement vertices cover the full visible extent so the interpolated model/bone pose remains complete under the display camera, including fixed-camera mouse look. Each copy preserves its texture-to-screen offset. Fixed-look current and previous lists preserve presentation vertices and clear rectangles while leaving resource retirement ownership with their source lists. Transparent texels in a depth-writing presentation composite do not write depth. GPU regressions compare a rotating thin part at alpha 0.5 against an independently recorded horizontal pose and complete fixed-look outline pixels at alpha 1 and 0.5 against a direct full-extent reference render; translation, scratch-target reuse and transparent-depth regressions remain covered. `tools/input/area-dungeon-chest-turn.input` with `--jump dungeon:0:1 --fast-load --frames 2518 --screenshot chest.png --screenshot-display 8` exercises the unopened chest during a camera turn. For multi-part NPCs, use `--input tools/input/repro-town-npc-outline.input --jump town:10 --fast-load --frames 278 --window 1280x720 --screenshot woman.png --screenshot-display 8`; the woman's face must remain visible throughout the turn. `--input tools/input/repro-clown-outline.input --frames 27230 --window 1280x720 --screenshot clown.png --screenshot-display 8` checks the clown's face during the prologue camera move.
- `--screenshot-display N` writes N display frames of the screenshot's frame, at alpha k/N from the previous list to the last, as `<stem>-display-<k>.png`. `--screenshot-fps` writes the screenshot with the FPS counter drawn into it, at window size, from a presented frame. With `--offscreen`, nothing is presented, and the image is read back from the display image instead. The counter reaches no other written image.
- `--screenshot-display N` writes N display frames of the screenshot's frame, at alpha k/N from the previous list to the last, as `<stem>-display-<k>.png`. `--screenshot-fps` writes the screenshot with the FPS counter and any visible menu pointer drawn into it, at window size, from a presented frame (`RecordHostOverlay`, shared with presentation). With `--offscreen`, nothing is presented, and the image is read back from the display image instead. Neither reaches any other written image.
- `video.max_fps=0` leaves presentation uncapped; a positive cap paces display starts without catching up in bursts. A due logic tick takes priority. FIFO follows the display's synchronization, while mailbox and immediate depend on device support. Interpolation off presents canonical frames at their game cadence; a high display cap does not increase the logic tick rate.

### Packets
Expand Down
2 changes: 2 additions & 0 deletions port/include/stubs/menuchr.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@
#include <menuchr.hpp>
#include "texture_color.hpp"

extern CMenuChrCngMenu *ChrChangMenuPt asm("MenuMouse_ChrChange");

void PortRetailInitMainCharaBG(int, mgCMemory *, int) asm("MenuChr_InitMainCharaBG");
int PortRetailKeyMainCharaBG() asm("MenuChr_KeyMainCharaBG");
3 changes: 3 additions & 0 deletions port/include/stubs/menuop.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@
extern CSaveMenuClass *SaveMenuPtr asm("MenuMouse_Save");
// Let the PC page wrap the original Options step without replacing its state machine.
extern CMenuOption *CMenuOptionPtr asm("MenuOption_Current");
// The menu pointer reads the option count and the Help list it scrolls.
extern float config_option_num_i asm("MenuOption_Count");
extern CManualMenu *CManualPtr asm("MenuMouse_Manual");
2 changes: 1 addition & 1 deletion port/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ Options g_options;
" --screenshot PATH with --frames: write the last frame to PATH as a PNG on exit\n"
" --screenshot-display N with --screenshot: also write N interpolated display frames of that frame\n"
" beside PATH, as PATH-display-K.png for K = 0..N-1\n"
" --screenshot-fps with --screenshot: draw the FPS counter into the written image\n"
" --screenshot-fps with --screenshot: draw the FPS counter and menu pointer into the written image\n"
" --input SCRIPT drive the pads named in SCRIPT from an input script\n"
" --window WxH window size in pixels (default: config.json, then the monitor's)\n"
" --display-per-tick N headless: also render N interpolated display frames per tick (discarded)\n"
Expand Down
69 changes: 63 additions & 6 deletions port/src/menu_mouse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ struct Hand {
gfx::TextureBinding texture;
gfx::LogicalRect uv{};
float rotation = 0, scale = 1;
int reverse = 0, alpha = 128, seen = -100;
int alpha = 128, seen = -100;
} hand;

struct Choices {
Expand All @@ -40,6 +40,16 @@ struct Choices {
std::vector<MenuMouseRect> rows;
} choices;

// Choice windows drawn in the last two ticks: the tick each first appeared, the order it was first
// drawn in, and the tick it was last drawn.
struct ShownChoices {
const void *owner;
int message, first, order, last;
};

std::vector<ShownChoices> shown_choices;
int shown_order = 0;

struct ListClick {
const void *owner = nullptr;
int seen = -100, ready = 0, target = -1;
Expand Down Expand Up @@ -67,6 +77,12 @@ int MenuMouseHit(std::span<const MenuMouseRect> rows, float x, float y) {
return -1;
}

MenuMouseRect MenuMouseClip(MenuMouseRect rect, MenuMouseRect clip) {
const float left = std::max(rect.x, clip.x), top = std::max(rect.y, clip.y);
const float right = std::min(rect.x + rect.w, clip.x + clip.w), bottom = std::min(rect.y + rect.h, clip.y + clip.h);
return right > left && bottom > top ? MenuMouseRect{left, top, right - left, bottom - top} : MenuMouseRect{};
}

int MenuMouseChoose(const MenuMouseSample &mouse, std::span<const MenuMouseRect> rows, int selected, int wheel_step) {
if (!mouse.pointing || rows.empty()) {
return selected;
Expand All @@ -81,6 +97,20 @@ int MenuMouseChoose(const MenuMouseSample &mouse, std::span<const MenuMouseRect>
return selected;
}

MenuMouseView MenuMouseScroll(float wheel, MenuMouseView view, int count, int visible, int hovered) {
if (wheel == 0 || !std::isfinite(wheel) || count <= 0 || visible <= 0) {
return view;
}
const int notches = static_cast<int>(std::clamp(wheel, -32.0f, 32.0f));
view.top = std::clamp(view.top - notches, 0, std::max(0, count - visible));
if (hovered >= 0 && view.top + hovered < count) {
view.selected = view.top + hovered;
} else {
view.selected = std::clamp(view.selected, view.top, std::min(count, view.top + visible) - 1);
}
return view;
}

namespace {
bool PadOrFocusOwnsPointer() {
const auto &pad = InputGetPad(0);
Expand Down Expand Up @@ -146,11 +176,32 @@ void SetCapture(bool on) {
void MenuMouseEnable(bool on) {
navigation = on;
choices = {};
shown_choices.clear();
list_click = {};
SetCapture(on);
}

void MenuMouseNoteChoices(const void *owner, int message, std::span<const MenuMouseRect> rows, int selected, bool horizontal) {
// A question can open over a list that still draws its cursor (Cedric's repair question over his
// commands). The newest window takes the mouse; alternating between them would drop every click.
const int now = mgPortFrameCount();
std::erase_if(shown_choices, [&](const ShownChoices &window) { return window.last < now - 1; });
auto window = std::ranges::find_if(shown_choices, [&](const ShownChoices &shown) {
return shown.owner == owner && shown.message == message;
});
if (window == shown_choices.end()) {
shown_choices.push_back({owner, message, now, ++shown_order, now});
window = shown_choices.end() - 1;
}
window->last = now;
// Windows that appear in one tick rank by first draw: the later one is on top. The ranking holds
// until a window closes, whatever order later redraws take.
const ShownChoices mine = *window;
if (std::ranges::any_of(shown_choices, [&](const ShownChoices &shown) {
return shown.first > mine.first || (shown.first == mine.first && shown.order > mine.order);
})) {
return;
}
if (choices.owner != owner || choices.message != message || choices.drawn < mgPortFrameCount() - 1) {
choices.target = selected;
choices.confirm = false;
Expand Down Expand Up @@ -336,14 +387,14 @@ gfx::InterpKey MenuMouseNextHandKey() {
gfx::InterpKey MenuMouseReplacedHandKey() { return MenuMousePointerVisible() ? hand.key : 0; }

gfx::InterpKey MenuMouseNoteHand(const gfx::TextureBinding &texture, gfx::LogicalRect uv,
float rotation, int reverse, int alpha, float scale) {
float rotation, int alpha, float scale) {
if (texture.texture == gfx::kNullTexture) {
return 0;
}
const auto key = MenuMouseNextHandKey();
++hand_index;
if (enabled) {
hand = {key, texture, uv, rotation, scale, reverse, alpha, mgPortFrameCount()};
hand = {key, texture, uv, rotation, scale, alpha, mgPortFrameCount()};
}
return key;
}
Expand All @@ -365,9 +416,9 @@ void MenuMouseDraw() {
state.alpha_test = true;
state.alpha_func = gfx::AlphaFunc::Greater;
const float c = std::cos(hand.rotation), s = std::sin(hand.rotation);
const float sign = hand.reverse ? -1 : 1;
// The pointing fingertip is at the far edge, halfway down the hand's atlas cell.
const float tip_x = sign * hand.uv.w * hand.scale;
// The pointing fingertip is at the far edge, halfway down the hand's atlas cell. The hand is
// never mirrored, so its body stays on the same side of the hotspot on every target.
const float tip_x = hand.uv.w * hand.scale;
const float tip_y = hand.uv.h * hand.scale * 0.5f;
const float texture_scale = mgPortTextureScale(hand.texture.texture);
gfx::Vertex2D vertices[4]{};
Expand All @@ -387,4 +438,10 @@ void MenuMouseDraw() {

void MenuMouseSetButtons(int mask) { buttons = mask; }

void MenuMouseClaimButtons(int mask) {
buttons = mask;
choices.target = choices.selected;
choices.confirm = false;
}

bool MenuMouseActive() { return enabled; }
39 changes: 37 additions & 2 deletions port/src/menu_mouse.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ struct MenuMouseSample {
std::uint32_t clicked = 0, released = 0, held = 0;
};

// The part of a hit rectangle inside a list's scissor; empty when none of it is.
MenuMouseRect MenuMouseClip(MenuMouseRect rect, MenuMouseRect clip);

struct MenuMouseView {
int top = 0, selected = 0;
};

// Wheel notches scroll a list by whole rows. The cursor takes the row that comes under the pointer
// (hovered, counted from the first shown row, or -1), or else stays in view.
MenuMouseView MenuMouseScroll(float wheel, MenuMouseView view, int count, int visible, int hovered);

// Relative window pixels map through the same logical extent as the menu art.
float MenuMouseUnits(float scale, float pixels, float window);
int MenuMouseHit(std::span<const MenuMouseRect> rows, float x, float y);
Expand All @@ -40,11 +51,15 @@ void MenuMouseDraw();
inline constexpr gfx::InterpKey kMenuMouseHandKey = 0x4d454e5548414e44;
bool MenuMousePointerVisible();
MenuMouseSample MenuMouseDisplaySample();
gfx::InterpKey MenuMouseNoteHand(const gfx::TextureBinding &texture, gfx::LogicalRect uv,
float rotation = 0, int reverse = 0, int alpha = 128, float scale = 1);
// The pointer keeps one facing, so a mirrored retail hand (Max on the Character page) passes no facing.
gfx::InterpKey MenuMouseNoteHand(const gfx::TextureBinding &texture, gfx::LogicalRect uv,
float rotation = 0, int alpha = 128, float scale = 1);

// Exclusive adapters translate this sampled frame into retail buttons.
void MenuMouseSetButtons(int buttons);
// Like MenuMouseSetButtons, for a screen drawn over choice windows: it also drops the target and
// confirmation this tick's sample gave them, so nothing acts on them once the screen closes.
void MenuMouseClaimButtons(int buttons);
bool MenuMouseActive();

class CSaveMenuClass;
Expand All @@ -54,4 +69,24 @@ gfx::InterpKey MenuMouseNextHandKey();
gfx::InterpKey MenuMouseReplacedHandKey();
class CMenuOption;
class CMenuPosDataForm;
// Retail Options targets are drawn rows and buttons inside the list's clip form (clip0).
bool MenuMouseOption(CMenuOption &menu, CMenuPosDataForm *form);
bool MenuMouseOption(CMenuOption &menu, CMenuPosDataForm *form, const CMenuPosDataForm *clip);
// Hover moves the retail Options cursor to a row or button; the wheel scrolls its rows.
void MenuMouseOptionHover(CMenuOption &menu, CMenuPosDataForm *form, const CMenuPosDataForm *clip);
// Character page portraits: hover selects, a click on a portrait confirms.
int MenuMouseCharaSelect(CMenuPosDataForm &form, unsigned party, int npc_no, int select);
// What the mouse drives on the Character page in a retail state.
enum class MenuMouseCharaInput {
Retail, // Any-button waits and drawn message choices keep the shared mouse.
Portraits, // Hover selects a portrait; only a portrait click confirms.
Locked, // A character change is loading; left clicks do nothing.
RepairBoard, // Cedric's item board: hover and wheel select; only an item click confirms.
};
MenuMouseCharaInput MenuMouseCharaInputFor(int mode, int sub_menu, int page, int step, int change_phase, int open_wait);
void MenuMouseRepairBoard(CMenuPosDataForm *board, int &select, int &top, int count);
class CMenuChrCngMenu;
void MenuMouseChara(CMenuChrCngMenu &menu);
// Help: entries follow hover and scroll with the wheel; the wheel turns picture pages.
class CManualMenu;
void MenuMouseManual(CManualMenu &menu, CMenuPosDataForm *background, const CMenuPosDataForm *clip);
Loading
Loading