Skip to content
Merged
47 changes: 47 additions & 0 deletions docs/item-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ item files hold what client and server share.
| `ground` | How the item lies on the ground, see below. Optional. |
| `glow` | How the item glows, see below. Optional. |
| `renderStyle` | The look of the model when it is more than a plain textured model, see below. Optional. |
| `itemEffect` | What the model does before it is drawn (sprites and particles on its bones, a pulsing glow mesh, ...), see below. Optional. |
| `cloth` | `true` for capes that are worn as cloth: when one is put on or taken off, the character's cloth is deleted, so the next cape builds its own. Optional. The flag does not make a cape cloth; which capes are drawn as cloth, and how, is still decided in code. |

`inventory` and `ground` hold these values; a missing value has the
Expand Down Expand Up @@ -339,6 +340,41 @@ only in the hands of the Metal Balrog and the Orc Archer of Doom,
`helperNpcPlate` only on the helper NPCs (Luke and Leo the Helper, Helper
Ellen; in the code the flag of a PC room look, which no player gets).

A few styles also shine below +3: the model is drawn with the light of the
item scaled, then with two shine passes. `sealOfAscension`, `sealOfWealth`
and `sealOfSustenance` (the seals, light at 0.9), `illusionSorcererCovenant`
and `harmonyShine` (the Jewel of Harmony and the Moonstone Pendant, which
are drawn plainly otherwise), and `cursedCastleWater`.

`itemEffect` names what the model does before it is drawn:

```json
{ "number": 37, "file": "Data/Item/wing09.bmd", "textureFolders": ["Item"], "itemEffect": "wingOfEternal" }
```

Each item effect is code with a name; the names are listed at the end of
`src/source/Render/Items/ItemEffects.cpp`. An item effect runs every frame
before the model is drawn and can:

- place sprites, particles and lightning on bones of the model, so they
follow its animation (the Devil's Key and Invitation, Rena, the wings of
the third tier, the Wings of Darkness);
- change values of the drawing: a pulsing glow mesh (`wingsOfDragon`,
`wingsOfSoul`, `redSpirit`, `staffOfKundun`, `divineSet`), a mesh hidden
by level (`hiddenMeshByLevel` of the Siege Potion and the Contract,
`hideMesh1`), the level potions glow like (`potion`: +7 at every level
above 0);
- draw the model itself instead of the usual drawing (the Dark Lord's
scrolls, `fruits`, `spirit`, `invisibilityCloak`, `firecracker`,
`gmGift`, `meshesPerLevel`).

Items with the same item effect share it. The socket seeds and spheres and
zen have no item effect; they glow like level 0 (`"glow": {"level": 0}`), whatever
their level.

The effects of the event models that level variants are drawn with stay in
code; they get model entries later.

- All item models are loaded at startup, on the loading screen.
- An item without a model entry is not drawn. Some items are drawn with
the model of another item or with an effect model; that choice, and
Expand Down Expand Up @@ -383,6 +419,7 @@ The problems are:
| `noneBlendMeshes` has a mesh number the model does not have. | warning |
| A `glow` value names a mesh the model does not have; that glow is not drawn (a hidden mesh: the glow is on all meshes). | warning |
| `renderStyle` names a style that does not exist; the model is drawn plainly. | warning |
| `itemEffect` names an item effect that does not exist; the model is drawn without it. | warning |

The model names textures as `.jpg`/`.tga`; the game reads the encrypted
copies with the same name, `.OZJ`/`.OZT`. Meshes whose texture name starts
Expand Down Expand Up @@ -417,6 +454,16 @@ The game runs from the build folder, which has a copy of `src/bin/Data`.
To keep your changes, copy the changed files from
`<build folder>/Data/Items` to `src/bin/Data/Items` and commit them.

### Looks

The **Looks** section above the item table shows the model data of the
selected item (`Data/Items/Models`), read only: its model file, its glow
values, its render style and its item effect. A render style or an item
effect opens to the list of all items that use it; clicking one selects it in
the table (the search is cleared when it hides that item). Items without
item data are listed too; they are not in the table, but selecting one
shows its looks. Changing the looks is done in the model files for now.

### Import from bmd / Export as bmd

The repository does not ship `Item_<lang>.bmd` files; the JSON files are the
Expand Down
90 changes: 77 additions & 13 deletions docs/superpowers/specs/2026-09-25-data-driven-items-design.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Data-Driven Content Roadmap (WIP)

> **Working document.** It keeps the order of the data-driven refactorings
> that go beyond one area, the decisions they share, and the editor parts
> they share. Each area has its own design document with its own phases;
> this document links them and stays while areas are open.
>
> Started from the items design
> ([2026-09-25-data-driven-items-design.md](2026-09-25-data-driven-items-design.md)),
> based on MuMain `upstream/main` @ `e4831483`.

## Goal

Content that is hardcoded in the client today (items and their looks,
effects and particles, skills, monsters and NPCs, ...) becomes data that
can be changed and edited in MuEditor without code. What belongs together
is planned together: items, skills and monsters all use the effect code,
so they share one way to describe looks and one set of editor parts.
Nothing gets slower in game: names and values are resolved when loading.

## Why these areas belong together

Items are not the only users of the effect code. About 3,950 calls create
effects, particles, lightning and sprites: about 1,600 for the maps and
their monsters (`World/GameMaps`), 1,100 for characters, objects and items
(`Engine`), 760 inside the effects themselves, 290 for skills, combat,
pets and events (`GameLogic`) and 140 for skill results from the server
(`Network`). The effect code knows about 440 effect types, 100 particle
types and 30 lightning types; `Render/Effects/EffectRegistry` has started
to describe them as a table instead of three large switches. Skills are
still read from `Skill.bmd` (with a table editor, as items had before
their phase 2), and monsters and NPCs are set up in a switch of 403 cases
in `ZzzCharacter.cpp`.

## Shared decisions

These decisions were made in the items design; they keep their numbers,
so the references there stay valid.

| # | Topic | Decision |
|---|---|---|
| D25 | Looks in data | Items, skills, monsters and NPCs reference their looks by name (for items: the render style and the item effect; the glow colors already are names in data). What a named look is made of moves from code into data files in `Data/Effects/`: a list of building blocks, namely draw passes (mesh or body, flags, texture, color, alpha, texture scrolling), things placed on bones (sprites, particles, lightning between two bones, effects), animated object values (glow mesh brightness, hidden mesh, texture scrolling), timing (pulses, random chances per frame) and conditions (item level, doppelganger, ...). The drawing code runs these lists; data that names looks stays valid when their definitions move. One look format and one look editor for all areas: a look made for an item can be used by a monster. Effect and particle types are referenced by the names of the effect catalogue (FX1) and become data themselves later (FX2). |
| D26 | Shared definitions | Anything that several things use is defined once, with a name, in a data file, and its users reference that name: glow colors, looks, effect types, skills. Editors show where a definition is used (items, skills, monsters, NPCs, other effects). Saving a change to a definition that others use first shows the list of those users; a copy makes a variant for one user; renaming updates all references; a definition that is still used cannot be deleted (the list shows why). Unknown names are reported when loading. Names are resolved when loading, so drawing and game logic never look names up. Every move from code into data is compared with the old code (recorder) before the old code goes. |

## Bones

Effects place sprites, particles and lightning on bones of a model, so
they follow its animation: the lights of a wing move as it flaps, the
casting effect of a skill sits in the caster's hand. Looks store bones as
numbers, as the code does, and the model loader checks that they exist.

- **Item models:** the names in the `.bmd` files are export names that do
not help to find a bone. Of the 778 item model files, 340 have the
biped skeleton with names like "Bip01 L Hand", 249 only names like
`Bone01`, `Box02` or `zx12`, in 396 the bone called "BoneNN" is not bone
NN, and one file has a name twice. Some models have helper bones placed
as effect points (the 22 blue lights of the Wing of Eternal sit on the
bones `zx01` to `zx22`).
- **Characters:** skills place effects mostly on the caster's bones
(hands, weapon), and effects follow their owner's bones. Characters have
the biped skeleton, whose names are readable ("Bip01 R Hand").

The look editor shows numbers and names together and picks bones by
clicking them.

## Areas and order

| Area | Name | Design document | Depends on | What moves into data | Editor |
|---|---|---|---|---|---|
| Items | Items | [items design](2026-09-25-data-driven-items-design.md), phases 0–14 | – | Item data, rules and categories, models, glow, render styles and effects (as names). | The item tools of section 9 there. |
| FX1 | Effect catalogue | not yet | items 4c | Every effect, particle, lightning and sprite type gets a name and its creation values (the `CreateParams` of the registry) in `Data/Effects/`: the effect types (`effectTypes` in the data). Behavior stays code. Items, skills and monsters then name effect types instead of using type numbers; an item effect (`itemEffect`, what an item does every frame before it is drawn) creates instances of effect types. | Effect browser: each effect with a preview, its values, and where it is used. |
| Items 13 | Looks in data (items) | items design, phase 13 | FX1, items 6 | What the item looks are made of (D25). | Look editor. |
| SK1 | Skills as data | not yet | items 2 (the same format rules) | `Skill.bmd` into JSON, like the items in their phase 2: names, requirements, rules; later synced with OpenMU like the items. | Focused skill editors, like the item tools; "used by" (classes, items that give a skill, monsters). |
| SK2 | Skill looks | not yet | SK1, FX1, items 13 | How a skill looks when cast, flying and hitting (effects, particles, sounds, character animations), as looks (D25), mostly on the caster's bones. | Look editor, with a preview of the skill cast by a test character. |
| MN | Monsters and NPCs | not yet | FX1, items 13, SK1 | The monster and NPC setup (model, size, the equipment of player-shaped NPCs), their looks (glow, render styles and effects as in D25; the plate of the helper NPCs, `helperNpcPlate`, moves from the items to them; the player transformations) and their skills. | Monster and NPC editors with preview; the look editor. |
| FX2 | Effect behavior as data | not yet | FX1 | How effects and particles move, fade, spawn and draw, as building blocks, one effect family at a time (continuing the handlers of the registry). New effects without code. | Effect editor with a live preview. |
| later | Map objects, buffs, pets, sounds | not yet | FX1 | To decide: the objects of the maps with their effects (most of `World/GameMaps`), buff visuals, pets, sound names. | – |

- **The effect catalogue comes first.** Its names are what the looks of
items (items 13), skills (SK2) and monsters (MN) reference. It moves no
behavior, so it is small and can be checked like the render styles.
- **Skills before monsters,** because monsters use skills.
- **Effect behavior comes last.** Once everything references effects by
name, their behavior can move into data without touching their users.
- **Map objects later,** although the maps are the largest users of
effects: they only need the effect catalogue, not the looks of items,
skills or monsters.

## Shared editor parts

The editors of all areas follow D26: focused tools that share one
selection, "used by" lists, a warning with the list of users before a
shared definition changes, copies for variants. These parts are shared
and opened from the item, skill, monster and NPC editors:

- **Look editor** (from items 13 on): edits a named look (D25) as its
list of building blocks, with a live preview of the model and its
animations (for skills: cast by a test character). Shows the skeleton
with every bone's number and name, highlights the bones the look uses,
and picks bones by clicking them. Lists all users of the look; saving a
change to a look that others use shows a warning with that list. A new
look starts as a copy of another.
- **Effect browser** (FX1) and **effect editor** (FX2): an effect alone in
a preview, its values, and where it is used.

## When an area starts

Its design document is written from the code first, like the items design
(current state, decisions, phases), linked in the table above, and this
document is updated. The default verification is the recorder method of
the items design: the old and the new code are run for every model, setup
and level, and what they draw and spawn is compared.
24 changes: 24 additions & 0 deletions src/Localization/Editor.en.resx
Original file line number Diff line number Diff line change
Expand Up @@ -463,4 +463,28 @@
<data name="Item data could not be written" xml:space="preserve">
<value>The item data files could not be written. See the console.</value>
</data>
<data name="Looks" xml:space="preserve">
<value>Looks</value>
</data>
<data name="Model file" xml:space="preserve">
<value>Model file</value>
</data>
<data name="Glow" xml:space="preserve">
<value>Glow</value>
</data>
<data name="Render style" xml:space="preserve">
<value>Render style</value>
</data>
<data name="Item effect" xml:space="preserve">
<value>Item effect</value>
</data>
<data name="Used by" xml:space="preserve">
<value>used by</value>
</data>
<data name="Select an item" xml:space="preserve">
<value>Select an item in the table to see its looks.</value>
</data>
<data name="No model" xml:space="preserve">
<value>The selected item has no model.</value>
</data>
</root>
150 changes: 150 additions & 0 deletions src/MuEditor/UI/ItemEditor/ItemEditorLooks.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
#include "stdafx.h"

#ifdef _EDITOR

#include "ItemEditorLooks.h"

#include "Data/GameData/ItemData/ItemDatabase.h"
#include "Data/GameData/ItemData/ItemModelDatabase.h"
#include "Data/GameData/ItemData/ItemModelGlowJson.h"
#include "I18N/All.h"
#include "imgui.h"

#include <algorithm>
#include <span>
#include <string>
#include <utility>
#include <vector>

namespace
{
using Data::Items::ItemModelDefinition;
using LookName = std::string ItemModelDefinition::*;

// At most this many items are listed at once; the list scrolls.
constexpr int MaxListedRows = 8;

struct Look
{
std::string name;
// The items that use the look: item type and log name.
std::vector<std::pair<int, std::string>> users;
};

// What the section shows for the selected item, built when the selection, the
// item model data or the items (their names in the lists) change, not every
// frame.
struct LooksCache
{
int itemType = -1;
int modelDatabaseVersion = -1;
int itemDatabaseVersion = -1;
std::string glow;
Look renderStyle;
Look itemEffect;
};

LooksCache g_cache;

// The glow values as the model file has them, or "-" without any.
std::string DescribeGlow(const ItemModelDefinition& model)
{
Data::Items::Json::OrderedJson json = Data::Items::Json::OrderedJson::object();
Data::Items::GlowJson::Write(model, json);
const auto glow = json.find(Data::Items::GlowJson::GlowKey);
return glow != json.end() ? glow->dump() : "-";
}

Look TakeLook(const ItemModelDefinition& model, LookName look)
{
Look result{model.*look, {}};
if (result.name.empty())
{
return result;
}
const std::span<const ItemModelDefinition> models = g_ItemModelDatabase.GetAllSlots();
for (int itemType = 0; itemType < static_cast<int>(models.size()); ++itemType)
{
if (models[itemType].Exists() && models[itemType].*look == result.name)
{
result.users.emplace_back(itemType, g_ItemDatabase.GetLogName(itemType));
}
}
return result;
}

void Refresh(int itemType, const ItemModelDefinition& model)
{
const int modelDatabaseVersion = g_ItemModelDatabase.GetVersion();
const int itemDatabaseVersion = g_ItemDatabase.GetVersion();
if (g_cache.itemType == itemType && g_cache.modelDatabaseVersion == modelDatabaseVersion &&
g_cache.itemDatabaseVersion == itemDatabaseVersion)
{
return;
}
g_cache.itemType = itemType;
g_cache.modelDatabaseVersion = modelDatabaseVersion;
g_cache.itemDatabaseVersion = itemDatabaseVersion;
g_cache.glow = DescribeGlow(model);
g_cache.renderStyle = TakeLook(model, &ItemModelDefinition::renderStyle);
g_cache.itemEffect = TakeLook(model, &ItemModelDefinition::itemEffect);
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking. While the section is open, every frame builds and dumps the glow JSON, goes through all MAX_ITEM slots for each look to count its users (again for an open list), and builds a log name string per listed item. Small in absolute terms, but CODING_RULES rule 12 counts per-frame render functions as hot paths; the lists and the glow text could be cached per selected item and g_ItemModelDatabase.GetVersion().

// A look with the number of items that use it; opened, the list of those
// items in a box of fixed height. Returns the item clicked, -1 for none.
int RenderLook(const char* label, const Look& look)
{
if (look.name.empty())
{
ImGui::Text("%s: -", label);
return -1;
}

int clicked = -1;
ImGui::PushID(label);
if (ImGui::TreeNode("look", "%s: %s (%s %d)", label, look.name.c_str(), I18N::Editor::UsedBy,
static_cast<int>(look.users.size())))
{
const int rows = std::min(static_cast<int>(look.users.size()), MaxListedRows);
const ImVec2 size(-FLT_MIN,
ImGui::GetTextLineHeightWithSpacing() * rows + ImGui::GetStyle().FramePadding.y * 2.0f);
if (ImGui::BeginListBox("##users", size))
{
for (const auto& [itemType, name] : look.users)
{
if (ImGui::Selectable(name.c_str(), itemType == g_cache.itemType))
{
clicked = itemType;
}
}
ImGui::EndListBox();
}
ImGui::TreePop();
}
ImGui::PopID();
return clicked;
}
} // namespace

int CItemEditorLooks::Render(int itemType)
{
if (!ImGui::CollapsingHeader(I18N::Editor::Looks))
{
return -1;
}

const ItemModelDefinition* model = g_ItemModelDatabase.Find(itemType);
if (model == nullptr)
{
ImGui::TextDisabled("%s", itemType < 0 ? I18N::Editor::SelectAnItem : I18N::Editor::NoModel);
return -1;
}
Refresh(itemType, *model);
ImGui::Text("%s: %s", I18N::Editor::ModelFile, model->file.c_str());
ImGui::Text("%s: %s", I18N::Editor::Glow, g_cache.glow.c_str());
const int clickedStyleUser = RenderLook(I18N::Editor::RenderStyle, g_cache.renderStyle);
const int clickedEffectUser = RenderLook(I18N::Editor::ItemEffect, g_cache.itemEffect);
return clickedStyleUser >= 0 ? clickedStyleUser : clickedEffectUser;
}

#endif // _EDITOR
17 changes: 17 additions & 0 deletions src/MuEditor/UI/ItemEditor/ItemEditorLooks.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#pragma once

#ifdef _EDITOR

// A read-only view of the looks of the selected item (Data/Items/Models): its
// model file, glow, render style and item effect, and the items that share a
// look.
class CItemEditorLooks
{
public:
// Shown above the item table; `itemType` is the selected item, -1 for none.
// Returns the item clicked in a list of the items that share a look, -1
// for none.
static int Render(int itemType);
};

#endif // _EDITOR
Loading
Loading