Repository navigation
test(items): pin today's item requirement, power-up and price formulas for every item - #2
Conversation
…s for every item Adds frozen copies of the item requirement formulas (with the item checks and durability functions they use), ItemPowerUpFactory and ItemPriceCalculator at 90a5711, and a test which compares them with the current code for every item of the 0.75, 0.95d and Season 6 data: at every item level, as normal, excellent (all options and one option), ancient and excellent ancient item (made up where the data has none), at option level 0, 1 and the highest, with harmony and made-up requirement reductions, in every slot and at full, half and no durability. All differences are listed in one report. This prepares moving these formulas into configuration data: that change has to keep every value the same.
Mosch0512
left a comment
There was a problem hiding this comment.
Verdict: changes requested. Four items are required; the other 9 are non-blocking.
Required
- "Half durability" sits exactly on the 50 % threshold, so the 0.8 factor only shows for odd maximum durabilities, and 0.7 and 0.5 never (inline, ItemFormulasEqualTodayTest.cs:437).
- No item gets a skill, Luck, wing options or a 380 option, so those price surcharges are never compared (inline, ItemFormulasEqualTodayTest.cs:234).
- The power-up copy still calls live helpers that change values, and the remarks don't name them, e.g.
IsFixedOption(inline, LegacyItemPowerUpFactory.cs:281). - The power-up copy implements the live
IItemPowerUpFactory, so changing that interface in the next step means editing the copy (inline, LegacyItemPowerUpFactory.cs:26).
Non-blocking
- Only the first normal option of an item is tested (inline, ItemFormulasEqualTodayTest.cs:205).
- Buying prices do depend on the durability, and stacks are never larger than one piece (inline, ItemFormulasEqualTodayTest.cs:457).
Capturekeeps only the exception type (inline, ItemFormulasEqualTodayTest.cs:314).- The live calls that prepare the durability run outside
Capture(inline, ItemFormulasEqualTodayTest.cs:434). - Power-ups that follow the character level always read 0 (inline, ItemFormulasEqualTodayTest.cs:320).
- Nothing says when the test and the copies are removed, or what to do after an intentional formula change (inline, ItemFormulasEqualTodayTest.cs:20).
FormulaTestItemrepeatsTemporaryItem(inline, ItemFormulasEqualTodayTest.cs:476).- About 115 copied lines are never run (inline, LegacyItemPowerUpFactory.cs:114).
- The two
CompareRequirementsoverloads differ only in the list type (inline, ItemFormulasEqualTodayTest.cs:417).
I compared the three copies with the code at 90a5711 by the line ranges in their headers: there are no differences besides the class name in lines 30, 33 and 36 of ItemPowerUpFactory.cs. I checked where each extension method call in the copies binds, and read the test against the price and power-up code and the Season 6 data of wings, capes and stackable items. I didn't build or run the test. CI: build passed.
| var fullDurability = item.GetMaximumDurabilityOfOnePiece(); | ||
| foreach (var slot in slotType.ItemSlots) | ||
| { | ||
| foreach (var durability in new[] { fullDurability, fullDurability / 2 }.Distinct()) |
There was a problem hiding this comment.
Required. GetCurrentDurabilityFactor returns 0.8 only when the durability is below half of the maximum (<, not <=). For an even maximum, e.g. 20, half is 10, and 10 < 10 is false, so this case gets factor 1, the same as full durability. Only odd maxima reach 0.8, and nothing goes below 30 % or 20 %, so the factors 0.7 and 0.5 are never tested. The next step rewrites the excellent and ancient bonuses, which are multiplied by this factor; if it applies the factor wrongly at low durability, every case still matches. One durability under each threshold (e.g. 40 %, 25 % and 10 % of the maximum) would cover all three.
| { | ||
| foreach (var optionLevel in optionLevels) | ||
| { | ||
| var item = new FormulaTestItem { Definition = definition, Level = (byte)level }; |
There was a problem hiding this comment.
Required. The item never gets HasSkill, Luck, wing options or a Jewel of Guardian option. CalculateBuyingPrice adds 150 % for a skill (except the worthless ones), 25 % for Luck, 25 % per wing option and 16 % for the guardian option. So a weapon as the shops sell it (skill and Luck), 2nd and 3rd wings with their wing options, and 380 items are never priced, and a change that reorders or breaks one of these steps passes. Variants with HasSkill and Luck, plus the wing and guardian options where the item has them, would cover those branches.
| var optionOfLevel = option.LevelDependentOptions?.FirstOrDefault(l => l.Level == level); | ||
|
|
||
| // Fixed options (e.g. of the Dinorant) have no level dependent values, so their level doesn't matter. | ||
| if (optionOfLevel is null && level > 1 && !option.IsFixedOption()) |
There was a problem hiding this comment.
Required. IsFixedOption comes from src/GameLogic/ItemOptionExtensions.cs, which is live code, and here it decides whether an option's power-up is skipped. PowerUpWrapper.CreateByPowerUpDefinition, Boost.GetSimpleValueElement, ItemBasePowerUpDefinition.GetBaseValueElement and LevelBonus.GetAdditionalValueElement are live too, and they apply the durability factor. A change in any of them moves the copy and the live code together, so the test can't see it. The remarks in LegacyItemExtensions.cs (lines 23-24) name only IsWearable, IsTrainablePet, IsDarkRaven, IsWing and Durability. Copying IsFixedOption into LegacyItemExtensions (the call then binds there, like the others) and naming the rest in the remarks would make the claim true.
| /// Lines 17-455 are copied unchanged, except the class name in lines 30, 33 and 36. | ||
| /// The item checks, the drop level and the durability factor come from <see cref="LegacyItemExtensions"/>. | ||
| /// </remarks> | ||
| internal class LegacyItemPowerUpFactory : IItemPowerUpFactory |
There was a problem hiding this comment.
Required. The copy implements the live IItemPowerUpFactory. The next step gives the live side the configuration; if that adds a parameter to GetPowerUps or GetSetPowerUps, this class stops compiling and has to be changed, although its header says not to. The test calls GetPowerUps through a delegate, so the interface isn't needed. Removing : IItemPowerUpFactory (and the inheritdoc tags) keeps the copy independent.
| public void CompareItem(ItemDefinition definition) | ||
| { | ||
| var possibleOptions = definition.PossibleItemOptions.SelectMany(o => o.PossibleOptions).ToList(); | ||
| var option = possibleOptions.FirstOrDefault(o => o.OptionType == ItemOptionTypes.Option); |
There was a problem hiding this comment.
Non-blocking. FirstOrDefault takes only the first option of type Option. Several items have two or three: Wings of Spirits (HP recovery, then physical damage), the 3rd wings (plus defense), Cape of Fighter (HP recovery, then physical damage), and the Dinorant with its fixed options. So the cape's physical damage option (whose price gets no option bonus), the wings' defense option, more than one Dinorant option in its price, and the HP recovery exception from the durability factor (LegacyItemPowerUpFactory.cs:323) are covered only when that option comes first. Looping over all options of type Option would cover them.
|
|
||
| private static string DescribePowerUps(Item item, Func<Item, AttributeSystem, IEnumerable<PowerUpWrapper>> getPowerUps) | ||
| { | ||
| using var attributes = new AttributeSystem([], [], []); |
There was a problem hiding this comment.
Non-blocking. The excellent options "damage +level/20" (wizardry, curse and physical) follow Stats.TotalLevel through a related value. In this empty attribute system that input is 0, so their value is always 0 and only the target and the aggregate type are compared. Giving the attribute system a level (e.g. 400) before reading the elements would compare their values too.
|
|
||
| /// <summary> | ||
| /// Tests that the item requirement, power-up and price formulas give for every item of every version's data | ||
| /// what the server computed at commit 90a571100, which the frozen copies in the <c>Legacy</c> folder keep. |
There was a problem hiding this comment.
Non-blocking. The copies say "Don't change it", and the test fails on any intentional change to the requirements, power-ups or prices, e.g. fixing the DevilInvitation price that the code itself marks with Bug?. Nothing says whether the test and the Legacy folder are removed after the move to configuration data, or how to update them after such a fix. A sentence about that in the remarks would help whoever changes a formula next.
| /// <summary> | ||
| /// An item which isn't persisted. | ||
| /// </summary> | ||
| private sealed class FormulaTestItem : Item |
There was a problem hiding this comment.
Non-blocking. This is MUnique.OpenMU.GameLogic.TemporaryItem (src/GameLogic/TemporaryItem.cs): an item that isn't persisted, with the same constructor. The file already imports MUnique.OpenMU.GameLogic, so new TemporaryItem { Definition = definition, Level = (byte)level } would do.
| } | ||
|
|
||
| /// <inheritdoc/> | ||
| public IEnumerable<PowerUpWrapper> GetSetPowerUps( |
There was a problem hiding this comment.
Non-blocking. The test never calls GetSetPowerUps, GetOptionCombinationBonus and AreRequiredOptionsFound (about 110 lines), and IsTimeDecaying in LegacyItemExtensions.cs isn't called either. They come from the interface and the contiguous line ranges, and they suggest that the set bonuses are pinned, which they aren't. Either compare GetSetPowerUps too, or leave them out together with the interface.
| } | ||
| } | ||
|
|
||
| private void CompareRequirements(Item item, List<AttributeRequirement> madeUpRequirements, string itemCase) |
There was a problem hiding this comment.
Non-blocking. This overload and the one at line 405 differ only in the element type of the list (List<AttributeRequirement> and List<ReductionOption>), and they call each other. Passing the other list still compiles and silently picks the other overload. A name of its own, e.g. CompareRequirementsWithReductions, would make each call clear.
…nterface A change of IItemPowerUpFactory, e.g. a configuration parameter, would otherwise force a change of the copy.
…ozen copies The test never ran them, and they suggested that the set bonuses are pinned, which they aren't.
…till call The copies no longer import the game logic's namespace, so none of its helpers can move the copy and the live code together; the remarks name the live data model code which builds the power-up elements.
…name The two overloads differed only in the element type of their list, so passing the other list would have picked the other one silently.
…essage A throw no longer replaces the whole list of power-ups or requirements, and two failures of the same type with different messages now differ. The throwing method isn't compared, because the live code may compute the same in other methods.
…ifference The cases were prepared outside the capture, so an item which throws there would have stopped the test without reporting the others.
…er level The empty attribute system read the total level as 0, so the excellent damage +level/20 options were compared by their target only.
…ity factor Half of an even maximum durability isn't below the 50 % threshold, so the factor 0.8 showed only for odd maxima, and 0.7 and 0.5 never.
…tacks The buying prices of potions, arrows and other stackable items multiply the number of pieces, which is their durability; a stack of more than one piece wasn't priced.
…ptions The prices add a surcharge for a skill, luck, each wing option and the option of the Jewel of Guardian, which no compared item had.
…ions together Only the first option of type Option was tested, but wings, capes and the Dinorant have two or three.
… change needs The copies said only "Don't change it"; now the test's remarks say how to update them after an intentional formula change and when they can be removed.
The durability factor multiplies the same parts at every item level, so the durabilities below its thresholds are compared at the lowest and the highest item level; the prices, which have no durability factor, at full, 40 % and no durability, and with the item's skill and extra options at full durability. Season 6 takes about 33 s instead of 61 s.
…nguage The item names already were; the option type names followed the culture of the machine.
They import MUnique.OpenMU.GameLogic.Attributes, so PowerUpWrapper and Stats are live code too; only the extension methods of the game logic can't change the copies and the live code together.
…tory The copy's namespace lies within MUnique.OpenMU.Persistence, so GetId() resolves without it (Codacy).
Why
The next change moves the item requirement, power-up and price formulas into configuration data, so that the client's item sync can read and write them. With the default data, players must see exactly the same values as before. This PR adds the proof first, without changing any game code: a test that compares today's formulas with the current code for every item.
What
Frozen copies of today's code at 90a5711, in
tests/MUnique.OpenMU.Persistence.Initialization.Tests/ItemFormulas/Legacy/:LegacyItemExtensions: the requirement formulas (GetRequirement,CalculateDropLeveland the two private helpers) plus the item checks and durability functions they use, fromsrc/GameLogic/ItemExtensions.cslines 19-106, 131-141, 174-289, 361-426, 474-500 and 512-561.LegacyItemPowerUpFactory:src/GameLogic/ItemPowerUpFactory.cslines 17-455; only the class name changed.LegacyItemPriceCalculator:src/GameLogic/ItemPriceCalculator.cslines 19-581.Each header names its line ranges, so the copy can be checked with a line-range diff. The extension method calls in the copies bind to
LegacyItemExtensions, because extension methods of the own namespace come before imported ones. So the copies don't depend on the file the next change edits; they still callIsWearable,IsTrainablePet,IsDarkRaven,IsWingandDurabilityofsrc/DataModel/ItemExtensions.cs.ItemFormulasEqualTodayTestruns every item of the 0.75, 0.95d and Season 6 data through both sides and compares:Cases:
A wearable item which can't be excellent or ancient in the data gets a made-up excellent option or ancient set, so that every kind of item (sceptres, books, wings, pets...) runs through those formulas. The requirements are also checked:
All differences are listed in one report, with a count per kind.
TestGameConfiguration.Create075Async.Testing
dotnet build src/MUnique.OpenMU.sln -p:ci=true, thendotnet test tests/MUnique.OpenMU.Persistence.Initialization.Tests --no-build -p:ci=true: all 674 tests pass (7 skipped, as before). The new test takes about 34 s (Season 6 23 s, 0.95d 8 s, 0.75 3 s).To see that the test catches changes, I broke 10 formulas in the live code on purpose, one round at a time, and reverted them afterwards. The test reported each one:
IsArmorItem(this one shows that the copies no longer use the live item checks).0.75 has no excellent items, so it only fails for the breaks that affect normal items.