mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-14 07:00:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ead52e79ce |
@@ -86,9 +86,11 @@ The DLL hooks the engine's own text pipeline — no companion addon:
|
||||
(`49934b0`, `b9d0705`).
|
||||
- **Chat fade** — icons fade in lockstep with their chat line (`3ca3b7e`).
|
||||
- **Edit boxes** — render the icons in their text, as 4.3.4 does; the caret,
|
||||
the mouse and the arrow keys treat an icon as one character, and Ctrl+C
|
||||
drops icons (4.3.4's 0x1500 strip mask). Typed markup stays literal by the
|
||||
engine's own input escape (below).
|
||||
the mouse and the arrow keys treat an icon as one character, Ctrl+C
|
||||
drops icons (4.3.4's 0x1500 strip mask), and a multi-line box's caret,
|
||||
selection and click rows follow the lines a tall icon moves (below;
|
||||
4.3.4's own rows do not). Typed markup stays literal by the engine's own
|
||||
input escape (below).
|
||||
- **Payload** — the full
|
||||
`|Tpath:height:width:offsetX:offsetY:texW:texH:left:right:top:bottom:r:g:b|t`:
|
||||
size, pen offset, sprite-sheet crop, and vertex tint. BLP and uncompressed
|
||||
@@ -377,7 +379,7 @@ funnel for exactly 4 callers:
|
||||
|
||||
- `Script_GetStringWidth` (0x0079E510)
|
||||
- the GameTooltip auto-size (0x00530640)
|
||||
- the editbox caret positioner (0x0077DE70)
|
||||
- the editbox selection-piece placer (0x0077DE70, a whole last line's width)
|
||||
- the layout effective-width vmethod (`FUN_00772930` = fs+0x24 vtbl +0x1C,
|
||||
which feeds auto-width fontstring layout)
|
||||
|
||||
@@ -523,6 +525,22 @@ the same input escape, so ClassicAPI's edit boxes need no special case:
|
||||
icon with the following character into one step; ours is its own step.
|
||||
- **Caret x** — `FUN_0077DA80` measures the line through
|
||||
`FUN_00772AE0`, whose co-hook adds the icons.
|
||||
- **Rows (multi-line)** — `PlaceCaret` (`0x77da80`), `PlaceSelectionPiece`
|
||||
(`0x77de70`) and `GetIndexAtPoint` (`0x77d0d0`) step rows of one height
|
||||
(font height + spacing, pixel-aligned), as 4.3.4's `UpdateVisibleCursor`,
|
||||
`UpdateHighlightArea` and `ConvertCoordinateToIndex` do — while the render
|
||||
moves a tall-icon line and every line after it down by the overflow
|
||||
(`InitializeText`; the emitter co-hook) and the box grows to match (the
|
||||
string-height co-hook). 4.3.4 has that fault too; `EditBox::IconRows`
|
||||
(`src/editbox/IconRows.cpp`) gives each placer the render's move, from
|
||||
`Text::InlineTexture::LineIconShift` over the box's own line table: the
|
||||
click's y is moved onto the uniform row of the icon-aware row under it
|
||||
before the engine maps it; a selection piece is re-anchored after the
|
||||
engine placed it, with the engine's own x; the caret likewise, then
|
||||
`OnCursorChanged` fired once with the moved y through
|
||||
`FUN_FRAME_RUN_SCRIPT_VARIADIC` exactly as `PlaceCaret` fires it (its own
|
||||
fire muted for the call by emptying the slot), so
|
||||
`ScrollingEditBoxMixin:ScrollCursorIntoView` scrolls to the real caret.
|
||||
- **Click → cursor, single-line window** — `GetIndexAtPoint` (`0x77d0d0`),
|
||||
`RebuildLabel` and `MeasureWindow` count characters with
|
||||
`FUN_FONTSTRING_FIT_TEXT` (`0x772bc0` → `0x44d860` → `0x5c6c50`), whose
|
||||
|
||||
+47
-1
@@ -13062,6 +13062,51 @@ enum Offsets {
|
||||
// ending at 0x0077D49D.
|
||||
PATCH_EDITBOX_TEXT_CHANGED_FIRE = 0x0077D485,
|
||||
OFF_EDITBOX_ONTEXTCHANGED_SLOT = 0x418,
|
||||
// --- Edit box rows (EditBox::IconRows) ---
|
||||
// The mode flags byte (benilla-re ui/editbox): bit 0 autoFocus, 1
|
||||
// multiLine, 2 numeric, 3 password, 4 ignoreArrows. Every row placer
|
||||
// below tests it (`TEST byte ptr [ESI+0x318], 0x2 / 0x8`). Not the dirty
|
||||
// word OFF_EDITBOX_FLAGS (+0x31c).
|
||||
OFF_EDITBOX_MODE_FLAGS = 0x318,
|
||||
EDITBOX_MODE_MULTILINE = 0x02,
|
||||
EDITBOX_MODE_PASSWORD = 0x08,
|
||||
// The password copy of the text (every byte '*'), the buffer the label
|
||||
// and the rows read with EDITBOX_MODE_PASSWORD set (4.3.4's
|
||||
// m_textHidden); OFF_EDITBOX_TEXT otherwise. RebuildLabel 0x0077D4D0
|
||||
// refreshes it.
|
||||
OFF_EDITBOX_TEXT_MASKED = 0x334,
|
||||
// The visible window in bytes: its first byte and its length (4.3.4's
|
||||
// m_visiblePosition / m_visibleLength). PlaceCaret hides the caret and
|
||||
// fires nothing for a cursor outside it (0x0077DA89..0x0077DAA7).
|
||||
OFF_EDITBOX_VISIBLE_START = 0x348,
|
||||
OFF_EDITBOX_VISIBLE_LENGTH = 0x34C,
|
||||
// The caret texture (4.3.4's m_cursorTexture), anchored by PlaceCaret.
|
||||
OFF_EDITBOX_CARET = 0x368,
|
||||
// OnCursorChanged's script slot; PlaceCaret fires it when non-zero
|
||||
// (0x0077DD8C).
|
||||
OFF_EDITBOX_ONCURSORCHANGED_SLOT = 0x428,
|
||||
// The three row placers, all `__thiscall`: PlaceCaret (no args; 4.3.4's
|
||||
// UpdateVisibleCursor), PlaceSelectionPiece `(editbox, texture, from, to)`
|
||||
// `ret 0xC` (UpdateHighlightArea) and GetIndexAtPoint `(editbox, float x,
|
||||
// float y, int *index)` `ret 0xC` (ConvertCoordinateToIndex). Each steps
|
||||
// rows of FUN_PIXEL_ALIGN_Y(fontH × scale) + FUN_PIXEL_ALIGN_Y(spacing ×
|
||||
// scale), the first two divided by the box's OFF_LAYOUT_SCALE (benilla-re
|
||||
// ui/editbox "The caret" / "Selection"; Ghidra). Co-hooked by
|
||||
// EditBox::IconRows: tall inline icons move the rows.
|
||||
FUN_EDITBOX_PLACE_CARET = 0x0077DA80,
|
||||
FUN_EDITBOX_PLACE_SELECTION_PIECE = 0x0077DE70,
|
||||
FUN_EDITBOX_INDEX_AT_POINT = 0x0077D0D0,
|
||||
// "%f%f%f%f" (.data; verified in the file at 0x479CB8) — the format
|
||||
// PlaceCaret fires OnCursorChanged with through
|
||||
// FUN_FRAME_RUN_SCRIPT_VARIADIC: x, y, width, height, each a double of
|
||||
// FUN_COORD_DIVIDE_BY_SCREEN_SCALE_X(FUN_COORD_GET_SCALED_SCREEN_ASPECT()
|
||||
// × [FLOAT_SCRIPT_COORD_SCALE] × value) — 4.3.4's
|
||||
// RunOnCursorChangedScript (0x0077DDA0..0x0077DE20).
|
||||
VAR_SCRIPT_FMT_FFFF = 0x00879CB8,
|
||||
// 1024.0f (.rdata; verified in the file at 0x3FFD68): the anchor→script
|
||||
// push factor every script coordinate push multiplies by (the `1024` of
|
||||
// the GetFont note at FUN_COORD_GET_SCALED_SCREEN_ASPECT).
|
||||
FLOAT_SCRIPT_COORD_SCALE = 0x007FFD68,
|
||||
// `void __thiscall(editbox, int maxLines)` — the SetHistoryLines internal
|
||||
// (FUN_0077cd90). Reallocates the history ring to maxLines; maxLines == 0
|
||||
// frees the whole buffer and zeroes the container. ClearHistory calls it
|
||||
@@ -13079,7 +13124,8 @@ enum Offsets {
|
||||
// core FUN_0044D670(fontHandle fs+0xE0, text, len, fontH*[fs+0x7C], &out,
|
||||
// fs+0x108, 0, flags fs+0x120), then `fs+0xFC = out / [fs+0x7C]`. Exactly 4
|
||||
// callers: Script_GetStringWidth (0x0079E510), GameTooltip auto-size
|
||||
// (0x00530640), the editbox caret positioner (0x0077DE70), and the layout
|
||||
// (0x00530640), the editbox selection-piece placer (0x0077DE70, a whole
|
||||
// last line's width), and the layout
|
||||
// effective-width vmethod (FUN_00772930 = fs+0x24 vtbl slot +0x1C: explicit
|
||||
// rect width else string width). Prologue PUSH EBX; PUSH ESI; MOV ESI,ECX;
|
||||
// FLD [ESI+0xFC] — MinHook-safe. Co-hooked by Text::InlineTexture to add
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Tall inline icons move the edit box's rows.
|
||||
//
|
||||
// A multi-line edit box places its caret, its selection pieces and a click's
|
||||
// cursor by rows of one height — the label's font height plus its spacing,
|
||||
// pixel-aligned (FUN_EDITBOX_PLACE_CARET, FUN_EDITBOX_PLACE_SELECTION_PIECE,
|
||||
// FUN_EDITBOX_INDEX_AT_POINT; 4.3.4's UpdateVisibleCursor, UpdateHighlightArea
|
||||
// and ConvertCoordinateToIndex step the same rows). The label's text leaves
|
||||
// those rows once a line holds an icon taller than the font: the render moves
|
||||
// that line and every line after it down by the overflow (4.3.4's
|
||||
// InitializeText; the emitter co-hook in Text::InlineTexture), and the box
|
||||
// grows by the same amount through the string-height co-hook. So below the
|
||||
// first tall-icon line the caret and the highlight sat above their text by
|
||||
// the summed overflow, and OnCursorChanged reported that y, which
|
||||
// ScrollingEditBoxMixin:ScrollCursorIntoView scrolls by — pfUI's chat copy
|
||||
// box with faction-emblem icons. 4.3.4 has the same fault; nothing it ships
|
||||
// puts a tall icon in an edit box.
|
||||
//
|
||||
// Each placer keeps its engine code and gets the move the render makes, from
|
||||
// Text::InlineTexture::LineIconShift over the box's own line table (the
|
||||
// breaks the render lays out), split by the label's vertical justify as the
|
||||
// emitter splits it:
|
||||
// - the click: its y is moved onto the uniform row that the icon-aware row
|
||||
// under it has, before the engine maps it;
|
||||
// - a selection piece: re-anchored after the engine placed it, with the
|
||||
// engine's own x, lower by the move;
|
||||
// - the caret: the same re-anchor, then OnCursorChanged fired once with the
|
||||
// moved y through the engine's own forwarder. The engine's fire is muted
|
||||
// for the call by emptying the script slot: it reads the row y before
|
||||
// anything can correct it.
|
||||
// A single-line box has one row and needs nothing: its label is justified
|
||||
// MIDDLE in a rect of fixed height, so its text stays centred on the caret.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "text/InlineTexture.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace EditBox::IconRows {
|
||||
|
||||
namespace {
|
||||
|
||||
using PlaceCaret_t = void(__fastcall *)(void *editBox);
|
||||
using PlaceSelectionPiece_t = void(__fastcall *)(void *editBox, void *edx, void *piece, int from, int to);
|
||||
using IndexAtPoint_t = void(__fastcall *)(void *editBox, void *edx, float x, float y, int *index);
|
||||
using FontHeight_t = float(__fastcall *)(void *fs, void *edx, int mode);
|
||||
using MeasureRun_t = float(__fastcall *)(void *fs, void *edx, const uint8_t *text, int len);
|
||||
using PixelAlignY_t = float(__stdcall *)(float length);
|
||||
using GetRect_t = int(__fastcall *)(void *anchor, void *edx, float *out);
|
||||
using ClearAllPoints_t = void(__fastcall *)(void *anchor);
|
||||
using SetPoint_t = void(__thiscall *)(void *anchor, int point, void *relAnchor, int relPoint, float x, float y,
|
||||
int flag);
|
||||
using Resize_t = void(__fastcall *)(void *anchor, void *edx, int force);
|
||||
using Extent_t = float(__fastcall *)(void *anchor, void *edx);
|
||||
using ScreenAspect_t = float(__cdecl *)();
|
||||
using DivideByScreenScaleX_t = float(__stdcall *)(float value);
|
||||
using FireVariadic_t = void(__cdecl *)(void *frame, uint32_t *slotPtr, const char *fmt, ...);
|
||||
|
||||
PlaceCaret_t g_origPlaceCaret = nullptr;
|
||||
PlaceSelectionPiece_t g_origPlaceSelectionPiece = nullptr;
|
||||
IndexAtPoint_t g_origIndexAtPoint = nullptr;
|
||||
|
||||
void *AnchorOf(void *region) { return static_cast<uint8_t *>(region) + Offsets::OFF_REGION_ANCHOR; }
|
||||
|
||||
// A multi-line box's rows: its label, the buffer the line table indexes (the
|
||||
// '*' copy of a password box) and the table — each line's first byte, the
|
||||
// last entry the end.
|
||||
struct Rows {
|
||||
void *fs = nullptr;
|
||||
const uint8_t *text = nullptr;
|
||||
const uint32_t *starts = nullptr;
|
||||
int count = 0; // entries; lines = count - 1
|
||||
float scale = 1.0f; // the box's OFF_LAYOUT_SCALE
|
||||
};
|
||||
|
||||
bool MultiLineRows(void *editBox, Rows &rows) {
|
||||
const uint8_t mode = Game::Read<uint8_t>(editBox, Offsets::OFF_EDITBOX_MODE_FLAGS);
|
||||
if ((mode & Offsets::EDITBOX_MODE_MULTILINE) == 0)
|
||||
return false;
|
||||
rows.fs = Game::Read<void *>(editBox, Offsets::OFF_EDITBOX_FONTSTRING);
|
||||
rows.text = Game::Read<const uint8_t *>(editBox, (mode & Offsets::EDITBOX_MODE_PASSWORD)
|
||||
? Offsets::OFF_EDITBOX_TEXT_MASKED
|
||||
: Offsets::OFF_EDITBOX_TEXT);
|
||||
rows.starts = Game::Read<const uint32_t *>(editBox, Offsets::OFF_EDITBOX_LINE_STARTS);
|
||||
rows.count = Game::Read<int>(editBox, Offsets::OFF_EDITBOX_LINE_COUNT);
|
||||
rows.scale = Game::Read<float>(editBox, Offsets::OFF_LAYOUT_SCALE);
|
||||
return rows.fs != nullptr && rows.text != nullptr && rows.starts != nullptr && rows.count >= 2 &&
|
||||
rows.scale > 0.0f;
|
||||
}
|
||||
|
||||
// The line holding byte `pos` — the placers' walk down the table.
|
||||
int LineOf(const Rows &rows, uint32_t pos) {
|
||||
int line = 0;
|
||||
while (pos >= rows.starts[line + 1]) {
|
||||
if (line >= rows.count - 2)
|
||||
break;
|
||||
++line;
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
bool LineShift(const Rows &rows, int line, float &before, float &after) {
|
||||
before = 0.0f;
|
||||
after = 0.0f;
|
||||
const uint32_t start = rows.starts[line];
|
||||
const uint32_t end = rows.starts[line + 1];
|
||||
return end > start &&
|
||||
Text::InlineTexture::LineIconShift(rows.fs, rows.text + start, static_cast<int>(end - start), before, after);
|
||||
}
|
||||
|
||||
// How far the render moves line `line`'s text below its uniform row: the
|
||||
// moves of every line above it, and its own text's move.
|
||||
float ShiftOf(const Rows &rows, int line) {
|
||||
float shift = 0.0f;
|
||||
for (int i = 0; i <= line; ++i) {
|
||||
float before = 0.0f;
|
||||
float after = 0.0f;
|
||||
if (!LineShift(rows, i, before, after))
|
||||
continue;
|
||||
shift += before;
|
||||
if (i < line)
|
||||
shift += after;
|
||||
}
|
||||
return shift;
|
||||
}
|
||||
|
||||
// The placers' row in the box's parent units: the label's font height and
|
||||
// spacing, each pixel-aligned at the box's scale (PlaceCaret
|
||||
// 0x0077DAEB..0x0077DB0E).
|
||||
float RowHeight(const Rows &rows) {
|
||||
const float fontH = reinterpret_cast<FontHeight_t>(Offsets::FUN_FONTSTRING_FONT_HEIGHT)(rows.fs, nullptr, 1);
|
||||
const float spacing = Game::Read<float>(rows.fs, Offsets::OFF_FONTSTRING_SPACING);
|
||||
const auto align = reinterpret_cast<PixelAlignY_t>(Offsets::FUN_PIXEL_ALIGN_Y);
|
||||
return align(fontH * rows.scale) + align(spacing * rows.scale);
|
||||
}
|
||||
|
||||
float MeasureRun(const Rows &rows, uint32_t from, uint32_t to) {
|
||||
return reinterpret_cast<MeasureRun_t>(Offsets::FUN_FONTSTRING_MEASURE_SUBSTRING)(rows.fs, nullptr, rows.text + from,
|
||||
static_cast<int>(to - from));
|
||||
}
|
||||
|
||||
// The x the placers anchor byte `pos` of `line` at, and the label point they
|
||||
// anchor to, by the label's horizontal justify (OFF_FONTSTRING_FLAGS & 7).
|
||||
// False for none, which anchors nothing.
|
||||
bool AnchorX(const Rows &rows, int line, uint32_t pos, float &x, int &relPoint) {
|
||||
const uint32_t start = rows.starts[line];
|
||||
const uint32_t end = rows.starts[line + 1];
|
||||
switch (Game::Read<uint32_t>(rows.fs, Offsets::OFF_FONTSTRING_FLAGS) & 7u) {
|
||||
case 1: // LEFT
|
||||
x = (pos == start) ? 0.0f : MeasureRun(rows, start, pos);
|
||||
relPoint = Offsets::FRAMEPOINT_TOPLEFT;
|
||||
return true;
|
||||
case 2: // CENTER
|
||||
x = (pos == start) ? 0.0f : MeasureRun(rows, start, pos);
|
||||
x -= MeasureRun(rows, start, end) * 0.5f;
|
||||
relPoint = Offsets::FRAMEPOINT_TOP;
|
||||
return true;
|
||||
case 4: // RIGHT
|
||||
x = (pos == end) ? 0.0f : -MeasureRun(rows, pos, end);
|
||||
relPoint = Offsets::FRAMEPOINT_TOPRIGHT;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Anchors `region` as the placer did, at `y`.
|
||||
void Anchor(const Rows &rows, void *region, int relPoint, float x, float y, int flag) {
|
||||
void *anchor = AnchorOf(region);
|
||||
reinterpret_cast<ClearAllPoints_t>(Offsets::FUN_LAYOUT_CLEAR_ALL_POINTS)(anchor);
|
||||
reinterpret_cast<SetPoint_t>(Offsets::FUN_REGION_SET_POINT)(anchor, Offsets::FRAMEPOINT_TOPLEFT, AnchorOf(rows.fs),
|
||||
relPoint, x, y, flag);
|
||||
}
|
||||
|
||||
// --- the click ---------------------------------------------------------------
|
||||
|
||||
void __fastcall IndexAtPoint_h(void *editBox, void *edx, float x, float y, int *index) {
|
||||
Rows rows;
|
||||
if (!MultiLineRows(editBox, rows))
|
||||
return g_origIndexAtPoint(editBox, edx, x, y, index);
|
||||
float rect[4] = {0.0f, 0.0f, 0.0f, 0.0f}; // bottom, left, top, right
|
||||
if (reinterpret_cast<GetRect_t>(Offsets::FUN_REGION_GET_RECT)(AnchorOf(rows.fs), nullptr, rect) == 0)
|
||||
return g_origIndexAtPoint(editBox, edx, x, y, index);
|
||||
// The engine walks rows down from the label's top while the click is
|
||||
// below the row (strictly), stopping at the last line. Walk the rows as
|
||||
// the render lays them out: each taller by its line's moves.
|
||||
const float row = RowHeight(rows);
|
||||
const float below = rect[2] - y;
|
||||
int line = 0;
|
||||
float top = 0.0f; // the icon-aware top of `line`, below the label's top
|
||||
bool moved = false;
|
||||
for (;;) {
|
||||
float before = 0.0f;
|
||||
float after = 0.0f;
|
||||
if (LineShift(rows, line, before, after))
|
||||
moved = true;
|
||||
const float height = row + (before + after) * rows.scale;
|
||||
if (!(below > top + height) || line >= rows.count - 2)
|
||||
break;
|
||||
top += height;
|
||||
++line;
|
||||
}
|
||||
// Without a move the rows are the engine's own; with one, the click is
|
||||
// moved onto the middle of the uniform row the engine maps to `line`.
|
||||
const float yMapped = moved ? rect[2] - (static_cast<float>(line) + 0.5f) * row : y;
|
||||
g_origIndexAtPoint(editBox, edx, x, yMapped, index);
|
||||
}
|
||||
|
||||
// --- a selection piece --------------------------------------------------------
|
||||
|
||||
void __fastcall PlaceSelectionPiece_h(void *editBox, void *edx, void *piece, int from, int to) {
|
||||
Rows rows;
|
||||
if (!MultiLineRows(editBox, rows) || from < 0)
|
||||
return g_origPlaceSelectionPiece(editBox, edx, piece, from, to);
|
||||
const int line = LineOf(rows, static_cast<uint32_t>(from));
|
||||
const float shift = ShiftOf(rows, line);
|
||||
g_origPlaceSelectionPiece(editBox, edx, piece, from, to);
|
||||
if (!(shift > 0.0f) || piece == nullptr)
|
||||
return;
|
||||
// The piece starts at `from`, clamped to its line.
|
||||
uint32_t pos = static_cast<uint32_t>(from);
|
||||
if (pos < rows.starts[line])
|
||||
pos = rows.starts[line];
|
||||
float x = 0.0f;
|
||||
int relPoint = 0;
|
||||
if (!AnchorX(rows, line, pos, x, relPoint))
|
||||
return;
|
||||
const float y = -(RowHeight(rows) / rows.scale) * static_cast<float>(line) - shift;
|
||||
Anchor(rows, piece, relPoint, x, y, 0);
|
||||
reinterpret_cast<Resize_t>(Offsets::FUN_LAYOUT_RESIZE)(AnchorOf(piece), nullptr, 1);
|
||||
}
|
||||
|
||||
// --- the caret ----------------------------------------------------------------
|
||||
|
||||
void __fastcall PlaceCaret_h(void *editBox) {
|
||||
Rows rows;
|
||||
if (!MultiLineRows(editBox, rows))
|
||||
return g_origPlaceCaret(editBox);
|
||||
const int cursor = Game::Read<int>(editBox, Offsets::OFF_EDITBOX_CURSOR_BYTE);
|
||||
const int visibleStart = Game::Read<int>(editBox, Offsets::OFF_EDITBOX_VISIBLE_START);
|
||||
const int visibleLength = Game::Read<int>(editBox, Offsets::OFF_EDITBOX_VISIBLE_LENGTH);
|
||||
// Outside the window the engine hides the caret and fires nothing.
|
||||
if (cursor < visibleStart || cursor > visibleStart + visibleLength)
|
||||
return g_origPlaceCaret(editBox);
|
||||
const int line = LineOf(rows, static_cast<uint32_t>(cursor));
|
||||
const float shift = ShiftOf(rows, line);
|
||||
if (!(shift > 0.0f))
|
||||
return g_origPlaceCaret(editBox);
|
||||
uint32_t *slot = Game::Ptr<uint32_t>(editBox, Offsets::OFF_EDITBOX_ONCURSORCHANGED_SLOT);
|
||||
const uint32_t handler = slot[0];
|
||||
slot[0] = 0;
|
||||
g_origPlaceCaret(editBox);
|
||||
slot[0] = handler;
|
||||
void *caret = Game::Read<void *>(editBox, Offsets::OFF_EDITBOX_CARET);
|
||||
if (caret == nullptr)
|
||||
return;
|
||||
const float y = -(RowHeight(rows) / rows.scale) * static_cast<float>(line) - shift;
|
||||
float x = 0.0f;
|
||||
int relPoint = 0;
|
||||
if (AnchorX(rows, line, static_cast<uint32_t>(cursor), x, relPoint))
|
||||
Anchor(rows, caret, relPoint, x, y, 1);
|
||||
if (handler == 0)
|
||||
return;
|
||||
// OnCursorChanged(x, y, width, height) as the engine fires it: each
|
||||
// value by the anchor→script factor, pushed as a double.
|
||||
void *anchor = AnchorOf(caret);
|
||||
void **vtable = *static_cast<void ***>(anchor);
|
||||
const float width = reinterpret_cast<Extent_t>(vtable[7])(anchor, nullptr); // +0x1C GetWidth
|
||||
const float height = reinterpret_cast<Extent_t>(vtable[8])(anchor, nullptr); // +0x20 GetHeight
|
||||
const auto aspect = reinterpret_cast<ScreenAspect_t>(Offsets::FUN_COORD_GET_SCALED_SCREEN_ASPECT);
|
||||
const auto divide = reinterpret_cast<DivideByScreenScaleX_t>(Offsets::FUN_COORD_DIVIDE_BY_SCREEN_SCALE_X);
|
||||
const float factor = Game::Read<float>(Offsets::FLOAT_SCRIPT_COORD_SCALE);
|
||||
const auto push = [&](float value) { return static_cast<double>(divide(aspect() * factor * value)); };
|
||||
const double args[4] = {push(x), push(y), push(width), push(height)};
|
||||
void *L = Game::Lua::State();
|
||||
const int savedTop = (L != nullptr) ? Game::Lua::GetTop(L) : 0;
|
||||
reinterpret_cast<FireVariadic_t>(Offsets::FUN_FRAME_RUN_SCRIPT_VARIADIC)(
|
||||
editBox, slot, reinterpret_cast<const char *>(Offsets::VAR_SCRIPT_FMT_FFFF), args[0], args[1], args[2],
|
||||
args[3]);
|
||||
if (L != nullptr)
|
||||
Game::Lua::SetTop(L, savedTop);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _caretHook{Offsets::FUN_EDITBOX_PLACE_CARET, reinterpret_cast<void *>(&PlaceCaret_h),
|
||||
reinterpret_cast<void **>(&g_origPlaceCaret)};
|
||||
const Game::HookAutoRegister _pieceHook{Offsets::FUN_EDITBOX_PLACE_SELECTION_PIECE,
|
||||
reinterpret_cast<void *>(&PlaceSelectionPiece_h),
|
||||
reinterpret_cast<void **>(&g_origPlaceSelectionPiece)};
|
||||
const Game::HookAutoRegister _clickHook{Offsets::FUN_EDITBOX_INDEX_AT_POINT,
|
||||
reinterpret_cast<void *>(&IndexAtPoint_h),
|
||||
reinterpret_cast<void **>(&g_origIndexAtPoint)};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace EditBox::IconRows
|
||||
@@ -2234,6 +2234,32 @@ void PrepareForReload() {
|
||||
g_nodeBakedColor.clear();
|
||||
}
|
||||
|
||||
bool LineIconShift(void *fs, const uint8_t *text, int len, float &before, float &after) {
|
||||
before = 0.0f;
|
||||
after = 0.0f;
|
||||
if (!g_inlineEnabled || !LooksReadable(fs) || !LooksReadable(text) || len <= 0 ||
|
||||
!HasInlineTexture(text, len))
|
||||
return false;
|
||||
FsPenFont pf;
|
||||
if (!ResolveFsPenFont(fs, pf))
|
||||
return false;
|
||||
const float overflow = IconOverflowPx(text, len, pf.fontHPen, pf.layoutScale);
|
||||
if (!(overflow > 0.0f))
|
||||
return false;
|
||||
// The node's justify-V as the block creator (FUN_0044d420) reads the
|
||||
// style word: TOP (8) wins, then BOTTOM (0x20), else middle — the value
|
||||
// the emitter reads from the node to split the move.
|
||||
const uint32_t style = Game::Read<uint32_t>(fs, Offsets::OFF_FONTSTRING_FLAGS);
|
||||
const int justifyV = (style & 0x8u) ? 0 : (style & 0x20u) ? 2 : 1;
|
||||
const float move = (justifyV == 1) ? std::floor(overflow * 0.5f) : overflow;
|
||||
const float toFs = pf.fontHInt / pf.fontHPen; // pen px → the getter's units
|
||||
if (justifyV != 0)
|
||||
before = move * toFs;
|
||||
if (justifyV != 2)
|
||||
after = move * toFs;
|
||||
return true;
|
||||
}
|
||||
|
||||
static const Game::ReloadAutoRegister _reloadReg{&PrepareForReload};
|
||||
|
||||
} // namespace Text::InlineTexture
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Text::InlineTexture {
|
||||
|
||||
// Called from FrameScript_Initialize_h before the /reload teardown: the reload
|
||||
@@ -20,4 +22,13 @@ namespace Text::InlineTexture {
|
||||
// icon records are dropped (they rebuild as the reloaded UI re-emits its text).
|
||||
void PrepareForReload();
|
||||
|
||||
// The room the tall inline icons on the line [text, text + len) take when
|
||||
// `fs` renders it, in the font string's own units (its height getter's):
|
||||
// `before` is how far the render moves that line's text down, `after` how
|
||||
// far it moves the lines after it — 4.3.4's InitializeText offsets, split by
|
||||
// the font string's vertical justify (top: none / all; middle: half / half,
|
||||
// floored; bottom: all / none). Both 0, and false, without an icon taller
|
||||
// than the font or with the feature off.
|
||||
bool LineIconShift(void *fs, const uint8_t *text, int len, float &before, float &after);
|
||||
|
||||
} // namespace Text::InlineTexture
|
||||
|
||||
Reference in New Issue
Block a user