Repository navigation
feat(items): item descriptions in the item data - #813
Conversation
A "description" list in the item data: lines the tooltip shows for an item after its own, each with a text written like the item's name (plain English, or texts by language; "" is a blank line) and an optional tooltip color by name. Read, written (left out when empty), kept by the bmd import, and checked: a line longer than the tooltip's 99 characters gets a warning. The tooltip draws them in the UI's language. So a new item's text is data instead of another case in the tooltip code. No shipped item has one yet; the original items keep their texts in the tooltip code.
A description line longer than the tooltip's widest lines wraps there, measured with the tooltip's font, and a "\n" in a text breaks the line, so long texts need neither hand-split lines nor the 99-character limit and its warning, which go. Each piece keeps the line's color.
f155727 to
efcd7ff
Compare
…iptions # Conflicts: # src/source/Data/GameData/ItemData/ItemEnumNames.cpp # src/source/Data/GameData/ItemData/ItemEnumNames.h
Mosch0512
left a comment
There was a problem hiding this comment.
Verdict: changes requested for two items; the rest is non-blocking. The format is a good first step for #815. The description lines are LocalizedStrings like item names, as phase 8 of the items design asks; colors are names checked at load; and nothing changes in game. I also looked at the later steps (#816 to #836). They only add fields to ItemDescriptionLine, so data written for this step stays valid. The drawing code, though, is carried over almost unchanged to the end of the stack (WriteItemDescriptionLines in #836), so the two bugs below survive all 16 PRs unless they're fixed here.
Required
- A blank line inside a text (
"a\n\nb", or a text that starts with\n) cuts off the rest of the tooltip (inline, ZzzInventory.cpp:3754). - The 50-row check stops the description, but the rows after it are written without a check, so a long description writes past the end of
TextList(inline, ZzzInventory.cpp:3752).
Non-blocking
- Description texts get none of the checks that item names get: English required, valid language codes, a warning for languages the game can't show (inline, ItemJsonFormat.cpp:409).
- The wrap measures the line again after every letter, every frame, through SDL_ttf's shared text cache (inline, ZzzInventory.cpp:3735).
- Rule 1 of the coding rules: the new block could be its own function (inline, ZzzInventory.cpp:3730).
- The wrap width is a tooltip value in the item data header (inline, ItemDefinition.h:96).
- The docs say the lines come "after its stats", but they come before durability, the requirements and the options (inline, item-data.md:84).
- No test covers turning a description into tooltip rows (inline, test_item_json.cpp:288).
- The color name table sits outside the file's anonymous namespace and is sized with a literal 14 (inline, ItemEnumNames.cpp:94).
%.99ls(ZzzInventory.cpp:3754) hard-codesTextList's row width."%.*ls"withstd::size(TextList[0]) - 1ties it to the array.- The Dark Horse and the Dark Raven show no description outside shops. Their tooltip goes to
giPetManager::RenderPetItemInfoand returns at ZzzInventory.cpp:1662, before the description is written. That line isn't in the diff, so there's no inline comment. Either draw the description there too, or mention the exception in the docs. - The items design doesn't know about this yet. D15 and phase 8 plan the tooltips as JSON of their own, converted from the
ItemTooltip*files. D22 keeps client-only display data out of the item files ("the item files keep what client and server share, and only those take part in the OpenMU exchange"). This PR puts client-only tooltip texts into the item files, as #815 plans. That split makes sense, especially with the shared texts and the layout inData/Items/Tooltips/Tooltip.jsonlater in the stack. But it should be written into D15, D22 and phase 8 in this PR, where the decision is made. Phase 7's exchange file then has to leavedescriptionout. - The first commit's message still says a line longer than 99 characters gets a warning. The wrap commit removed that check.
Follow-up (the stack)
- All 16 PRs of #815 target
main, so each diff holds every earlier step (#836 is +16,089/−12,674). On GitHub, a reviewer can't see one step's own change. Basing each PR on the previous step's branch would let each step be reviewed on its own.
What I checked: I read the diff, the code around it in RenderItemInfo, both tooltip renderers (RenderTipTextList and the RmlUi tooltip's BuildLines), WrapTextToWidth, the SDL_ttf measuring and the item data checks. I also read the items design, the roadmap and the later steps up to #836. I built WrapTextToWidth on its own with g++ and ran it on texts with blank lines. I didn't build the client or run the game. CI: no checks ran on this PR.
| { | ||
| if (TextNum >= static_cast<int>(std::size(TextList))) | ||
| break; | ||
| mu_swprintf(TextList[TextNum], L"%.99ls", piece.c_str()); |
There was a problem hiding this comment.
Required. WrapTextToWidth returns an empty piece for a blank line inside a text. I ran it on a few texts:
"Line one\n\nLine two"gives[Line one] [] [Line two]"\nLeading break"gives[] [Leading break]"One\n \nTwo"gives[One] [] [Two]
The empty piece is written here as L"". Both tooltip renderers treat an empty row as the end of the list: BuildLines (LegacyTextListTooltip.cpp:46) and RenderTipTextList (line 282). The height count at line 5156 stops there too. So everything after it is gone: the rest of the description, the jewel's own text, durability, requirements, options, the set and socket lines and the expiry date.
The docs say \n breaks the line, so \n\n is the natural way to leave a gap. No shipped text uses \n yet, also not in the later steps. So this shows up first with custom item data, or with the tooltip editor of step 6.
Writing L"\n" for an empty piece fixes it. The blank-line branch above then only has to produce one L"\n" piece, so both cases can share this loop.
There was a problem hiding this comment.
Confirmed: WrapTextToWidth returns an empty piece for a blank line inside a text, and BuildLines, RenderTipTextList and the height count all stop at an empty row. Fixed in f62aa0c: a blank piece is written as the tooltip's own blank row ("\n"), the same way as a blank line, in one loop. Test: "A blank line inside a description text is a blank tooltip row, not the tooltip's end" ("a\n\nb", a leading "\n", a blank line). It fails on the old code (3 checks) and passes now.
| const std::wstring text = Core::Text::FromUtf8(line.text.Get(g_ItemDatabase.GetDisplayLocale())); | ||
| for (const std::wstring& piece : WrapTextToWidth(text, Data::Items::ItemDescriptionWrapWidth, measure)) | ||
| { | ||
| if (TextNum >= static_cast<int>(std::size(TextList))) |
There was a problem hiding this comment.
Required. This check stops the description at 50 rows, but the code after it adds rows without a check. Line 4730 writes L"\n" into TextList[TextNum] for every item. The jewel lines from line 3761, the options, RenderSetOptionListInItem, the socket lines and the expiry date follow. So when the description fills TextList, the next write goes to TextList[50], past the array into the globals next to it. ShowLegacyTextList then reads rows beyond 50 too.
A description that wraps to about 45 pieces is enough, for example in a server's own item files. Nothing limits its size at load: the first commit's ValidateDescription went away with the wrap commit.
A limit at load (an issue for a description with more than a set number of lines), or stopping here with room left for the rest of the tooltip, would fix it.
There was a problem hiding this comment.
Confirmed: TextList is [50][100], and from line 4730 on nothing checks the bounds. Fixed in f62aa0c by giving the description rows of its own:
TextList,TextListColor,TextBoldandSizehaveMaxItemDescriptionRows(32) rows on top of the original 50 (MaxTextListRows).- The description writes at most those 32, so it never takes the room the rest of the tooltip had before descriptions existed.
- Six files declared the arrays themselves with 50 rows. They use
ZzzInventory.h's declarations now; MSVC's decorated names include the size, so a stale one fails to link rather than going unnoticed.
Later in the stack (#830) the description has a second part after the options; both parts share the 32 rows through a budget the caller holds. Test: "A description writes at most its own rows, leaving the tooltip's rows to the rest".
| } | ||
|
|
||
| ItemDescriptionLine line; | ||
| const bool read = Json::ReadLocalizedString(*text, field + "." + Keys::Text, line.text, |
There was a problem hiding this comment.
Non-blocking. The docs say text "is written like name", but names get checks that description texts don't (items design, section 2 "Validation"; ValidateName and ValidateNameLocales):
- a missing English text is an error;
- a language code may only have letters, digits and
-; - a language the game has no UI texts for gets a warning.
Without these checks, {"text": {"de": "…"}}, or a typo like {"DE": "…"}, loads with no issue. In English the line then just disappears: Get returns the empty English text, WrapTextToWidth returns nothing, and not even a blank line is drawn. From step 2 on, many translated texts move into the data, so these checks pay off early. Missing description translations aren't counted anywhere either, unlike names in Item Translations, but that can come with the editor.
There was a problem hiding this comment.
Agreed, done in f62aa0c. A description text gets the checks a name gets:
- the English text is required, except for a blank line;
- a language code has only letters, digits and
-; - a language the game has no texts in is a warning (with the names' check in
ValidateNameLocales).
The same checks went into the later steps, where their texts appear: Tooltip.json's texts (#824), level values (#827) and name lines (#833). Every language in the shipped data is a UI language, so no new warnings. The missing-translation counts can come with the editor (step 6), as you say.
| if (const Data::Items::ItemDefinition* definition = g_ItemDatabase.Find(ip->Type)) | ||
| { | ||
| g_pRenderText->SetFont(g_hFont); | ||
| const auto measure = [](const wchar_t* text, size_t length) |
There was a problem hiding this comment.
Non-blocking. The tooltip is built every frame while an item is hovered. WrapTextToWidth calls this lambda after every letter, with the whole line so far. Each call goes through SdlTtfTextCache::Prepare: a linear search over up to 256 entries and, for a new string, TTF_CreateText with shaping. Every prefix is a new string, so a 150-letter description adds about 130 entries to a cache that all UI text shares. When that cache is full it is emptied completely, and every text on screen is created again.
Measuring only when a word ends would make this cheap. The language could also be picked at load, as SetDisplayLocale does for name; section 1 of the items design asks for "No hash maps or string lookups at runtime". Once placeholders come in step 3, the filled text depends on the item and the hero, so the wrap itself has to stay per tooltip. Cheap measuring is what matters.
This lambda is also the third copy of the same measure function, after MeasureInReferenceUnits (ChatCommandWindow.cpp:65) and the one in CustomMessageBox.cpp:47.
There was a problem hiding this comment.
Confirmed: the line was measured after every letter, and each prefix was a new entry in the shared text cache. Fixed in 94af869:
WrapTextToWidthmeasures a line once per word ending.- It measures letter by letter only inside a word wider than a whole line.
I compared it with the old version on 200,000 random texts with characters of different widths:
- the lines are the same, except where the old version left a line wider than the limit (a word too wide for a line, after a break);
- the new one has about half the measure calls on that mix, and fewer on real text with longer words.
The language is still picked per line. With placeholders from step 3 on, the text is filled per tooltip anyway, as you note, so measuring was the costly part. The measure function stays separate from MeasureInReferenceUnits and the message box's: those measure in other units (reference units, the message box's font), the tooltip in its own font.
| TextListColor[TextNum] = TEXT_COLOR_WHITE; TextBold[TextNum] = false; TextNum++; | ||
| } | ||
| } | ||
| // The item data's description in the UI's language: each line wrapped to the tooltip's width and at its line |
There was a problem hiding this comment.
Non-blocking. Rule 1 of docs/CODING_RULES.md: "When touching a large function, extract at least one logical block into its own function before you're done." RenderItemInfo is about 3,600 lines long, and this block is a natural WriteItemDescriptionLines(const ITEM&), which #821 extracts later anyway. Doing it here also makes building the rows testable (see the comment on the test).
There was a problem hiding this comment.
Done in f62aa0c: the block is WriteItemDescriptionLines(definition, …, measure) in ZzzInventory.cpp, declared in ZzzInventory.h. It takes the item definition and the measure function, so the new tests check its rows directly. Later steps extend that function rather than the block (#830 merges its own extraction into it).
|
|
||
| // How wide a description line is drawn before it wraps, in the tooltip's text units: about the widest lines of the | ||
| // original tooltips. | ||
| constexpr int ItemDescriptionWrapWidth = 200; |
There was a problem hiding this comment.
Non-blocking. This is a tooltip layout value ("in the tooltip's text units") in the item data definition, and only ZzzInventory.cpp uses it. Rule 5 of docs/CODING_RULES.md keeps UI and data apart, so it fits better next to the tooltip code.
There was a problem hiding this comment.
Moved in f62aa0c: ItemDescriptionWrapWidth is a constant of the tooltip code in ZzzInventory.cpp now, out of ItemDefinition.h.
| | Field | Meaning | | ||
| |---|---| | ||
| | `tags` | Categories the game code asks for, e.g. `["mount", "flying"]`; see [Tags](#tags) | | ||
| | `description` | Lines the tooltip shows for the item, after its stats (the original client's items still have their texts in the tooltip code, written after the description). Each line is `{"text": ..., "color": ...}`: `text` is written like `name` (a plain English text, or the texts by language), an empty text (`{"text": ""}`) is a blank line, a long line wraps at about the width of the original tooltips' widest lines, and `\n` in a text breaks the line there. `color` is one of the tooltip colors: `white` (left out), `blue`, `red`, `yellow`, `green`, `darkRed`, `purple`, `darkBlue`, `darkYellow`, `greenBlue`, `gray`, `redPurple`, `violet`, `orange`. A client-only field; the bmd import keeps it | |
There was a problem hiding this comment.
Non-blocking. "after its stats" is true for the jewels, but the block sits before durability, the requirements, the options and the set and socket lines. On a weapon, the description shows between the speed lines and durability. Saying where it goes avoids surprises, for example "after the damage, defense and speed lines, before durability and the requirements".
There was a problem hiding this comment.
Fixed in 8b26e65: "after its damage, defense and speed lines and before its durability, requirements and options". The row also says the Dark Horse and Dark Raven show it only in shops, describes the checks and the 32-row limit, and that \n\n leaves a blank line.
| CHECK(read.sellable); | ||
| } | ||
|
|
||
| TEST_CASE("An item's description is kept through write and read [data][items]") |
There was a problem hiding this comment.
Non-blocking. This tests the file format well. Nothing tests how a description becomes tooltip rows: blank lines, \n, wrapping and the 50-row limit. A test of the extracted function, or of WrapTextToWidth with "a\n\nb", would have caught the blank-line bug.
There was a problem hiding this comment.
Added tests/data/test_item_descriptions.cpp in f62aa0c. It's in test_item_database_files, which links Winmain.cpp, as the tooltip code needs. It tests:
- blank lines inside a text and at its start;
- wrapping to the width in the line's color;
- the row limit;
- the checks.
test_text_line_wrap gained cases for a blank line inside a text, measuring once per word, and a too-wide word after a break.
| }}; | ||
| } // namespace | ||
|
|
||
| constexpr std::array<EnumName<ItemTextColor>, 14> ItemTextColorNames = {{ |
There was a problem hiding this comment.
Non-blocking. Every other name table is in the anonymous namespace above; this one is outside it and sized with a literal 14. A color added to ItemTextColor without a name gives no compile error. Such a color is then saved as a number, and reading the file refuses a number that has no name.
There was a problem hiding this comment.
Fixed in f62aa0c:
- The table is in the anonymous namespace, sized by
ItemTextColorCount(ItemTextColor::Count, asItemRequirementdoes). - A
static_assertchecks that every entry is set and in value order, so a color added without a name fails to compile.
It measured the whole line after every letter. A tooltip is built every frame while an item is hovered, and through the SDL_ttf renderer each measured prefix is a new text for the shared text cache, which empties itself when full. Now a line is measured when a word ends, and letter by letter only inside a word wider than a whole line. Compared with the old version on 200,000 random texts: the same lines, except where the old one left a line wider than the limit (a word too wide for a line, after a break), and about half the measure calls.
…ike names Review of sven-n#813: - A blank line inside a description text ("a\n\nb", or a leading "\n") wrote an empty row, and an empty row ends the tooltip in both renderers and in the height count: the rest of the tooltip was gone. A blank piece is now the tooltip's own blank row, "\n", as a blank line is. - The description stopped at TextList's end, but the rows after it are written unchecked, so a long description wrote past the array. TextList, TextListColor, TextBold and Size have 32 rows more than the original client's 50, and the description writes at most those 32: it never takes the room the rest of the tooltip needs. The files that declared the arrays themselves with 50 rows use ZzzInventory.h's declarations now. - The block is WriteItemDescriptionLines (rule 1), with the measure passed in, so a test can check its rows; the wrap width moved from the item data header to it, and the row width comes from TextList. - Description texts get the checks names get: the English text is required, a language code has only letters, digits and "-", and a language the game has no texts in is a warning. - The color names are in the anonymous namespace with the other tables, and a color without a name fails to compile.
… texts in the items design Review of sven-n#813: the description comes after the damage, defense and speed lines and before durability, the requirements and the options; the Dark Horse and Dark Raven show it only in shops. The items design records the split this step makes: an item's own tooltip texts are client-only fields of its item file (D15, D22, the field table), which the exchange file of phase 7 leaves out, and phase 8 goes in the steps of sven-n#815.
…s stay English sven-n#843 makes the item database's display locale the item names' language, which is English when "Translate item names" is off. The description read its texts in that locale, so the whole description turned English with the names. It is read in the UI's language now: only the names follow the option.
|
Thanks for the thorough review. I checked each point against the code before changing anything, and they all held. The inline comments have the details; the rest:
Every branch builds, and its tests pass on Windows (MSVC), with no new clang-format issues on the changed lines. |
A line that starts with spaces and whose first word is wider than the line was broken at those spaces, which left an empty line above the word. In a tooltip that is an extra blank row. The spaces now go with the break instead of becoming a line of their own; the text's own blank lines stay.
|
I pushed a small fix to this branch, b9a7430, so please pull before your next push.
Every later step of the stack has the same |
… line never writes an empty row As the review of #813 asked for description texts: - Tooltip.json's texts (shared texts and layout lines' own texts) get the checks item names get: the English text is required (a blank line has none), a language code has only letters, digits and "-", and a translation in a language the game has no texts in is a warning (ValidateTooltipTextLocales, when the item data loads). - A layout line whose text comes out empty in the UI's language writes the tooltip's blank row ("\n"): an empty row ends the tooltip. Its row width comes from TextList instead of a literal 99.
…n texts As the review of #813 asked for description texts: an item's level values with their translations get the checks names get (the English text is required, a language code has only letters, digits and "-", and a language the game has no texts in is a warning). One helper checks a description line's and a level value's text.
… Archangel and the Command Fruit come from the data
The 30 cases of the second item chain that write texts with values move
into the items' descriptions, as their first lines: the quest items, the
event, Doppelganger and Varka tickets (with their map's name written in),
the elite potions, buff scrolls, elixirs and seals, the cherry blossom
items, the cash shop cards, the gamble items, the Wizard's Ring by level,
the wing-mix charms, the Secromicon items, the Chaos Castle and paid
channel tickets and the Heart. The reset fruits, the Talisman of
Resurrection, the socket seeds and spheres and the Sign of Dimensions
stay code: their lines need the game (reset points, the saved revive
place, the socket system, the signs still needed). Texts written more
than once are shared texts of the tooltip file; the .resx texts only
these cases used are gone.
The Weapon of Archangel's lines, a preview of the reward weapon its +
level stands for (+0 the staff, +1 the sword, +2 the crossbow: damage,
speed, requirements and options), are its description now, with the
sword's and crossbow's skill mana from the skill data.
"Can be equipped by Dark Lord" of the Command Fruits (+4) was white for
every hero; like every other line that names an item's classes, it is
white for a Dark Lord and dark red for the others now ("class:darkLord").
Tooltips that change: lines longer than the tooltip's width wrap, as
description lines do since sven-n#813 (the tickets', seals' and Secromicon
papers' long lines, the Weapon of Archangel's longest); the cash shop
cards and cherry blossom branches lose the empty full-height line they
wrote for a text no .resx has (legacy ids 2535-2537 and 2551-2556); the
Master Seal of Ascension shows its own lines again without the wing-mix
charm's, which it got when sven-n#820 moved its case out of the chain ahead of
the charms' (its number is in the charms' range). The Weapon of
Archangel's speed and requirement lines above +2, which the item never
has, showed uninitialized values; they're gone.
The tooltip snapshot records eleven variants per item (plain, full,
ancient, harmony, each option alone, worn, broken and expired) and blank
lines without color or style: they show none, and what their slot kept of
an earlier tooltip made the recorded hashes change with unrelated
tooltips.
Summary
Step 1 of 6 of #815 (data-driven item tooltips, phase 8 of the data-driven items design). This step only adds the format and draws it; the next steps move the original items' texts into the data, add named placeholders filled from the item and its stats, and finally build the whole tooltip from data.
Adds an optional
descriptionlist to the item data (src/bin/Data/Items/Group*.json): lines the item tooltip shows after the item's stats. Today every item text is a case in the tooltip code (ZzzInventory.cpp); with this, a new item brings its text as data.For example, the Jewel of Bless's line ("It is used to increase your item level up to 6", today a case in the tooltip code) would read in its item data:
textis written likename: a plain English text or the texts by language; an empty text ({"text": ""}) is a blank line.coloris one of the tooltip colors by name (blue,yellow,red, …); white when left out.in a text breaks the line there; each piece keeps the line's color.docs/item-data.md.No shipped item uses it yet, and the original items keep their texts in the tooltip code, so nothing changes in game.
Screenshots
The Jewel of Bless's tooltip in the client built from
mainand from this branch, in both UI themes.mainThis branch, no item data changed: the same tooltip.
This branch, with an example description (only in the screenshot's local item data, not in this PR): a blank line, a blue line that wraps to the tooltip's width, and a yellow line with a
\n. The jewel's own line is still written by the tooltip code, after the description: the original items keep their texts in code for now, and moving them into the item data is step 2 of #815.Related issues
Step 1 of #815. Part of phase 8 (data-driven tooltips, D15) of the data-driven items design (#628).
Checklist
docs/CODING_RULES.md.docs/build/README.md). Windows x64 Release; all 977 tests pass, including the newAn item's description is kept through write and read. Changed lines pass clang-format 21.1.8 as CI checks them.