Repository navigation
refactor(items): build the item tooltip's lines apart from drawing them - #821
Conversation
RenderItemInfo() is two functions now: BuildItemTooltipText() writes the tooltip's lines into TextList / TextListColor / TextBold as before, and RenderItemInfo() draws them (the Dark Lord's pets still draw their own). The code only moves, in the same order: the early returns of the item, the pet check and the set and socket lines stay in the builder, the height and position of the box in the drawing. Nothing changes in game. BuildItemTooltipLines() returns the built lines without drawing, and two control socket commands use it (in builds with the control socket): - `tooltip` gives the lines of an item made from the fields the server sends (level, luck, skill, option, excellent, ancient, harmony); - `tooltip-snapshot` writes the lines of every item at every level, as plain, full, ancient and harmony items, for the hero as he is, to a text file: 60,736 tooltips in about a second, the same every run, to compare the tooltip code before and after a change. This is step 5b-1 of sven-n#815: the snapshot is the check for moving the stat lines into data.
The tooltip and tooltip-snapshot commands this branch adds were missing from the test's list, so the editor build's "Control protocol serves the documented command vocabulary" counted 36 commands against 34.
A line break inside its text split the row in two and broke the commands table.
sven-n
left a comment
There was a problem hiding this comment.
Bug: the static debounced pet-info callback captures the first tooltip item forever. In src/source/Engine/Object/ZzzInventory.cpp, RenderItemInfo, the new static DebouncedAction debouncedPetInfoRequest([ip, Inventype] { ... }) initializes its lambda only on the first Dark Lord pet tooltip. Subsequent tooltips reuse the first ip and Inventype, potentially after the item was destroyed, and can request the wrong pet information or dereference a dangling pointer. Please make the callback use current item/context at invocation time rather than capturing a transient ITEM* in static storage; add a test that alternates raven/horse items and inventory types.
Input validation: src/source/App/Control/ControlCommandsQuery.cpp, ItemTooltip: the level, durability, option, excellent, ancient, harmony, and harmony_level integers are cast directly to BYTE without bounds checks. Values outside 0..255 wrap and may create unintended item states. Reject invalid ranges with bad_request and cover boundaries. Also check output stream state after ItemTooltipSnapshot writes.
Reviewed against AGENTS.md and docs/CODING_RULES.md; build not run.
… tooltip commands refuse what wraps The static debounced pet information request captured the first Dark Lord pet tooltip's item and storage for good; it reads the ones of the latest call now, and GetPetInfoRequest, which picks the request, is tested with the raven and the horse in turn from other storages. The control socket's tooltip command refuses a level, durability, option, excellent, ancient or harmony value its item's fields can't hold instead of wrapping it, and tooltip-snapshot reports a file it couldn't write completely.
… the item data, and the gate's lines are formatted
|
Thanks! Both addressed: Pet info callback. Right, and it predates this PR (main has the same Control socket input. Built and tested locally (all tests pass), and CI's quality gate (clang-format 21.1.8 on the changed lines, cppcheck 2.13 with CI's flags) passes locally. Carried through #822–#836. |
Mosch0512
left a comment
There was a problem hiding this comment.
Verdict: changes requested for one item; the rest is non-blocking. The split itself is right:
- Nothing changes in game. The builder runs the old code up to the same point, and the code just before the split is identical. The old block that added the set and socket lines and then measured the box is now two blocks under the same
!bItemTextListBoxUsecondition. The drawing half only needsp, which it looks up again. - Every early exit is covered:
Nothingfor the two returns,DarkLordPetfor the pets, which still draw their own tooltip, in the same order as before. - The pet request fix is right.
DebouncedActionruns the callback insideinvoke(), right after the two statics are set, so it always reads the item being drawn.GetPetInfoRequestkeeps the old slot rules. - The snapshot is safe to run in game:
- The builder only reads game state: no server requests, effects or statics.
ShowLegacyTextListcopies the lines when it is called, so the commands rewriting the shared text list between frames can't touch a shown tooltip.- The temporary items are freed again (ref count 1 → 0).
Required
tooltip-snapshotemptiesoutbefore it checksgroup(inline, ControlCommandsQuery.cpp:638).
Non-blocking
tooltipanswers with a tooltip of nothing for an item number no item uses (inline, ControlCommandsQuery.cpp:555).- The snapshot's
outisn't resolved likescreenshot's (inline, ControlCommandsQuery.cpp:684). harmony_levelwithoutharmonyis silently ignored (inline, ControlCommandsQuery.cpp:610).- The docs don't say that per-group requests overwrite
out(inline, control-socket.md:85). - The pet request passes its item through two statics (inline, ZzzInventory.cpp:4407).
Before merging (not code)
- The branch needs main. In a random-order run of
test_item_database_files, "Item Requirements does not store a +0 value that needs a weight picked when its window is hidden" fails. That's the order-dependent editor bug fixed by #854, which is on main but not on this branch yet. - CI doesn't compile the new commands. The jobs that ran for this PR use presets with
ENABLE_CONTROL_SOCKEToff, and the editor-ON jobs were skipped. SoControlCommandsQuery.cpp's new code andtest_control_tooltip_commandsare only checked by local editor builds so far.
What I checked:
- the builder and the drawing half against the old function;
- the pet request against the old capture and against
DebouncedAction; - the builder for side effects, since the snapshot runs it on 60,000 made-up items;
- the item manager's create and delete, and how the tooltip copies its lines;
- the input checks and their test.
Built on Windows x64 Debug with the editor and the control socket: test_item_database, test_item_database_files, test_control_tooltip_commands and test_control_protocol pass in file order. I didn't run the two commands against a server.
| return EncodeError(request.EncodedId(), ErrorCode::BadRequest, | ||
| "`tooltip-snapshot` needs `out`, the file to write"); | ||
| } | ||
| std::ofstream out(std::filesystem::path(Core::Text::FromUtf8(path)), std::ios::binary); |
There was a problem hiding this comment.
Required. The ofstream is constructed, and so the file truncated, before group is checked. A request with a bad group therefore empties out and only then answers bad_request.
For example, record {"cmd":"tooltip-snapshot","out":"before.txt","group":0}, change the code, then mistype {"out":"before.txt","group":16} while meaning after.txt. The reply is bad_request, but before.txt is now empty, and the "before" snapshot the whole step relies on is gone. Checking group (and anything else that can fail) before the ofstream is constructed fixes it.
There was a problem hiding this comment.
Fixed in 78e5861: group is checked before out is opened, so a refused request leaves the file as it was. A test writes a file, sends group: 16 and checks the file still holds what it did.
| { | ||
| int group = -1; | ||
| int number = -1; | ||
| if (!request.GetInt("group", group) || !request.GetInt("number", number) || group < 0 || group >= MAX_ITEM_TYPE || |
There was a problem hiding this comment.
Non-blocking. This only range-checks group and number. For a number no item uses, CreateItemByParameters still makes an item, and the reply is a tooltip of an empty item rather than an error. That looks like a valid answer to a script with a typo'd number. The "the item could not be made" branch can't trigger either, because CreateItemByParameters returns null only for a null pointer. Refusing with bad_request when g_ItemDatabase.Find(type) is null, as the snapshot skips items that don't Exists(), would make that branch real.
There was a problem hiding this comment.
Done in 78e5861: tooltip answers bad_request ("no item has group G and number N") when g_ItemDatabase.Find(group, number) is null. Since CreateItemByParameters can't fail for an item that exists, I dropped the unreachable "could not be made" branch and TooltipOf returns the lines directly. Tested.
|
|
||
| json result; | ||
| result["tooltips"] = tooltips; | ||
| result["out"] = path; |
There was a problem hiding this comment.
Non-blocking. screenshot resolves its out with ResolveScreenshotPath (UTF-8 to a wide, absolute path) and reports the path it wrote. This command opens out relative to the client's working directory and replies with the path as given. So {"out":"before.txt"} lands in whatever folder the client runs in, and the reply only says "before.txt". Using ResolveScreenshotPath for both opening and the reply would keep the two file-writing commands alike.
There was a problem hiding this comment.
Done in 78e5861: the snapshot opens ResolveScreenshotPath(out) and replies with that absolute path, as screenshot does. A test sends a relative out and checks the reply names the absolute path.
| { | ||
| params.HasHarmonyOption = true; | ||
| params.HarmonyOptionType = static_cast<BYTE>(harmony); | ||
| if (harmonyLevel >= 0) |
There was a problem hiding this comment.
Non-blocking. harmony_level only counts inside if (harmony > 0). So {"group":0,"number":0,"harmony_level":13} replies with a plain tooltip and no error, and harmony without harmony_level silently uses level 0. A bad_request for harmony_level without harmony would catch the forgotten field.
There was a problem hiding this comment.
Done in 78e5861: harmony_level without harmony is a bad_request ("harmony_level needs harmony"), with a test. harmony alone still uses level 0, as documented.
| | `ui` | the open windows by name — `inventory`, `inventory_extension`, `character`, `trade`, `storage`, `storage_extension`, `mix`, `npc_shop`, `lucky_item`, `chat_input`, `party`, `command`, `my_shop`, `purchase_shop`, `npc_quest` (the quest dialog of Sebina, Marlon and Devin), and `message_box` while a dialog waits for Enter or Esc — and the window pixels of the named elements that are shown: `trade.confirm`, `trade.zen`, `inventory.repair`, `inventory.my_shop`, `npc_shop.repair`, `npc_shop.repair_all`, `my_shop.title`, `my_shop.open`, `my_shop.close`, `command.trade`, `command.purchase`, `command.party`, `npc_quest.answer.0` and on (the rows of the quest dialog's answers), `npc_quest.complete`, `npc_quest.close`, `character.stat.strength` (`agility`, `vitality`, `energy`, `command`: the character window's "+" buttons, while there are level-up points) | | ||
| | `slot-pixel` (`grid`, `slot`) | the window pixel of a slot's square: `inventory` and `equipment` (the slot numbers `state` reports), `trade`, `trade_partner`, `storage`, `mix`, `npc_shop`, and the personal shops `my_shop` and `purchase_shop` (slots 204 and up, as `state` and the server number them); `not_open` while that window is closed, `bad_request` for a slot the grid does not have | | ||
| | `tooltip` (`group`, `number`, `level`, `durability`, `luck`, `skill`, `option`, `excellent`, `ancient`, `harmony`, `harmony_level`) | the lines of an item's information tooltip, as the inventory builds them for an item made from these fields (as the server would send it; left out: 0, full durability) and the hero as he is, without drawing them: `lines`, each with `text` (`"\n"` for a blank line), `color` (the `TEXT_COLOR_*` number) and `bold` | | ||
| | `tooltip-snapshot` (`out`, `group`) | writes the `tooltip` lines of every item (of `group` only, when given: one group per request keeps the frame short) at every level 0 to 15, as plain, full (luck, skill, +4 option, every excellent option), ancient (set 1) and Jewel of Harmony (option 1 at level 13) items, for the hero as he is, to the text file `out`: a line `<group>/<number> <variant> +<level>`, then one line ` <color> [bold ]<text>` per tooltip line (`<blank>` for a blank line). Two files compare with any diff tool, e.g. before and after a change of the tooltip code; replies `tooltips`, the count | |
There was a problem hiding this comment.
Non-blocking. The row recommends one group per request, but each request truncates out. A full snapshot by groups therefore needs 16 files; with one out it ends up holding the last group only. A note ("use one file per group"), or an append option, avoids that. It could also say that the Dark Horse and the Dark Raven get a header but no lines, since the pet manager draws their tooltip.
There was a problem hiding this comment.
Done in 78e5861: the row now says each request writes out anew, so a snapshot by groups takes one file per group, and that the Dark Horse and the Dark Raven get their header line but no lines.
| { | ||
| // The request is made once a second at most, for the pet whose tooltip is drawn then: the callback lives on, | ||
| // so it reads the item of this call, not one it was made with. | ||
| static int petItemType = -1; |
There was a problem hiding this comment.
Non-blocking. This is correct, since invoke() runs the callback synchronously right after these assignments, but it takes the comment to see why. A throttle that keeps only the last request time would need no shared state: it calls GetPetInfoRequest(ip->Type, g_pMyInventory->GetPointedItemIndex(), Inventype) directly when a second has passed.
There was a problem hiding this comment.
Done in 78e5861: it keeps only the last request time and calls GetPetInfoRequest(ip->Type, GetPointedItemIndex(), Inventype) directly when a second has passed. No statics for the item any more, and Debouncer.h had no other user, so it's gone.
…ything - tooltip-snapshot checks `group` before opening `out`, so a refused request no longer empties a recorded snapshot; `out` is resolved like screenshot's and the reply names the absolute path written. - tooltip refuses an item number no item uses and a `harmony_level` without `harmony`, instead of answering with an empty or a plain tooltip. - The Dark Lord pet's information request keeps only its last request time instead of passing the item through two statics; the debouncer goes. - control-socket.md: one file per group, and the pets' header-only entries.
|
Thanks for the review. Pushed 78e5861 on top of a merge of main:
I didn't run the commands against a server for this round; the request validation and path handling are covered by the new tests; the pet throttle has no test and wasn't tried in game. |
This step's own changes: compare with #820. The diff of this PR also holds every earlier step of #815, since a PR from a fork can only target this repository's branches.
Summary
Step 5b-1 of #815 (data-driven item tooltips; design). Stacked on #820 (→ #819 → … → #813): only the last commit,
refactor(items): build the item tooltip's lines apart from drawing them, is this step.Before the stat lines move into data (5b-3), there must be a way to check that a tooltip didn't change, for every item rather than a few. This PR adds it.
The split
RenderItemInfo()(ZzzInventory.cpp) is two functions now:BuildItemTooltipText()writes the lines intoTextList/TextListColor/TextBold, as before.RenderItemInfo()draws them. The Dark Lord's pets still draw their own tooltip.The code only moves, and in the same order:
Nothing changes in game.
The snapshot
BuildItemTooltipLines()returns the built lines without drawing them. Two control socket commands use it; they exist only in builds with the control socket (docs/control-socket.md):tooltip: the lines of one item made from the fields the server sends (group,number,level,luck,skill,option,excellent,ancient,harmony, …), for the hero as he is.tooltip-snapshot: the lines of every item at every level 0–15, as plain, full (luck, skill, +4 option, every excellent option), ancient and harmony items, written to a text file. One request per item group (group) keeps the frame short.A part of the file:
On the local OpenMU:
tooltip.So the steps after this one record a snapshot before and after their change, with a hero that meets the requirements and one that doesn't, and compare them with any diff tool.
Next (5b-1, continued)
The builder still reads the hero and the UI directly (
CharacterAttribute,g_pNewUISystem,g_pMyInventory, the pet, socket and harmony managers). Moving those reads behind aTooltipContext, one subsystem at a time and each checked with the snapshot, lets the same loop run inctestwith a made-up hero. Then the snapshot check runs in CI too.Screenshots
None: nothing changes in game. I checked the tooltips by hand in both themes after the split (the Wing of Storm +15 and the Jewel of Bless, as in #818 and #816).
Related issues
Step 5b-1 of #815. Builds on #820, #819, #818, #817, #816 and #813.
Checklist
docs/CODING_RULES.md.docs/build/README.md).windows-x64-mueditor) builds, and both commands were run against a local OpenMU as above.