Skip to content
Merged
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
1 change: 1 addition & 0 deletions docs/item-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ variants or option words. An online keyword need not select the same items offli
| 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 damage, defense and speed lines and before its durability, requirements and options (the original client's items still have their texts in the tooltip code, written after the description). The Dark Horse and the Dark Raven show it only in shops: elsewhere the pet window writes their tooltip. Its texts are in the UI's language, also when the item names stay English ("Translate item names" off). Each line is `{"text": ..., "color": ...}`: `text` is written like `name` (a plain English text, or the texts by language) and checked like it (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), 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 (`\n\n` leaves a blank line). The description draws at most 32 rows, which it has to itself, so a long one never takes the room of the tooltip's other lines. `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 |
| `width`, `height` | Size in inventory slots |
| `slot` | Equipment slot: `mainHand`, `offHand`, `helm`, `armor`, `pants`, `gloves`, `boots`, `wings`, `pet`, `pendant`, `ring`. Left out = not equippable |
| `wingTier` | Only for wings: `small`, `first`, `second`, `third`. The wing formulas (defense, defense per level, damage increase, absorption) depend on it; excellent second and third tier wings get no extra durability, and the chaos mix knows a wing's tier from it |
Expand Down
9 changes: 5 additions & 4 deletions docs/superpowers/specs/2026-09-25-data-driven-items-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@ share with items) is planned in the
| D12 | Id ranges | No reserved number ranges for custom items, and no range checks (`type >= X && type <= Y`) in new code; everything an item "is" comes from its data. OpenMU must keep supporting the original Season 6 client **and** this client (see section 7). |
| D13 | Other item files | `ItemAddOption`, `SocketItem`, `Mix`, `pet` and set options are decided later, when we know more. |
| D14 | Item options | The item file only holds **links** to the option groups an item can have (Luck, additional option, excellent, wing, harmony, guardian, socket, …). The option definitions (values, chances, levels) live in their own files. Related data stays together in one file, e.g. an option definition with all its levels and values. |
| D15 | Tooltips | Tooltips become data-driven with JSON data, converted from `ItemTooltip*` / `ItemLevelTooltip` (see "Unused item files in the repo"). Phase 8, after the item phases; moved earlier if the item changes turn out to need it. The tooltip bmd files stay in the repo until then. |
| D15 | Tooltips | Tooltips become data-driven with JSON data, converted from `ItemTooltip*` / `ItemLevelTooltip` (see "Unused item files in the repo"). Phase 8, after the item phases; moved earlier if the item changes turn out to need it. The tooltip bmd files stay in the repo until then. **Update (2026-10-22, #815):** an item's own tooltip texts (its `description` lines) live in its item file, with their translations, as client-only fields that the OpenMU exchange leaves out (D22). Texts and layouts that many items share get a tooltip file of their own. |
| D16 | Unused item bmd copies | Removed on this branch; the game only loads `Item_<lang>.bmd`. |
| D17 | Translations in phase 2 | Item names moved into phase 2, because the per-language bmd files were the only place the Portuguese and Spanish names existed. Names are `LocalizedString`s imported from all three files; the UI locale (the same setting as the `.resx` texts) picks the shown name, also when it changes at runtime. On disk, each item's `name` is an object of names by language (`{"en": "Blade", "es": "Espada", "pt": "Lâmina"}`, English first) in the item files; separate translation files were tried and dropped as extra maintenance. Phase 5 keeps only the translation tooling. |
| D18 | Repairing the legacy data | The bmd import recovers names that ran past the 30-byte name field and takes the fields they overwrote from a language whose name did not reach them (defaults when none has them). Names that are not UTF-8 are read as Windows-1252. The changes are listed in the phase 2 PR and in `docs/item-data.md`. |
| D19 | Editor sync | Every item editor change goes into the database right away (phase 2), so the editor and the database never differ. Moving the editor fully onto the database stays in phase 6 (done: the item tools edit the database, the item table is gone). |
| D22 | Model data files | Model and display data (model file, textures, inventory and ground display, cloth, effects) lives in separate files, `Data/Items/Models/GroupNN_*.json`, one per item group, items by `number`. It is client-only: the item files keep what client and server share, and only those take part in the OpenMU exchange. Separate files also keep the item files small and let the model editor and the stats editor change different files. |
| D22 | Model data files | Model and display data (model file, textures, inventory and ground display, cloth, effects) lives in separate files, `Data/Items/Models/GroupNN_*.json`, one per item group, items by `number`. It is client-only: the item files keep what client and server share, and only those take part in the OpenMU exchange. The exception is an item's own tooltip texts (D15): they stay in the item file next to its name, as client-only fields that the exchange leaves out. Separate files also keep the item files small and let the model editor and the stats editor change different files. |
| D23 | Model slots | Two steps. **Own model slots (phase 4a–4c):** every item keeps its own model slot, `MODEL_ITEM + item type`, as today; the data only says which file is loaded into it, so the ~100 places that compute `+ MODEL_ITEM` / `- MODEL_ITEM` stay unchanged. **Shared models (phase 4d, right after them):** models become entries of their own, and items reference them by name, so several items can share one model (loaded once; 4d1, done). Every item keeps a slot of its own, and the conversions between item types and model slots go through one place, `ToModelSlot` / `ToItemType` (4d2), so a moved item can keep its slot (phase 12) and the named slots of items (`MODEL_KRIS`, …) stay valid. One slot per shared model, with objects keeping item types, is not planned: about 240 places identify items by their model slot (see "Item-specific code that is left"), and none of them gains anything while each item has its own slot. Phase 12 (level variants as items of their own) shares models without loading them twice. |
| D24 | Render effects | The effect code stays code, but which item uses which effect moves into the model data: the glow as values (color names from a glow color list, `Data/Effects/GlowColors.json`, which has the colors of the old `PartObjectColor*` palettes; the meshes it is drawn on; the level it glows like), a render style (`RenderPartObjectBody` recipes; identical recipes share one name) and an item effect (the item branches of `RenderPartObjectEffect`). Effects that depend on the item level (+7 glow, excellent, ancient) stay generic code, and the entries for monsters and NPCs stay in code. The named looks are code for now; D25 moves what they are made of into data. |
| D25 | Looks in data | Items keep naming their looks; what a named look is made of moves from code into data (phase 13). Skills, monsters and NPCs use the same look format, so the decision lives in the [roadmap](2026-09-29-data-driven-content-roadmap-design.md). |
Expand Down Expand Up @@ -292,6 +292,7 @@ Each field belongs to one of two groups:
| Categories/tags | wing, flying, mount, pet, potion, jewel, ticket, cash shop, event, … | client-only at first; shared where OpenMU gets matching data |
| Model | model file, texture folders, cloth flag (in the model files, D22) | client-only |
| Rendering | inventory offset, rotation and scale; ground rotation and scale; glow color, render style, effects (D24) | client-only |
| Tooltip texts | `description` (D15) | client-only, in the item files; the exchange file leaves them out |

### 4. Rule code on top of data

Expand Down Expand Up @@ -413,9 +414,9 @@ in both repos (as separate PRs, one per repo).
| 4e | Clear model loading errors | Client | MuMain | 4 | One message for missing model files and textures of item models that names the item, the model entry, the texture and the searched folders. |
| 5 | Translation tooling | Client | MuMain | 2 | Translations editor (items × languages), missing names counted in the editor. The names themselves moved to phase 2 (D17). Done before phase 6. |
| 6 | Editors | Client | MuMain | 2–5 | Focused MuEditor tools (section 9) on the rules of D26, including add/remove items and picking the looks of an item from lists. Takes over phase 5's item list. Done. |
| 7 | Item sync, client side | Client | MuMain | 2, 6 | MuEditor import/export of the item exchange file, with diff. |
| 7 | Item sync, client side | Client | MuMain | 2, 6 | MuEditor import/export of the item exchange file, with diff. The client-only fields of the item files (tags at first, `description`) are left out. |
| B | Item sync, server side | Server | OpenMU | 7 (file format) | Admin panel import/export pages for the item exchange file, with diff. |
| 8 | Data-driven tooltips | Client | MuMain | 2, 5 | Tooltip JSON converted from the `ItemTooltip*` files; `RenderItemInfo()` reads data; tooltip editor. Moved earlier if needed. |
| 8 | Data-driven tooltips | Client | MuMain | 2, 5 | Tooltip JSON converted from the `ItemTooltip*` files; `RenderItemInfo()` reads data; tooltip editor. Moved earlier if needed. In steps (#815): an item's own texts go into its item file (`description`, D15), shared texts and layouts into a tooltip file. |
| 9 | Item option definitions | Both | MuMain + C | 2 | Option groups in their own JSON files matching OpenMU's `ItemOptionDefinition`; exchange file and editor. |
| 10 | Item sets | Both | MuMain + C | 2 | Item sets in JSON matching OpenMU's `ItemSetGroup`; exchange file and editor; based on the `item-set-editor` branch. Moved earlier if needed. |
| C | Option and set sync, server side | Server | OpenMU | 9, 10 | Admin panel import/export for the option definition and item set exchange files. |
Expand Down
70 changes: 45 additions & 25 deletions src/source/Core/Text/TextLineWrap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -125,48 +125,68 @@ std::vector<std::wstring> WrapTextToWidth(const std::wstring& text, int maxWidth
// has seen. npos while it has none.
size_t breakAt = std::wstring::npos;

const auto fits = [&](size_t length) { return measureWidth(line.c_str(), length) <= maxWidth; };
// Called when a word ends: the line is measured once per word, not after
// every letter (a tooltip is built every frame, and each measured piece
// is a new text for the renderer's text cache).
const auto wrapAfterWord = [&]()
{
// Spaces never make a line too wide: it is measured up to its last
// word, and the spaces it ends with are dropped when it is broken.
if (line.empty() || line.back() == L' ' || fits(line.size()))
{
return;
}
if (breakAt != std::wstring::npos)
{
std::wstring word = line.substr(breakAt);
line.erase(breakAt);
TrimTrailingSpaces(line);
// Only spaces before the word (an indented line): no line of
// their own, or an empty line would appear above the word.
if (!line.empty())
{
lines.push_back(line);
}
line = std::move(word);
breakAt = std::wstring::npos;
}
// A single word which is wider than a whole line. It has to be broken
// somewhere, otherwise it would never fit: where it stops fitting.
while (line.size() > 1 && !fits(line.size()))
{
size_t length = 1;
while (length + 1 < line.size() && fits(length + 1))
{
++length;
}
lines.push_back(line.substr(0, length));
line.erase(0, length);
}
};

for (const wchar_t character : text)
{
if (character == L'\n')
{
wrapAfterWord();
TrimTrailingSpaces(line);
lines.push_back(line);
line.clear();
breakAt = std::wstring::npos;
continue;
}

line += character;
if (character == L' ')
{
wrapAfterWord();
line += character;
breakAt = line.size();
continue;
}

if (measureWidth(line.c_str(), line.size()) <= maxWidth)
{
continue;
}

if (breakAt != std::wstring::npos)
{
std::wstring rest = line.substr(breakAt);
line.erase(breakAt);
TrimTrailingSpaces(line);
lines.push_back(line);
line = std::move(rest);
}
else if (line.size() > 1)
{
// A single word which is wider than a whole line. It has to be
// broken somewhere, otherwise it would never fit.
lines.push_back(line.substr(0, line.size() - 1));
line = line.substr(line.size() - 1);
}

breakAt = std::wstring::npos;
line += character;
}

wrapAfterWord();
TrimTrailingSpaces(line);
if (!line.empty())
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ void CopyItemAttributeStats(const ITEM_ATTRIBUTE& attribute, ItemDefinition& def
void CopyFieldsNotInItemAttribute(const ItemDefinition& source, ItemDefinition& target)
{
target.tags = source.tags;
target.description = source.description;
target.wingTier = source.wingTier;
target.weaponKind = source.weaponKind;
target.statProfile = source.statProfile;
Expand Down
50 changes: 47 additions & 3 deletions src/source/Data/GameData/ItemData/ItemDataValidation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,33 @@ void ValidateName(const ItemDefinition& definition, std::vector<ItemDataIssue>&
check(locale, text);
}
}
// A description line's text is written like a name: an English text, and translations under language codes. A blank
// line has no text in any language.
void ValidateDescription(const ItemDefinition& definition, std::vector<ItemDataIssue>& issues)
{
for (size_t index = 0; index < definition.description.size(); ++index)
{
const LocalizedString& text = definition.description[index].text;
if (text.IsEmpty())
{
continue;
}
const std::string field = "description[" + std::to_string(index) + "].text";
if (text.GetNeutral().empty())
{
AddIssue(issues, ItemDataIssueSeverity::Error, definition, "the English text is missing", field.c_str());
}
for (const auto& [locale, translation] : text.GetTranslations())
{
if (!IsValidLocaleCode(locale))
{
AddIssue(issues, ItemDataIssueSeverity::Error, definition,
"\"" + locale + "\" is not a language code (letters, digits and \"-\" only)", field.c_str());
}
}
}
}

// The slot of each weapon kind: the left hand (offHand) for bows, shields and
// books, the right hand (mainHand) for the others. The code looks for a bow or
// a shield only in the left hand and for a crossbow or a spear only in the
Expand Down Expand Up @@ -224,6 +251,7 @@ void ValidateItems(std::span<const ItemDefinition> items, std::vector<ItemDataIs
}

ValidateName(definition, issues);
ValidateDescription(definition, issues);
ValidateSlot(definition, issues);
ValidatePotionPriority(definition, issues);
ValidateExcellentBonus(definition, issues);
Expand Down Expand Up @@ -297,16 +325,32 @@ void ValidateNameLocales(std::span<const ItemDefinition> items, std::span<const
{
continue;
}
const auto isShown = [&](const std::string& locale)
{
return std::any_of(uiLocales.begin(), uiLocales.end(),
[&](const char* uiLocale) { return locale == uiLocale; });
};
for (const auto& [locale, text] : definition.names.GetTranslations())
{
const bool shown = std::any_of(uiLocales.begin(), uiLocales.end(),
[&](const char* uiLocale) { return locale == uiLocale; });
if (!shown)
if (!isShown(locale))
{
AddIssue(issues, ItemDataIssueSeverity::Warning, definition,
"the " + locale + " name is never shown: the game has no texts in that language");
}
}
for (size_t index = 0; index < definition.description.size(); ++index)
{
for (const auto& [locale, text] : definition.description[index].text.GetTranslations())
{
if (!isShown(locale))
{
const std::string field = "description[" + std::to_string(index) + "].text";
AddIssue(issues, ItemDataIssueSeverity::Warning, definition,
"the " + locale + " text is never shown: the game has no texts in that language",
field.c_str());
}
}
}
}
}

Expand Down
34 changes: 34 additions & 0 deletions src/source/Data/GameData/ItemData/ItemDefinition.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,36 @@ enum class WeaponKind : BYTE
Book,
};

// The color of a tooltip line; the values are the tooltip's TEXT_COLOR_*.
enum class ItemTextColor : BYTE
{
White = TEXT_COLOR_WHITE,
Blue = TEXT_COLOR_BLUE,
Red = TEXT_COLOR_RED,
Yellow = TEXT_COLOR_YELLOW,
Green = TEXT_COLOR_GREEN,
DarkRed = TEXT_COLOR_DARKRED,
Purple = TEXT_COLOR_PURPLE,
DarkBlue = TEXT_COLOR_DARKBLUE,
DarkYellow = TEXT_COLOR_DARKYELLOW,
GreenBlue = TEXT_COLOR_GREEN_BLUE,
Gray = TEXT_COLOR_GRAY,
RedPurple = TEXT_COLOR_REDPURPLE,
Violet = TEXT_COLOR_VIOLET,
Orange = TEXT_COLOR_ORANGE,
Count
};
constexpr size_t ItemTextColorCount = static_cast<size_t>(ItemTextColor::Count);

// One line of an item's description in its tooltip: a text with its translations (empty: a blank line) and its color.
struct ItemDescriptionLine
{
LocalizedString text;
ItemTextColor color = ItemTextColor::White;

bool operator==(const ItemDescriptionLine& other) const = default;
};

// What a player may do with an item. See the rule flags in ItemDefinition.
enum class ItemAction : BYTE
{
Expand Down Expand Up @@ -171,6 +201,10 @@ struct ItemDefinition

ItemTagSet tags;

// What the tooltip says about the item, line by line, after its own lines (empty for most items; the jewels'
// texts of the original client are still in the tooltip code).
std::vector<ItemDescriptionLine> description;

BYTE width = 0;
BYTE height = 0;
ItemSlot slot = ItemSlot::None;
Expand Down
Loading
Loading