Skip to content

feat(items): add item stat profiles as configuration data - #3

Closed
Mosch0512 wants to merge 162 commits into
feat/item-formulas-datafrom
feat/item-formulas-model
Closed

Mosch0512 wants to merge 162 commits into
feat/item-formulas-datafrom
feat/item-formulas-model

Conversation

@Mosch0512

Copy link
Copy Markdown
Owner

Stacked on #2 (the formula comparison test); this PR's base is that branch, so the diff shows only this change.

Why

The item formulas (how requirements grow with the item level, what excellent and ancient items get more, the price's excellent bonus) are fixed numbers in the game logic. To exchange them with the item stat file of the MuMain client, they have to be configuration data. This PR adds that data, with the values the server computes today. The game logic doesn't read it yet, so nothing changes for players; that follows in a separate pull request, which the comparison test of #2 checks.

What

Data model

  • GameConfiguration: ExcellentDropLevelBonus (25) and AncientDropLevelBonus (30), and ItemStatProfiles.
  • ItemStatProfile: a named set of item formulas, in the parts of the client's item stat file. The profile default writes every part, the others only what differs from it. It has the level growth of the level requirement, what an excellent item needs more, the strength per option level, and:
    • ItemStatTerms, one for each stat the profile writes (damage, magic power, block rate, defense, magic defense): the level bonus table, the excellent part (base × times ÷ drop level + add + drop level ÷ divisor, with the item's own drop level) and the ancient part (value × times ÷ d + add + d ÷ divisor, d = drop level + ancient bonus; AncientAfterLevelBonus adds the level bonus to the value), each "not written", "none" or with values; for magic power also PerLevelAfterHalving;
    • ItemRequirementFormula, one per stat requirement: add + base × (drop level + item level × level weight) × percent ÷ 100, where the drop level includes the ancient or else the excellent bonus; or "keep the base value". LevelRequirementBasis takes the item's level requirement instead of the drop level; no built-in profile uses it yet.
  • ItemDefinition: StatProfile, ExcellentBonus (a fixed excellent bonus instead of the profile's term; 0 = the term) and ExcellentRaisesLevel (true).
  • The new enums ItemStat, ItemStatTermState and ItemRequirementStat (stored in the database, so new values may only be appended).
  • GameConfigurationHelper.Enumerables, captions in en/de/zh-CN, and localized summaries for the admin panel.

Built-in values (src/GameLogic/ItemFormulas/BuiltInItemStatProfiles.cs): the profiles with today's server numbers, also where the client differs (e.g. a leadership formula and 4 strength per option level). Only armor, book and shield compute differently than default. Of the others, the wing profiles name the level bonus tables of their items, sceptre has no magic power growth after halving (0 instead of 2, as in the sceptre rise tables), orb and scroll keep the base stat requirements (their items have no slot, so the server doesn't raise them today), and helper and weapon write nothing. They live in the game logic because the next step falls back to them when a configuration has no profiles.

Initialization and update

  • Items/ItemStatProfiles.cs creates the profiles which a configuration doesn't have yet and gives each item without a profile the one of its kind, following the client's profile rules with the server's item data (ammunition, sceptres, books, shields, weapons, armour slots, the wing tiers, orbs, scrolls, helpers; the Horn of Fenrir and requirements given by item level as in the client). It runs once for every version, after the version's initializer.
  • The mandatory configuration update "Add item stat profiles" (base class plus 0.75, 0.95d and Season 6) does the same for existing databases. It only adds data: profiles which exist (by name) and items which have a profile stay as they are.

Persistence

  • Migration AddItemStatProfiles: the three tables, and the new columns. Existing databases get the bonuses 25 and 30 and ExcellentRaisesLevel true, set by hand as in AddItemRuleFlags. A profile which items use can't be deleted (Restrict, as the castle siege references).
  • Regenerated: BasicModel and EF model, the compiled models of EntityDataContext and PlugInConfigurationTypedContext, and GameConfigurationLoader.Generated.cs.

Testing

  • dotnet build src/MUnique.OpenMU.sln -p:ci=true, then the whole solution with dotnet test --no-build -p:ci=true. All tests of Persistence.Initialization.Tests pass, including the formula comparison of test(items): pin today's item requirement, power-up and price formulas for every item #2. The only failures are the same as on the base without this change: 16 web tests and 3 Discord tests, which fail on Windows. CompiledModelTests and GameConfigurationLoaderGeneratorTest pass.
  • ItemStatProfilesDataTest, for 0.75, 0.95d and Season 6 unless noted:
    • a new database has the profiles and the bonuses, and every item has a profile;
    • the profiles which compute differently follow today's checks exactly: armor = the armour slots, book = a book rise, shield = group 6;
    • each item's damage, block rate and defense level table is its profile's table;
    • the number of Season 6 items per profile, so that no item moves silently between profiles which compute alike;
    • made-up items get the profile of their kind (Season 6);
    • the update gives a database without profiles the same data as a new one, and a second run changes nothing; on Season 6 it keeps a changed profile and a changed item profile, and gives a removed profile back to its items.
  • Broken on purpose: a wrong wing table and a wrong shield rule. Both were caught.

claude and others added 30 commits October 2, 2026 17:33
…mini games

Adds an IsActive flag to GameMapDefinition, CharacterClass,
MonsterDefinition, ItemDefinition and MiniGameDefinition. Inactive
entries stay in the configuration, but are not available in the game:

* maps can't be entered by warps or gates and aren't created; characters
  which log in on an inactive map are moved to an active one
* character classes can't be created anymore
* monsters and NPCs aren't spawned
* items aren't dropped and not offered by merchants
* mini games can't be entered and aren't opened by their periodic start

The flags are true by default, and the migration adds them with the
default true, so existing configurations stay unchanged.

The new "Content activation" page of the admin panel allows to
(de)activate these entries in batches, e.g. to restrict a server to
the content of a certain season.

Refs MUnique#707

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WRxyrvkiYwxHqaKgRTvkM6
* Quests which would evolve a character into an inactive next generation
  class are not offered and can't be completed.
* Invasions leave out mobs of inactive monsters and inactive maps, and
  don't start (and announce) when nothing of them is active.

Refs MUnique#707

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WRxyrvkiYwxHqaKgRTvkM6
Adds the persisted GameVersion enum (0.75 up to Season 6 Episode 3, with
spaced values so that versions can be inserted later) and IntroducedIn to
maps, character classes, monsters, items and mini games.

IntroducedGameVersions holds the versions of the content. They were
compiled from the 0.75 and 0.95d data, the patch history at
https://github.com/Khdoop/mu-online-history and, for monsters, the maps on
which they spawn. Entries which couldn't be dated reliably stay Unknown.
The data initializations set them, and the SetIntroducedGameVersions
update adds them to existing databases without overwriting set values.

The content activation page shows the version and can restrict the
content to a game version in one step: everything up to it is
activated, everything later deactivated, unknown entries are kept.

The migration of the active flags is replaced by AddContentActivation,
which adds both columns.

Refs MUnique#707

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WRxyrvkiYwxHqaKgRTvkM6
The versions before Season 1 are now as fine grained as the version
numbers of the original game, e.g. 0.34, 0.89c, 0.95k or 0.99G+. Their
values are calculated from the version number (major * 10000 +
minor * 100 + patch, where the patch is the number of the version letter,
e.g. 0.99G+ = 0.99.33 = 9933), the seasons from the season and episode
(season * 100000 + episode * 1000), so further versions fit in later.

The content is tagged with these versions according to the MU Online
history. Content of patches without a known version number is
attributed to the next version with a known number, e.g. the Dark Lord
and Kalima to 0.99G+ and the patches between 1.00s and Season 1 to
Season 1. The versions 0.75, 0.95d and 0.97d are kept, as they are the
versions of the data initializations.

Refs MUnique#707

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WRxyrvkiYwxHqaKgRTvkM6
Describes how the hard-coded special cases of the ItemPriceCalculator can
be replaced by price definitions which are referenced by the item
definitions, while equipment keeps an automatic price by convention.
Includes the data model, the migration of existing databases, a phased
implementation plan with a golden master test, and a mapping of every
special item to a definition.

Refs MUnique#75

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HKaEUPxADDTXCqEmoA2xCr
Adds a golden master test which calculates the buying, selling, repair
and crafting reference prices for every item of the initialized 0.75,
0.95d and season 6 configurations, at every item level and with the
option combinations the item can have, and compares them with a frozen
copy of the current calculator. It's the safety net for moving the price
rules into the configuration.

Refs MUnique#75

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HKaEUPxADDTXCqEmoA2xCr
An ItemPriceDefinition describes how the price of an item is calculated,
if it differs from the automatic equipment price: a base price formula,
fixed prices per item level, the quantity scaling, the applicable option
modifiers, the selling price rounding and an optional crafting reference
price. Item definitions reference it by ItemDefinition.PriceDefinition,
and the game configuration holds them.

Includes the generated persistence models and the migration.

Refs MUnique#75

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HKaEUPxADDTXCqEmoA2xCr
Creates the price definitions of the special items and assigns them in
the data initialization of all versions, following the same decisions
as the item price calculator did with its hard-coded item numbers.
A mandatory update adds them to existing databases; it only assigns
definitions to items which don't have one yet.

Refs MUnique#75

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HKaEUPxADDTXCqEmoA2xCr
The item price calculator no longer knows any item numbers, groups or
skill numbers. Special items are priced by their ItemPriceDefinition,
with formulas evaluated by mXparser, and everything else gets the
automatic equipment price as before.

The golden master test proves that the prices of all items of all
versions are unchanged, now also before rounding. The existing price
tests take the price definitions from the season 6 data. The concept
document is updated to the implementation, and the admin panel
documentation describes the item prices.

Closes MUnique#75

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HKaEUPxADDTXCqEmoA2xCr
Gives the pseudo code blocks a language and wraps the long lines of the
code blocks.

Refs MUnique#75

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HKaEUPxADDTXCqEmoA2xCr
…-75-concept-nvwc91

# Conflicts:
#	src/Persistence/Initialization/Properties/PlugInResources.Designer.cs
#	src/Persistence/Initialization/Properties/PlugInResources.resx
Regenerates the AddContentActivation migration after the migrations of
master, so it isn't merged in between them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WRxyrvkiYwxHqaKgRTvkM6
The history lists the Magic Gladiator (and its Storm Crow set) as
introduced in 0.47, but the 0.75 client is not capable of showing it.
The data of the older versions wins here, so both are listed as
introduced in 0.95d, the first data initialization which contains the
class. The version 0.47 has no content anymore and is removed.

Refs MUnique#707

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WRxyrvkiYwxHqaKgRTvkM6
Adds the German texts of the content activation page, the map message,
and the update which sets the game versions. Also adds the captions and
descriptions of the new IsActive and IntroducedIn properties and of the
GameVersion values to the model resources, in English and German.

Refs MUnique#707

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WRxyrvkiYwxHqaKgRTvkM6
Regenerates the AddContentActivation migration after the migrations of
master, the compiled models of the EntityDataContext and the mini game
ranking context, and the generated game configuration loader, so that
they contain the new IsActive and IntroducedIn columns.

Tags the new cash shop items of master with their game version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WRxyrvkiYwxHqaKgRTvkM6
The design time build created an output folder named 'bin\Release' on
Linux, which isn't ignored, and its files got into the previous merge
commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WRxyrvkiYwxHqaKgRTvkM6
Fire Scream required 70 energy and 150 leadership instead of 150 energy
and 70 leadership, as the skill table and the game client have it. A dark
lord with 70 to 149 leadership could learn the skill, but the server
dropped every cast without a message.

Existing season 6 databases are corrected by the new
FixFireScreamRequirementsUpdatePlugIn, which only touches the swapped
initial values, so a deliberately changed configuration is kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ao6aAh4m9VQWdgXJTNLeB3
Add boundary and authorization coverage for alliances, selection,
registration, Crown capture, economy, Life Stones, and rewards.

Fix transitive hostility lookup between allied member guilds.
Document regression coverage, test commands, and manual verification.
Use project-path variables and multiline filters to keep testing
commands within 80 columns while preserving PowerShell syntax
and the existing test selections.
Publish rivalry changes through existing game-server notifications
when alliance membership changes, preserving direct hostilities.

Serialize relationship mutations and add twelve integration cases
covering cache consistency across two game servers.

Avoid alliance scans and allocations for standalone guild queries.
Clear retired runtime-ID rivalry pairs when guilds unload and
restore current relationships when guilds or alliances reload.

Serialize runtime lifecycle changes with relationship updates
while keeping cached alliance queries lock-free.

Add ten integration cases covering offline alliance changes,
repeated reconnects, runtime-ID remapping, and cold alliance loads.
Implement phase 1 of MUnique#1257 with UI independent read operations, an opt-in stateless HTTP endpoint, API-key authorization, request limits, agent setup documentation, and parity/schema integration tests.
Resolve guild filtering through the registered guild service and isolate rate limits by persistent API-key identity. Add regression coverage and address Codacy style, API visibility, and resource ownership findings.
A new database marks every update of its version as installed, since the initialization contains their changes. An
update whose changes it doesn't contain, e.g. a plugin's which adds its own items, sets RunsOnNewDatabase, and the
initialization applies it instead.
The unit tests cover what a marked update's entry holds, the order of the updates which run (by their dates) and an
empty list. An end to end test initializes a Season 6 database with a plugin's update which adds an item: it's applied
and installed, while OpenMU's own Rena update is only marked, its item there once. The admin docs say how a new
database treats updates.
… own ApplyAsync

Codacy flagged the unused context parameter of the shared override (S1172).
Mosch0512 and others added 28 commits October 25, 2026 16:45
…model

Conflicts: the German resources (both sides added entries at the end), and the compiled model of the item definition and the configuration loader, which are generated again from the merged model. The PostgreSQL test expects the PersistenceConflictException which master now throws around a rejected change.
Two concurrent logins with the same valid code could both read the old
LastAcceptedTotpStep, both pass the check and both sign in. A stale user
saved by another update could also set the step back.

The step is now advanced in a single conditional operation:
IAdminUserRepository.TryAdvanceTotpStepAsync updates it only if the new
step is greater (one conditional UPDATE in the database, a lock for the
in-memory bootstrap user). UpdateAsync no longer writes the column, so a
concurrently loaded user can't move it backwards.

Also resolves the Codacy finding about the culture-specific Replace.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UfKAs82Ga6XgJNq1wYRGcm
…of the initialization

The game logic doesn't need the initialization data: the built-in profiles, the names of their level bonus tables and the rule which item gets which profile are only used to create the profiles of a configuration. The tables are named by their keys of the level bonus names, so the enum of the tables is gone. An item without a profile has the formulas of the profile default, which the game logic can find in the configuration.
Copy users in the detached test repository by serialization instead of
calling MemberwiseClone through reflection, and qualify Lock instead of
importing System.Threading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UfKAs82Ga6XgJNq1wYRGcm
The minigames_leaderboard tool of MUnique#1285 is a 21st read tool, but the
integration test still expected 20, which made master fail.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D7SXuEktJd46sB6iTcG32U
…9hxlu

fix(admin): reject replayed authenticator codes across time steps
…-75-concept-nvwc91

Conflicts in the docs, the plugin resources and the season 6 initializer
are resolved by keeping both sides; the item price definitions are
initialized after the cash shop. The Mapster configurator was deleted on
master.

The migration of the item price definitions is created again on top of
the newer migrations of master. The persistence models, the compiled
model of the EntityDataContext and the GameConfigurationLoader are
generated again, so the configuration loading path of master includes
the item price definitions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HKaEUPxADDTXCqEmoA2xCr
A database from before the item price definitions gets them only by the
mandatory update. Until it's applied, the server would sell special
items like jewels at the price of equipment. The game server logs the
reason and stays stopped instead, so it doesn't accept players, while
the admin panel keeps running to apply the update.

Refs MUnique#75

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HKaEUPxADDTXCqEmoA2xCr
A formula result which is infinite, e.g. of a division by zero, or out of
the range of a long, would become an arbitrary price when it's cast. It
throws like a syntax error now.

Refs MUnique#75

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HKaEUPxADDTXCqEmoA2xCr
The names of the item price definitions come from a resource with
Chinese translations, like the other configuration names on master, and
the new types and properties get their captions in the model resources.
All definitions are created, also the ones which no item of a version
uses yet, so they are ready for items which a server owner adds.

Refs MUnique#75

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HKaEUPxADDTXCqEmoA2xCr
The item of the test is priced by its value, like the scrolls and orbs
of the initialized data, which reference the fixed value price
definition. Without a price definition, it gets the automatic equipment
price since the prices are configuration data.

Refs MUnique#75

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HKaEUPxADDTXCqEmoA2xCr
Loads the season 6 configuration from PostgreSQL like the game servers
do, through the compiled model and the generated loader, and compares
all prices with the frozen calculator, for a new database and for one
which gets the price definitions by the update. Like the other database
tests, it's ignored by default.

Refs MUnique#75

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HKaEUPxADDTXCqEmoA2xCr
…for attributes

Instead of the enums of the client's stats, the terms and the requirement formulas of a profile reference an AttributeDefinition, so a profile isn't limited to predefined stats. The built-in profiles use the attributes which the item formulas compute today: the minimum physical damage (which the maximum damage follows), the staff, sceptre and book rise, the defense rate, the base defense and the shield defense, and the requirement values of the stats. The client's magic defense has no attribute on the server, so the profiles leave it out. The migration is generated again, now after master's, with the unique indexes on the profile and the attribute.
…model

Conflicts: the resources of the plug-ins, where both sides added an update (the item stat profiles and the seed sphere fix); both are kept.
…mes all of them

The update reassigned an item to a variant per attribute, while it went through the profiles one by one. So an item with its own tables for two attributes whose terms come from different profiles, e.g. a shield with its own defense rate table (of the default profile) and its own shield defense table (of the shield profile), lost the second one: its variant named the shield profile's table. Now each term first takes the table most of its items use, and then each item gets one variant which names all of its own tables. The table of an attribute is the one of its own power-ups, so for a weapon whose maximum damage uses another table, the profile names the table of the minimum damage, which the item formulas read; that's documented and tested.
Excellent items can't have sockets. GenerateRandomExcellentItem picked
the definition from the whole droppable list (or the box's list), and
ApplyRandomOptions added sockets before the excellent options were
added, so socket weapons like the Imperial Sword dropped as excellent
items with sockets. Socket items are now filtered out before the
excellent item is selected; they still drop as normal socket items.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rafting-bonus-update-localization

# Conflicts:
#	src/Persistence/Initialization/Properties/PlugInResources.Designer.cs
#	src/Persistence/Initialization/Properties/PlugInResources.de.resx
#	src/Persistence/Initialization/Properties/PlugInResources.resx
#	src/Persistence/Initialization/Properties/PlugInResources.zh-CN.resx
The minigames_leaderboard tool of MUnique#1285 is a 21st read tool, but the
integration test still expected 20, which made master fail.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D7SXuEktJd46sB6iTcG32U
(cherry picked from commit 4ac3659)
…dates localized

The Name and Description properties of these update plugins returned
hard-coded English texts, which the updates page shows, although the
localized resources were already there. They now return the resources,
so the page shows them in German and Chinese, too. The plugin ids and
the update behavior stay the same.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D7SXuEktJd46sB6iTcG32U
Addresses the review: the eligible definitions now have an excellent
option, so the tests assert the drop is excellent and has no sockets.
Also covers the generic monster drop (GetPossibleList) and excellent
item boxes with socket and normal items.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A configured price formula can return a price which overflowed when it
was scaled by the quantity or increased by the option modifiers, e.g.
2^62 for 4 pieces resulted in a price of 0. The quantity scaling uses
128 bit now, and every following step is limited to the maximum price.
They only increase the magnitude of the price, so the results don't
change otherwise.

The automatic price is calculated with 64 bit: with 32 bit, it
overflowed for wings with a high drop level, e.g. +15 excellent wings,
which got negative prices. Such wings don't exist in the shipped data.
The frozen calculator of the ItemFormulasEqualTodayTest got the same
change, as its remarks ask for.

Refs MUnique#75

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HKaEUPxADDTXCqEmoA2xCr
…model

Conflicts with the data-driven item price rules (MUnique#1064): GameConfiguration and GameConfigurationHelper get both collections, the resources keep both sides, and the generated code, the compiled models and the configuration loader are generated again from the merged model. The migration AddItemStatProfiles is generated again after AddItemPriceDefinitions, with its defaults set by hand as before.
@Mosch0512

Copy link
Copy Markdown
Owner Author

Merged upstream as MUnique#1290.

@Mosch0512 Mosch0512 closed this Oct 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants