<freeStyleBuild _class='hudson.model.FreeStyleBuild'><action _class='hudson.model.CauseAction'><cause _class='hudson.triggers.SCMTrigger$SCMTriggerCause'><shortDescription>Started by an SCM change</shortDescription></cause></action><action></action><action _class='hudson.plugins.git.util.BuildData'><buildsByBranchName><refsremotesoriginmaster _class='hudson.plugins.git.util.Build'><buildNumber>355</buildNumber><marked><SHA1>c1aad166e5bd658877dd44d5441269579047e959</SHA1><branch><SHA1>c1aad166e5bd658877dd44d5441269579047e959</SHA1><name>refs/remotes/origin/master</name></branch></marked><revision><SHA1>c1aad166e5bd658877dd44d5441269579047e959</SHA1><branch><SHA1>c1aad166e5bd658877dd44d5441269579047e959</SHA1><name>refs/remotes/origin/master</name></branch></revision></refsremotesoriginmaster></buildsByBranchName><lastBuiltRevision><SHA1>c1aad166e5bd658877dd44d5441269579047e959</SHA1><branch><SHA1>c1aad166e5bd658877dd44d5441269579047e959</SHA1><name>refs/remotes/origin/master</name></branch></lastBuiltRevision><remoteUrl>https://github.com/mcMMO-Dev/mcMMO.git</remoteUrl><scmName></scmName></action><action></action><action></action><action></action><action _class='org.jenkinsci.plugins.displayurlapi.actions.RunDisplayAction'><artifactsUrl>https://popicraft.net/jenkins/view/all/job/mcMMO/355/artifact</artifactsUrl><changesUrl>https://popicraft.net/jenkins/view/all/job/mcMMO/changes</changesUrl><displayUrl>https://popicraft.net/jenkins/view/all/job/mcMMO/355/</displayUrl><testsUrl>https://popicraft.net/jenkins/view/all/job/mcMMO/355/testReport</testsUrl></action><artifact><displayPath>mcMMO.jar</displayPath><fileName>mcMMO.jar</fileName><relativePath>target/mcMMO.jar</relativePath></artifact><artifact><displayPath>original-mcMMO.jar</displayPath><fileName>original-mcMMO.jar</fileName><relativePath>target/original-mcMMO.jar</relativePath></artifact><building>false</building><displayName>#355</displayName><duration>107352</duration><estimatedDuration>89124</estimatedDuration><fullDisplayName>mcMMO #355</fullDisplayName><id>355</id><inProgress>false</inProgress><keepLog>false</keepLog><number>355</number><queueId>268</queueId><result>SUCCESS</result><timestamp>1787175241041</timestamp><url>https://popicraft.net/jenkins/view/all/job/mcMMO/355/</url><builtOn>Builder</builtOn><changeSet _class='hudson.plugins.git.GitChangeSetList'><item _class='hudson.plugins.git.GitChangeSet'><affectedPath>src/main/resources/locale/locale_zh_CN.properties</affectedPath><commitId>787246ec22f08c88da40e298d5d73e76626e027c</commitId><timestamp>1784126393000</timestamp><author><absoluteUrl>https://popicraft.net/jenkins/user/github</absoluteUrl><fullName>github</fullName></author><authorEmail>noreply@github.com</authorEmail><comment>Update locale_zh_CN.properties

- Sync with latest translation keys
- Reorder keys to match en_US file
- Add and revise some translation entries
</comment><date>2026-07-15 22:39:53 +0800</date><id>787246ec22f08c88da40e298d5d73e76626e027c</id><msg>Update locale_zh_CN.properties</msg><path><editType>edit</editType><file>src/main/resources/locale/locale_zh_CN.properties</file></path></item><item _class='hudson.plugins.git.GitChangeSet'><affectedPath>src/test/java/com/gmail/nossr50/datatypes/player/McMMOPlayerBankedXpConversionTest.java</affectedPath><affectedPath>src/test/java/com/gmail/nossr50/util/experience/FormulaManagerTest.java</affectedPath><affectedPath>src/main/java/com/gmail/nossr50/util/experience/FormulaManager.java</affectedPath><affectedPath>Changelog.txt</affectedPath><commitId>48a049b8d9e8bff5bdd4e71db0df44eab1fdfe61</commitId><timestamp>1787174265000</timestamp><author><absoluteUrl>https://popicraft.net/jenkins/user/nossr50</absoluteUrl><fullName>nossr50</fullName></author><authorEmail>nossr50@gmail.com</authorEmail><comment>Keep next-level XP requirements positive and overflow-safe

The Standard-mode requirement sums ten Retro levels in an int, which
overflows at extreme skill levels and wraps to a garbage value, negative
or far too small. Zero or negative experience.yml formula values could
also push a requirement to zero or below. A non-positive requirement
never drains banked XP, so the level-up loop in checkXp gains levels
nonstop until the level cap, which defaults to Integer.MAX_VALUE.

The ten-level sum now accumulates in a long and saturates at
Integer.MAX_VALUE, and every computed requirement is clamped to at
least 1. Results are unchanged for curves that never left int range.

Tests pin the default-curve requirement values for both scaling modes,
the saturation and clamp behavior, and how banked XP converts into
levels on the next gain after requirements shrink, grow, or hit the
level cap.
</comment><date>2026-08-19 14:17:45 -0700</date><id>48a049b8d9e8bff5bdd4e71db0df44eab1fdfe61</id><msg>Keep next-level XP requirements positive and overflow-safe</msg><path><editType>edit</editType><file>Changelog.txt</file></path><path><editType>add</editType><file>src/test/java/com/gmail/nossr50/util/experience/FormulaManagerTest.java</file></path><path><editType>add</editType><file>src/test/java/com/gmail/nossr50/datatypes/player/McMMOPlayerBankedXpConversionTest.java</file></path><path><editType>edit</editType><file>src/main/java/com/gmail/nossr50/util/experience/FormulaManager.java</file></path></item><item _class='hudson.plugins.git.GitChangeSet'><affectedPath>src/main/java/com/gmail/nossr50/api/ExperienceAPI.java</affectedPath><affectedPath>Changelog.txt</affectedPath><affectedPath>src/test/java/com/gmail/nossr50/api/ExperienceAPIOfflinePersistenceTest.java</affectedPath><commitId>a3b0c3518c07cf9cc686852439d785498b10af36</commitId><timestamp>1787174265000</timestamp><author><absoluteUrl>https://popicraft.net/jenkins/user/nossr50</absoluteUrl><fullName>nossr50</fullName></author><authorEmail>nossr50@gmail.com</authorEmail><comment>Persist offline ExperienceAPI edits

setXPOffline, setLevelOffline, and removeXPOffline mutated a profile
freshly loaded from the database and let it go out of scope without
saving, so the calls were silent no-ops. They now schedule an async
save like their addOfflineXP and addLevelOffline counterparts.
</comment><date>2026-08-19 14:17:45 -0700</date><id>a3b0c3518c07cf9cc686852439d785498b10af36</id><msg>Persist offline ExperienceAPI edits</msg><path><editType>add</editType><file>src/test/java/com/gmail/nossr50/api/ExperienceAPIOfflinePersistenceTest.java</file></path><path><editType>edit</editType><file>Changelog.txt</file></path><path><editType>edit</editType><file>src/main/java/com/gmail/nossr50/api/ExperienceAPI.java</file></path></item><item _class='hudson.plugins.git.GitChangeSet'><affectedPath>src/main/java/com/gmail/nossr50/util/EventUtils.java</affectedPath><affectedPath>Changelog.txt</affectedPath><affectedPath>src/test/java/com/gmail/nossr50/util/EventUtilsTest.java</affectedPath><commitId>f15e53e9f179250083b3c1f9897b8c6a70afa843</commitId><timestamp>1787174265000</timestamp><author><absoluteUrl>https://popicraft.net/jenkins/user/nossr50</absoluteUrl><fullName>nossr50</fullName></author><authorEmail>nossr50@gmail.com</authorEmail><comment>Keep leftover banked XP when a level change event is cancelled

The cancel rollback restored the skill level with modifySkill, which
zeroes banked XP, then re-added only the XP the reverted levels had
consumed; any XP banked beyond those levels was destroyed. The rollback
now captures the leftover before the level restore and returns it
together with the consumed XP. Command paths like /mmoedit and
/skillreset are unaffected because they zero XP before firing the
event, leaving no leftover to capture.
</comment><date>2026-08-19 14:17:45 -0700</date><id>f15e53e9f179250083b3c1f9897b8c6a70afa843</id><msg>Keep leftover banked XP when a level change event is cancelled</msg><path><editType>edit</editType><file>Changelog.txt</file></path><path><editType>edit</editType><file>src/main/java/com/gmail/nossr50/util/EventUtils.java</file></path><path><editType>edit</editType><file>src/test/java/com/gmail/nossr50/util/EventUtilsTest.java</file></path></item><item _class='hudson.plugins.git.GitChangeSet'><affectedPath>src/test/java/com/gmail/nossr50/util/skills/SkillToolsTest.java</affectedPath><affectedPath>src/main/java/com/gmail/nossr50/util/skills/SkillTools.java</affectedPath><commitId>c41f2e4ae2984d2b4aef2d42812eebbf89d2c240</commitId><timestamp>1787174265000</timestamp><author><absoluteUrl>https://popicraft.net/jenkins/user/nossr50</absoluteUrl><fullName>nossr50</fullName></author><authorEmail>nossr50@gmail.com</authorEmail><comment>Swap the skill name match cache wholesale on locale reloads

matchSkill cleared its cache map in place when the locale generation
changed, so a lookup racing a locale reload could insert a match
computed against the old locale after the clear, and that stale entry
would survive until the next reload. The cache now lives in a record
stamped with the generation it was built for and is replaced wholesale
on a generation change; an in-flight lookup writes into its own
generation map, which the swap makes unreachable.

Tests pin the cache contract: hits are served without re-reading the
locale until the generation changes, a generation change drops every
cached match, and failed lookups are never cached.
</comment><date>2026-08-19 14:17:45 -0700</date><id>c41f2e4ae2984d2b4aef2d42812eebbf89d2c240</id><msg>Swap the skill name match cache wholesale on locale reloads</msg><path><editType>edit</editType><file>src/main/java/com/gmail/nossr50/util/skills/SkillTools.java</file></path><path><editType>edit</editType><file>src/test/java/com/gmail/nossr50/util/skills/SkillToolsTest.java</file></path></item><item _class='hudson.plugins.git.GitChangeSet'><affectedPath>src/test/java/com/gmail/nossr50/locale/LocaleLoaderSnapshotTest.java</affectedPath><affectedPath>Changelog.txt</affectedPath><affectedPath>src/main/java/com/gmail/nossr50/locale/LocaleLoader.java</affectedPath><commitId>10f6bd31ed29214ea8b0ca81919d4dcd7e7dc83d</commitId><timestamp>1787174265000</timestamp><author><absoluteUrl>https://popicraft.net/jenkins/user/nossr50</absoluteUrl><fullName>nossr50</fullName></author><authorEmail>nossr50@gmail.com</authorEmail><comment>Publish locale bundles as an immutable snapshot

The bundles and their string cache were separate static fields mutated
in place by reloadLocale, so a reader on another thread could pair
bundles from one locale with cached strings from the other, observe the
half-cleared state and throw, or race the override-file copy when two
reloads overlapped. The bundles and cache now live in one immutable
snapshot behind a volatile field; a reload builds the new snapshot
fully and publishes it with a single write, and loads are serialized
under a lock. The locale generation bumps after the snapshot is
published so generation-keyed consumers rebuild from the new strings.
</comment><date>2026-08-19 14:17:45 -0700</date><id>10f6bd31ed29214ea8b0ca81919d4dcd7e7dc83d</id><msg>Publish locale bundles as an immutable snapshot</msg><path><editType>add</editType><file>src/test/java/com/gmail/nossr50/locale/LocaleLoaderSnapshotTest.java</file></path><path><editType>edit</editType><file>Changelog.txt</file></path><path><editType>edit</editType><file>src/main/java/com/gmail/nossr50/locale/LocaleLoader.java</file></path></item><item _class='hudson.plugins.git.GitChangeSet'><affectedPath>Changelog.txt</affectedPath><commitId>8d1c1442f7f8e448a1311bb5395b35def1e1b953</commitId><timestamp>1787174265000</timestamp><author><absoluteUrl>https://popicraft.net/jenkins/user/nossr50</absoluteUrl><fullName>nossr50</fullName></author><authorEmail>nossr50@gmail.com</authorEmail><comment>Update changelog
</comment><date>2026-08-19 14:17:45 -0700</date><id>8d1c1442f7f8e448a1311bb5395b35def1e1b953</id><msg>Update changelog</msg><path><editType>edit</editType><file>Changelog.txt</file></path></item><kind>git</kind></changeSet><culprit><absoluteUrl>https://popicraft.net/jenkins/user/nossr50</absoluteUrl><fullName>nossr50</fullName><id>nossr50</id></culprit><culprit><absoluteUrl>https://popicraft.net/jenkins/user/github</absoluteUrl><fullName>github</fullName><id>github</id></culprit></freeStyleBuild>