Changelog
Complete version history
Full release history with all features, fixes, and technical changes. For a user-friendly overview, see the Patch Notes or the CurseForge page.
v1.7.4 developer notes
Built on Hytale Update 7 like 1.7.3, so ServerVersion is unchanged, against ZiggfreedCommon 2.5.1 and the RPG Stations 1.1.3 api (contract 10). MMOSkillTreeAPI and the api project are unchanged, and nothing is removed.
Breaking changes
- Requires ZiggfreedCommon 2.5.1.
ziggfreedCommonVersionandMMOSkillTreePlugin.REQUIRED_ZIGGFREED_COMMONread2.5.1, so the manifest's^2.5.1refuses 2.5.0 by name. The jar calls nothing 2.5.1 added; the pin moves so every server on 1.7.4 runs 2.5.1's fixes and its rollback sweep.rpgStationsApiVersionstays1.1.3, and the manifest's optionalZiggfreed:RpgStationsrange stays>=1.1.1 <2.0.0.
Technical
- The Skills Menu Scroll and the retired Quest Book open on right click.
Mmo_Menu_Scroll's Charging chain intommo_menu_openandMmo_Quest_Book's intommo_quest_menu_openmove fromPrimarytoSecondary, as CommonLib 2.5.1 moves the Objective Book, so a left click no longer opens either. - The Item Rewards room check asks CommonLib.
CommandRewardServiceasks CommonLib's room probe (GroundSpillSinks.pickupFits: hotbar, storage and backpack, the room vanilla/givefills) before an authored/give, on the Item Rewards Collect and for auto-claim rewards at level-up. It asked only the Storage section (canAddItemStacks), so a full storage refused a reward the hotbar or backpack had room for. It still fails closed with no live reference. - A level's
/giverewards share one room check. One Collect press and one level-up auto-claim pass ask about each/givereward together with the gives already let through (the newcommandreward.GiveBatchoverGroundSpillSinks.pickupFits), since a dispatched/givelands later on the command pool: two rewards that each fit alone could both pass, and the second was lost. A reward that would no longer fit waits for the next press, and the rewards after it still pay when they fit.CommandRewardService.claimSingleRewardtakes aGiveBatch(GiveBatch.of(player)) in place of thePlayer. Test:GiveBatchTest.
v1.7.3 developer notes
Built on Hytale Update 7 like 1.7.2, so ServerVersion is unchanged, against ZiggfreedCommon 2.5.0 and the RPG Stations 1.1.3 api (contract 10). MMOSkillTreeAPI and the api project are unchanged, and nothing is removed.
Breaking changes
- Requires ZiggfreedCommon 2.5.0.
ziggfreedCommonVersionandMMOSkillTreePlugin.REQUIRED_ZIGGFREED_COMMONread2.5.0, so the manifest's^2.5.0refuses 2.4.x by name: the jar calls 2.5.0'sObjectiveTargetNamesandRequiresVolumeAudit.auditAll(below).rpgStationsApiVersionmoves to1.1.3(RPG Stations' Greater Disenchanting Table recipe fix), and the manifest's optionalZiggfreed:RpgStationsrange stays>=1.1.1 <2.0.0.
Technical
- Swords gets its XP ladder, and the combat ladders pay combat.
Achievements/MMOSkillTree/Combat/Weapons/Swords_Xp_T1.jsonto_T3.jsoncopy the Axes ladder rung for rung (chainswords_xp,GAIN_XPonSWORDSat 10,000, 100,000 and 1,000,000, paying SwordsMmo_Xp500, 2,500 and 50,000 and a 2x Swords boost for 60 minutes at the top; meleeSortOrder70 to 72), with four newmmoskilltree.langkeys (achievement.swords_xp_t1.titleto_t3.title,achievement.announce.swords_xp_t3.title) in every locale. In thecombat_killsandcombat_damageladders,First_Bloodpays anMmo_Lampof 100 overCOMBATin place of DefenseMmo_Xp100, andWarlordandForce_Of_Naturedrop their SwordsMmo_Boost_Token(2x, 90 and 60 minutes), keeping their 50,000Mmo_LampoverCOMBAT, so every rung of both ladders pays a combat lamp. (A boost token naming theCOMBATcategory reads as a non-active skill to the achievement audit,NON_ACTIVE_REWARD_SKILL.)
- The last direct hand-overs go through CommonLib's delivery. The mastery respec's item refund (
MasteryService), the Class System's starting items (ClassService) and the ability cast item restore (AbilityCastItemService) hand items over through CommonLib's one delivery (LootRewardKinds.handToPlayer) instead of adding them straight to the Storage section. The respec refund is one spill: what fits goes where the player's pickup settings route it, with the game's pickup notice, the rest at their feet, and a part that lands nowhere is parked on the pending-rewards spool as a plaingiveline, whichLootRewardKinds.replayParkedpays at the next connect (never a CommonLibItemreward, which would count as loot for loot quests); the call is wrapped so a throw cannot skip the bookkeeping after the currency refund. The cast restore probesGroundSpillSinks.pickupFitsfirst, so it counts the hotbar and backpack, and still refuses on a full bag rather than spilling, so its "free up space" line stays true. - Gear-set steps name the set.
TargetNameResolverasks ZiggfreedCommon'sObjectiveTargetNamesfor a kind it has no reading of its own for (GEAR_SET_TIERtoday) before prettifying the id, so a gear-set step with no text of its own names the set by itsTitleKeyon the MMO's pages too.objective.text.gear_set_tierreads "Wear {0} pieces of the {1}", retranslated in every locale. /mmoconfig validatereports trigger-volume gates.ContentAuditregisters thegatedomain, "ZigRequires volume validation", from ZiggfreedCommon 2.5.0'sRequiresVolumeAudit.auditAll(), so/mmoconfig validateand the startup content audit report a pack'sZigRequirestrigger-volume problems beside every other content domain.- Tests and docs.
HallowsEveRetrofitPinTest's Old Jack conversation method andHarvestFeastPackContentTest's Martha intro method, which pinned the hosts'ForgottenTempleopening beats and temple-scoped greeting memory that Seasons of Orbis 1.3.0 drops, are deleted with their helpers.SeasonsOfOrbisFactionTest.everyReputationAndWalletThePackPaysOrChargesIsOneItOrAGatedCompanionShipsagain checks that every reputation and wallet the pack pays or charges is one it ships or a gated companion pack's (the bounty pack'sLife_Essencebehind its mod gate), with a fixtures test for its gate branches.CONTENT_PACKS.md's encounter macros table lists CommonLib'sZc_Wave_RiseandZc_Wave_Chain, andZigFeedback's row names thewaveargument every beat carries.
v1.7.2 developer notes
Built on Hytale Update 7 (0.7.0) like 1.7.1, so ServerVersion is unchanged, against ZiggfreedCommon 2.4.0 and the RPG Stations 1.1.2 api (contract 10). MMOSkillTreeAPI and the api project are unchanged. What a pack, an owner file or a caller can no longer reach is under Breaking changes and Removed.
Breaking changes
- Requires ZiggfreedCommon 2.4.0.
ziggfreedCommonVersionandMMOSkillTreePlugin.REQUIRED_ZIGGFREED_COMMONread2.4.0, so the manifest's^2.4.0refuses 2.3.0 by name: the jar uses 2.4.0's round kinds and its default board reading (below).rpgStationsApiVersionreads1.1.2, the RPG Stations release this build compiles against; the manifest's optionalZiggfreed:RpgStationsrange stays>=1.1.1 <2.0.0, since the jar calls nothing 1.1.2 added. - The jar no longer ships Hallow's Eve's MMO layer. Its eleven
Hallows_Evefiles go: the boost ruleMmo_Hallows_Eve_Harvest_Moon, the mob-kill mapMmo_Hallows_Eve, the XP mapHallows_Eve, the station extensionsMmo_Hallows_Eve_CarvingandMmo_Hallows_Eve_Carving_Hallowed, the four skill dailiesMmo_Hallows_Eve_Daily_{Artillery,Crafting,Harvesting,Mining}, the gear setMmo_Hallows_Eve_Hallowedand the lootableMmo_Hallows_Eve_Geode_Crack, with their lines in all nine locales (board.grade.hallows_eve_skill,boostrule.mmo_hallows_eve_harvest_moon.title,gearset.mmo_hallows_eve_hallowed.tier_fulland the dailies'quest.mmo_hallows_eve_daily_*pairs). Seasons of Orbis has carried the same ids and keys since 1.1.0 (itshallowseve.mmo.lang), so a server running it sees no change.HallowsEveLayerTwinTest, which held the two copies equal, is deleted; the layer tests (HallowsEveLayer,HallowsEveJarContentTest,HallowsEveMmoLangTestand the rest) read the pack alone. - Items keep their own interaction maps.
AbilityInteractionInjector, which wroteMMO_Trigger_Ability1into every item'sAbility1slot, is retired with its registration and test: the per-player key claim already owns Q whenever a bound slot can cast. The never-loadedServer/UnarmedInteractions/Empty.jsonand its scan directory go. The three unreachable hold-to-charge rootsMMO_Trigger_Ability1_Charged,MMO_Trigger_Ability2_ChargedandMMO_Trigger_Ability3_Chargedare removed, so a third-party item that names one loses that item at load; it re-authors on the plainMMO_Trigger_Ability1toMMO_Trigger_Ability3.TriggerKey's dead labels go;MmoCastAbilityInteraction'sTriggerandNativeInputdocs,CONTENT_PACKS.mdand the trigger roots' comments match the code, and the interaction and ability schemas are regenerated. Player-visible only in one case: Q on an item with a freeAbility1slot while abilities are off is now silent. - Three player commands check their own node.
/mmoachievements,/mmoabilitiesand/mmoability's player subcommands (use,bind,unbind,input,list) checkmmoskilltree.command.mmoachievements,mmoskilltree.command.mmoabilitiesandmmoskilltree.command.mmoabilitywhen command permissions are on, like every other command. Every per-command denial goes through one helper,PermissionUtil.requireCommandPermission, on the existingcmd.no_permissionkey instead of raw English, so it reads in the player's language. - School resistances are CommonLib's
DamageResist_stats. The elevenMMO_SchoolResist_stat assets are removed, with no grace version:SchoolResistStatsaliases each to its successors (Physical toDamageResist_Physicaland_Projectile), so an item enhanced before reads its roll under the new ids (StackStatsnormalizes through the aliases) and stores them on its next stamp. Its elevenitemTooltip.stats.MMO_SchoolResist_*lines stay inclient.lang, as the retiredMMO_Bonusones did: the tooltip a stamp wrote onto such an item (ItemDisplayMetadata, written byStampTooltiponly at a stamp or/mmoitemenhance refresh) still names the old key until then. A third party's item naming an old id in its nativeStatModifiersstill loads, but that stat gives nothing (the engine skips a stat id it does not know): rename it toDamageResist_, a Physical resistance on both_Physicaland_Projectile.
Technical
- One applier for school resistance, CommonLib's. Every writer (a claimed
SCHOOL_RESISTand class passives throughfoldGlobalContributions, mastery'sschoolResistmirror,/mmoitemenhance's armor roll) writesDamageSchools.resistStatIdsas a modifier on the stat's Max, which is what resists (a permanentMmoChangeStatmoves the value, so it resists nothing, as before; it still syncs), and ZiggfreedCommon 2.4.0'sDamageResistReconcilerkeeps the engine's resistance in step with those stats every tick, soSchoolResistReconciler, itsMMO_SchoolResistSync_*effects (dropped by the engine at load, never swept) and its bridge listener go.SchoolResistStatsinstalls the per-world combat-bonus rule (DamageResistances.installGate) and maps theSchoolResistclamp'sMaxonto the ceiling (installCeiling;maxPercent()for the clamps page andDamageSchoolValidator); the defense fold still reads no school term. Tests:SchoolResistReconcilerTestmoves to CommonLib'sDamageResistReconcilerTest(its defense-fold check toDefenseFoldNoSchoolTermTest, its clamp check toSchoolResistStatsTest, which also reads the alias table directly against the eleven retired ids);DamageSchoolsStatIdTest, the roster's resist-label check (nowDamageResistStatsTestin CommonLib) and an armor roll's one-to-three entry count go, andAuthoredStatTypeDecodeTestdrops the removed assets. - Bounty boards open on CommonLib's quest page. ZiggfreedCommon 2.4.0's
CommercePages.openBoardopens every board on the quest page with the board as its host, soMmo_Board,BountyBoardOpenInteraction,/mmobountyuiand the hub's contract deep link land there with no MMO change.MmoBounties.difficultyColor(both overloads) retires withCOLOR_UNGRADED,boardOrNulland their imports, since CommonLib paints a contract's grade from its board's own colours;MmoBountiesTestand itsMmoBountiesTestBoards, which covered only them, are deleted. Comments that named the old board page or board screen (BountyBoardOpenInteraction,ProgressStateMigration,ProgressionUnclaimedMoveTest,MmoHubPage,OnboardingPage) name the quest page. - The Objective Book's contracts read CommonLib's default board reading.
MmoObjectiveBookDepsno longer fillsboardManaged: itsBountyPresentation,bountyPillandgradePillgo, and ZiggfreedCommon 2.4.0'sBoardContractspaints a carried contract's tag word, its grade pill in the board's own colour and where it hands in.ui.quests.bounty_pillandui.quests.bounty_at_boardleave all nine locales;MmoObjectiveBookDepsTest's two tests of the retired pills are deleted. - Records' Skills tab ranks each period by XP gained.
SkillRecordsSplitopts into CommonLib'sStatSplit.offAllTime: on a week, a month or a season, Total Level, each category and each skill pick rankXp_Earnedgained in that period (StatisticsComponent.KEY_XP_EARNED, counted at each grant: the total on Total Level, beside abilities cast and station cycles, a category's skills summed, a skill's own line), headedui.leaderboard.header_xp_gained("XP GAINED"), with the player's All Time level beside their name (StatSplit.MetaoverSkillGauges.SKILL_LEVEL, parts over 1,ui.leaderboard.level_beside_name=Lv {0, number}). All Time ranks level with XP breaking a tie, as in 1.7.1. The period buckets fill from this version on. New en-US keysui.leaderboard.header_xp_gainedandui.leaderboard.level_beside_name;Mmo_Skills.json's$Commentsays so. - A level-up shows in an open page.
notifyLevelUpByNamesent only a HUD notification, which sits hidden behind an open page. With a toastable page showing it draws a REWARD page toast instead: the skill's icon and the same line the HUD notice carries (the new-player tip at levels 2 and 3, else the skill tree reward the level unlocks, else none), rankedToastSpec.PRIORITY_EARNED, so the reach-level quest step fired right after waits behind it in CommonLib's toast queue. With no page open, the HUD notification as before; never both. The level-up sound still plays apart and the toast adds none. - Reward toasts section their lootables.
RewardListRenderer.toastLineshands its per-reward reading to CommonLib'sRewardToastLines.linesBy, so a quest or claim paying XP and aLootableshows the XP first and the bundle under its own heading;QuestToastsdrops its duplicate cap.ToastablePage.showRewardToast(headline, skills, rewards)hands those rows, already sectioned and capped, straight to a privateshowRewardRows, so a receipt toast is capped once; the overload taking pre-built lines still caps the lines its callers built. - A skill's item rewards preview its XP token. The global XP token reward gives
XpToken_{skill}_Token, and only the claim filled{skill}(MmoPlaceholders.skillCommand), so the item-rewards ladder previewed the literal id, which no item answers to: the client showed "Invalid Item" over a blank slot. A skill's ladder fills{skill}first by the claim's own rule (newMmoPlaceholders.skillOnly, through aRewardPreviewBuilder.fromoverloadItemRewardsPagepasses its skill to); the total level's ladder, with no one skill, reads the line as authored. Only the item and currency reads use the filled line; a boost line reads as authored, since the fill is a skill's display name. - A named boost reward reads its boost as its description. A command reward's
localizationKey(ordisplayName) replaces the typed boost line as its card's name, which left a named/mmoboost giverow with no word of its multiplier or minutes.RewardPreviewBuilder's boost branch now usesMmoChipReading.boostLineas the description when the row names itself and writes nodescriptionKeyordescription; a row with its own description keeps it, and an unnamed row reads as before. The taming pack's collected level rewards rely on it. Test:RewardPreviewBuilderTest.namedBoostCommandRewardEntry_withNoDescription_readsTheBoostLineAsItsDescription. - Multi-criterion achievements read their own step lines. ZiggfreedCommon 2.4.0's
AchievementReaderasks each criterion's line by its authoredCriteriaid, the keyCommonAchievementFoldfiles it under, where it asked by position; no MMO change. - Admin pages answer every click, and the Abilities page saves a bind when it is set.
AdminSectionPagegainsanswer(), and everyhandleDataEventexit that sent nothing (a missing player, a text-field echo, a missing action, a null dropdown or row value, a default arm, a caught bad number) answers, in all 15 section pages and the admin hub. A refused admin re-check showscmd.no_permissionon the page's status line through a new overridablestatusSelector()(#AddStatusonCommandRewards,MobKillXpandMobDropCommands, whose documents carry no#ActionStatus);WhiteLabelConfigPageno longer reaches its switch with a null action. The General page's skill and command permission switches re-check OP on the event, not only on the binding. The HUD & Abilities page's Bar Panels note says what the HUD bars page sets (ui.admin.hud_bars_notereworded), and a failed open reports its own line (newui.admin.status.hud_bars_failed). Newability/input/AbilitySlotBindingServicebinds, unbinds and re-keys a slot (mutate, saveSKILLS, reconcile key claims), shared by/mmoability's three writes andAbilityBindPage.route(), so the page's binds save when set (the database backend saved them only at logout before); a throwaway component is never saved or reconciled, andAbilityCastItemService.restoreMissingsaves when it gave anything. - Native damage-over-time ticks skip the swing path. Since Update 7 an owned burn, poison or rune tick carries its caster (
Damage.EntityEffectSource), so each tick ran the held weapon's cancels, fold, crit, next-hit buff and mark spends, combo, passives, lifesteal and damage XP.MmoDamageSource.isNativeDotTickclassifies such a tick (never the MMO's ownDotDamageSource);CombatDamageEventSystemlets it deal its native damage untouched, andCombatXpEventSystempays it XP only through atag:row mapped to the effect, never acause:row or the held weapon. Kill credit,DEAL_DAMAGEprogress and a mob tick's Defense reduction are unchanged; a mob tick's Defense XP keys on atag:orcause:row, else the mob. A guard test pins the classifier. - Small fixes.
/mmoconfig validatefolds CommonLib'sLootAudit.auditAll, so reward-kind files and bonus rows are audited too and may report problems a pack already had. The absurd-cooldown and absurd-cost audits (AbilityValidator) read their ceiling from the liveParamClampTable(an owner'sability-clamps.jsonincluded), falling back to the shippedMax, and state the real bound.DamageSchools.resolveSchoolwalksInheritsthrough the engine'sDamageCause.isOrInherits, the nearest school winning.SkillType's Swords icon namesWeapon_Sword_Crude.SchooledDeployableAoeConfigdrops thehandleDetectionoverride the engine no longer needs.skill-config.json's never-readcomboChainIdandcomboChainLengthare gone withSkillConfig's accessors.HitDamage.json's comment andModifierClampsPage's javadoc state the clamp as it runs.presets/is an input of:test. - Name ladders and key probes are CommonLib's.
TargetNameResolverstays the MMO's seam and delegates prettify and the entity and zone name ladders toNativeNames;LocalizationConfig's full-key and client-key probes askLangCatalog; the four canonical copies they replaced go. Output is unchanged, except that a whitespace-only target id names nothing instead of a space, and a probe before the language module is up answers false instead of possibly throwing. - The retired
MMO_Bonusids' alias is pinned.SchoolBonusStatsAliasTestreads everyMMO_Bonus_andMMO_BonusFlat_id from the shipped en-US tooltip labels and asserts each resolves to exactly CommonLib'sDamageBonus_orDamageBonusFlat_(Physical to both Physical and Projectile), that unrelated ids stay unaliased, and that a saved stack's Physical roll reads in full under both. - Dialogue findings come from CommonLib's conversation audit. The MMO claims ZiggfreedCommon 2.4.0's first-join dialogue audit (
DialogueAudit.claimLateAudit) beside the placement one, so the first join logs[dialogue] content findings are reported by MMOSkillTreeand CommonLib reports no lines of its own.MmoDialogueValidatorrunsDialogueAudit.auditConversationswith its switched-off-feature exemption and keeps only the MMO's ownRewardandCompleteQuestchecks and its ids inside aRequiresblock (below);QuestValidatortakes completion findings fromDialogueAudit.completionFindings. In/mmoconfig validate's dialogue domain an unknown quest state is one error (QUEST_STATE_UNKNOWN); unknown quest ids are also found in node conditions,AnyOf/AllOf/Notbranches, Start beat actions and extension lines;UNKNOWN_QUEST_STATEis gone andACTION_MISSING_QUESTcoversCompleteQuestonly. The test that pinned a duplicate check goes. GateandHasActiveBountiesare CommonLib's;RewardandRunCommandare deprecated forGrant. ZiggfreedCommon 2.4.0 registers theRequiresdialogue condition (withGateas its deprecated spelling) andHasActiveBounties, so the MMO registers neither:MmoDialogueConditionsgoes (Removed, below),Mmo_Hub_Intro.json's six gates are written asRequires, andMmoDialogueEnginelatches onCompleteQuest.MmoDialogueValidatorreads a held-shut line through CommonLib'sRequiresDialogueCondition.Block, warnsREWARD_ACTION_DEPRECATEDandRUN_COMMAND_DEPRECATEDon each use on a conversation's own lines andStartbeats (both actions still run; dialogue-extension lines are not walked), and checks only this mod's ids inside aRequiresblock through the newValidationChecks.checkOwnVocabulary, whichcheckGateshares: an unknownmmoskilltree:featureisUNKNOWN_FEATURE(an ERROR), with unknown skills and the mastery-node param's shape. The block's generic audit is CommonLib's, so nothing is reported twice.CONTENT_PACKS.mdteachesGrant,RequiresandHasActiveBountiesand marks the three deprecated. Tests:DialogueValidatorTest.aMistypedFeatureInARequiresBlockIsAnErrorReportedOnce, andoptionGatedOffByFeature_questRefsNotAuditedonRequires.- A hard Gobliterator win earns the standard rung too.
Goblin_Gobliterator_T1'sENCOUNTER_DEFEATEDcriterion matchesGoblin_Gobliterator_EncounterbyPREFIXwhere it matchedEXACT, so it counts both scripts (the standard fight andGoblin_Gobliterator_Encounter_Hard); with noQualifier, a first win on hard earns it andGoblin_Gobliterator_Hardtogether. The criterion key is unchanged.achievement.goblin_gobliterator_t1.descdrops "with Hard Mode off". - A turn-in line reads a character's own sentence.
ObjectiveTextRendererreadsobjective.text.turn_in.return.orobjective.text.turn_in.find.(the id lower-cased; this mod's catalogue first, then any pack's) before the sharedReturn to {0}andGo to {0}, so a character whose name needs its article writes the whole line. New en-US keys formmo_hubandmmo_hub_temple: "Return to the Adventurer's Guide" and "Go to the Adventurer's Guide". - New: the dungeon round kinds.
ObjectiveTypegainsINSTANCE_ROUND_STARTEDandINSTANCE_ROUND_LOST, both quest-producible, their targets named the way the other round kinds' are.CONTENT_PACKS.mdlists the four round kinds with a dungeon run's target (dungeon:) and qualifier (the dungeon id). - New: the gear-set tier kind.
ObjectiveType.GEAR_SET_TIERmirrors ZiggfreedCommon 2.4.0's kind (ObjectiveKinds/Gear_Set_Tier.json, fired by its gear-set producer when a tier comes on):Targetthe set id, noQualifier, the amount the tier's piece count. It is value-based and quest-producible, so the MMO's folds, validator, sentences and name paths keep such a step. New en-US keysobjective.text.gear_set_tier("Wear {0} pieces of {1}") andobjective.text.gear_set_tier.any.ObjectiveKindVocabularyTest's calendar and reputation checks share one helper and check the new kind file too. - New: the encounter feat kind.
ObjectiveTypegainsENCOUNTER_FEAT, ZiggfreedCommon 2.4.0's built-in kind for a boss fight won a particular way (Targetthe encounter script id, as forENCOUNTER_DEFEATED;Qualifierthe name of a feat the binding row authors underProgression.Feats), quest-producible beside the other encounter kinds, so the MMO's fold, validator, target names (TargetNameResolver) and objective icons (MmoObjectiveIcons, the encounter icon) know it; CommonLib produces it.CONTENT_PACKS.md's binding-row and encounter-kind tables nameFeatsand the kind. - The Adventurer's Guide's questlines are main quests. The onboarding quests
Getting_Started,Craft_Starter_Tools,Gather_The_Basics,Into_The_FrayandClimbing_The_Ranksand the Temple questsWhat_The_Stones_Remember,The_Watch_BelowandKeeper_Of_The_DeepwriteListing.MainLinetrue (ZiggfreedCommon 2.4.0), so a quest list shows them first with the star and the gold bar; every other quest (Forgot_Something, the four underOnboarding/SideQuests/,Temple_TributeandThe_Gobliterator) writes none. CONTENT_PACKS.mdfollows CommonLib 2.4.0's boss credit and loot tables. The Participation row and "Who gets credit" nameDamageToAdds, the weight for damage dealt to a fight's adds, and its default of 0; aLootablereward reads as its table by default, withPlainBundle,ListContentsand a roll'sHideInPreview./mmoconfig validatereports dungeons and runs each validator once.ContentAuditregisters ZiggfreedCommon 2.4.0's dungeon domain (DungeonValidator.audit()folded throughValidationChecks.fold, asauditGearSetsdoes), so the startup audit and/mmoconfig validatereport a pack's dungeons beside every other content domain; CommonLib'sZigRequirestrigger-volume audit stays a check CommonLib runs at every boot./mmoconfig validateran every validator twice, once for the chat summary and again for the log: it runs them once and hands the same result tologAll, now public, the one logging pathrunAndLogAlland the boot pass use, so the chat and log lines read as before.labelOfnames exactly the registered domains plus the two that register at runtime (ability_mod,station_bridge):hit_tag,encounter,gear_set,reputation,dungeonandstation_bridgegain labels, andbounty,shopandstation, which no caller passes, leave.ContentAuditLibraryDomainsTestpinsdungeonwith the other library domains./mmomigratenames CommonLib 2.4.0's retired reputation pieces.MigrationClassifier.classifyPackandclassifyOwnerrun their old bodies, then one check: a JSON under a pack's shared or MMO store, or a live owner JSON (not a leftover copy or a_defaultsecho), whoseFactorleaf is exactlyziggfreedcommon:reputation_earned(which now fails closed) getsREPUTATION_EARNED_REPLACEMENTappended to the row of the family that already claims it (a converted row stays converted, any other becomes needs-hands), or a needs-hands row of its own; aFactors/file that only names the retired factor is told to delete itself (REPUTATION_EARNED_NAME_OVERLAY_DETAIL), and aReputations/file with a top-levelGear(inert since CommonLib 2.4.0) getsREPUTATION_GEAR_RETIRED_DETAIL, namingGear.Statand theReputationGainstats that replace it. Report only: boot converts neither, andOwnerApplyleaves an owner file naming the factor as it is. New migration-corpus cases (old-jack-reputation-earned,retired_reputation_earned_quest,owner_mastery_reputation_earned,reputation_earned_name_overlay,retired_reputation_gear, andnegative_reputation_gain, which is not a finding); tests inMigrationClassifierTestandOwnerApplyTest.- One writer for
_reference.config/ReferenceFilesis the one writer ofmods/mmoskilltree/_reference/and writes a reference only when its bytes change, so a boot that changes nothing touches no file.ReferenceFiles.removeRetireddeletes seven retired references by exact name once at setup (thedefaults-files for.json luck-loot,abilities,elite-mobs,currencies,mastery,classesandmob-drop-commands), leaving any other file there alone.MasteryConfig,ClassesConfigandMobDropCommandsConfigwrite no reference, since theirs held no defaults, andRewardIconsDefaults.byCombatTargetis insertion-ordered so its reference stays put.ui.help.mastery.howandui.help.mastery.examplesdescribe per-field overrides as they work. Test:ReferenceFilesTest. - Tests. For the Headless Horseman's content in Seasons of Orbis,
HallowsEveRetrofitPinTestlosesnoIdIsAddedOrDropped(withDROPPED_ON_PURPOSE),theIdsLeftOutAreExactlyTheLayersLibraryStoreTwinsandnoOtherIdCarriesTheMmosRows(withLATER_CONTRACTS), andSeasonsOfOrbisMmoRowsTestlosesthisModIsNamedOnlyInTheRewardRowsOfQuestsContractsAndAchievements(withHARVEST_FEAST_SKILL_JOBS,dropRowsandwithoutComments); the classes' other checks stay.HallowsEveRetrofitPinTestallows Old Jack'sopt.boardleaving his menu and the two reputations dropping theirGearleaf, only where the current file has none; the frozen tree is untouched.HarvestFeastPackContentTest.theConversationOpensNoClosedDoorOffSeasonkeeps its two live checks and drops the door count.HallowsEveRetrofitPinTestalso drops the one line pinning Old Jack'sStart.Questsrows, which The Rider's row grows on purpose (the loop over every frozen quest's offer, brief and hand-in still runs); the frozen-lock comparison and itsLOCK_DROPPED_ON_PURPOSEset, which The Hollow Lantern's newRequiresblocks; the pin thatopt.shopis written once, since Old Jack's sweets answer reuses it (opt.standingandopt.almanackeep theirs); andaChildOfABaseThatWritesItsOwnRequiresStillHidesOffSeason, which withSeasonsOfOrbisFactionTest.everyReputationAndWalletThePackPaysOrChargesIsOneItShips(also deleted) blocked Old Jack's Life Essence offers, a companion pack's wallet behind a mod gate the unit JVM reads as absent. Since each season's keepsake became its last quest's own pay,HallowsEveRetrofitPinTesttakesachievement:hallows_eve_lantern_keepsakeout of its collected-on-purpose list andHarvestFeastPackContentTestdrops its check that the keepsake achievement's stack carries@year.DialogueContentResolutionTestregistersDungeonDestinations, since the Gravekeeper's conversation opens a dungeon.SeasonsOfOrbisMmoRowsTestderives which Seasons of Orbis files must pay MMO rows from the pack itself, in place of its hand-kept list: every quest, contract or achievement with pay beyond a title carries a gated MMO row, and every dungeon Claim and boss daily Claim a gatedMmo_contribution.LethalHitAttributionTest's expiry case reads the clock afterrecordstamps its entry, so a slow build can no longer leave the entry live past the spin. - One ready-time gear recompute.
PlayerReadyFlowno longer callsrecomputeAllitself: ZiggfreedCommon 2.4.0 recomputes every player through the equip-stat bridge at a LATEPlayerReadyEvent(GearSets.onPlayerReady), so the MMO's pass only repeated it. The plugin still takes the installed bridge for its key segments and listeners. - A Skill Lamp and a parked reward land like a pickup.
SkillLampService.give(behind/mmolamp give, so theMmo_Lampreward kind, and a rub's hand-back) hands the lamp over through ZiggfreedCommon 2.4.0'sLootRewardKinds.handToPlayer: where the player's pickup settings route it, with the game's own pickup notice and sound, else at their feet through the overflow policy. It answers the newSkillLampService.Handed(BAG,FEET,LOST, read offGroundSpillSinks.pickupFitsbefore the hand-over) in place ofInventoryGrant.Landed, and/mmolamp givesays when a lamp could not be placed at all. ThePendingRewardsspool now drains inPlayerReadyFlow's NORMAL pass, after the hydrate, instead of atPlayerConnectEvent, where no item could reach a bag:RewardExecutor.deliverPendingRewards(PlayerRef)replays each line throughLootRewardKinds.replayParked, so a parked item reward lands the same way (and parks again on a live grant's terms) while every other line still runs as the console. The green "You received {0} quest reward(s)!" summary after a drain is gone, since the game's own pickup notices say what arrived, and its keynotify.quest_rewards_deliveredleaves every locale.
Removed
SkillComponent.setInventoryCastingMode(boolean)andisInventoryCastingMode(), deprecated since 1.0.1 (a no-op and an always-false). The inventory casting mode no longer exists.IntegrationRegistry's typed gettersgetFishingIntegration,getParriesIntegration,getCitizensIntegration,getEnchantingIntegrationandgetActiveIntegration, which nothing called.MmoBounties.difficultyColor, both forms, withCOLOR_UNGRADEDandboardOrNull. CommonLib paints a contract's grade from its board's colours.MmoDialogueConditions(GateCondition,HasActiveBountiesCondition) andMmoBounties.hasActiveBounties. CommonLib 2.4.0'sRequiresDialogueConditionandHasActiveBountiesDialogueConditionanswer both.- About 260 lang keys nothing read, from all nine locales: the legacy
ability.summarytemplates (155), the old bind page's lines,ability.condition, the pre-gate status lines,reward.cape, the old XP panel, the retired leaderboard and settings lines, page, boost, time and tip leftovers,achievement.hiddenand its progress line, the shadow NPC names,notify.xp_gain_bonus,notify.quest_rewards_deliveredandnpcs.lang'sMmo_QuestGiver.hint. - About 1,600 lines nothing called: eleven
QuestServicemembers,pages/ConfirmGate,pages/render/PlayerNameText,CommandRewardService's run-every and inventory helpers, the join-timeStatisticsComponentzero-init,SchoolResistReconciler.sweepRetired(the engine already drops an effect whose asset is gone),NativeChainAudit.decide,license/LicenseGate,SkillDisplayUtil's chat-era formatters,CostChipRenderer's dead renderers and colours,SkillTreeService's unused getters, a dozen small members and the boot sweep of*_backup_backup*files. The tests that pinned only them go (ConfirmGateTest,PlayerNameTextTest,NativeChainAuditTest's cases).
v1.7.1 developer notes
The Update 7 port of 1.7.0, built against the 0.7.0 server jar (patchline=pre-release until Oct 12), ZiggfreedCommon 2.3.0 and the RPG Stations 1.1.1 api (contract 10). Behaviour changes only where Update 7 moved the ground under the mod.
Breaking changes
- Requires Hytale Update 7 and ZiggfreedCommon 2.3.0.
ServerVersionreads>=0.7.0-pre.0 <0.8.0.ziggfreedCommonVersionandMMOSkillTreePlugin.REQUIRED_ZIGGFREED_COMMONread2.3.0, so the manifest's^2.3.0refuses the 0.6.8-built 2.2.0 by name.rpgStationsApiVersionreads1.1.1; the optionalZiggfreed:RpgStationsrange rises to>=1.1.1 <2.0.0.MMOSkillTreeAPI's signatures are unchanged;getStatisticsreturns a read-only copy of CommonLib's stat record (Technical, below). - The school damage bonus stats are CommonLib's. The 22
MMO_Bonus_andMMO_BonusFlat_stat assets are removed. A pack item's nativeStatModifiersnaming one counts for nothing, since the engine skips a stat it does not know, so it namesDamageBonus_andDamageBonusFlat_instead, a Physical bonus on both_Physicaland_Projectile. Rolls an item saved through enhancement read under the new ids by themselves (Technical, below). - Ability inputs move to the free keys.
MmoSlot.defaultInputs()is slot 1 onP(middle mouse,Pick), slot 2 onPC, slot 3 onA4(H,Ability4).TriggerKey.freeInputs(), so the picker and stance mode's off-move fallback, run middle mouse, crouch+middle mouse, H, crouch+H. Every save moves there once at its next ready, whatever it held (AbilityInputMigration, run inPlayerReadyFlow's player-data pass behind the newSkillComponentkeyAbilityInputLayout, and saved at once withmarkDirtyAndFlush, since the database backend re-hydrates on every world entry). A one-time chat line goes only to a player with an ability in a slot.TriggerKeygainsABILITY_4,ABILITY_4_CROUCH,PICKandPICK_CROUCH(codecA4,A4C,P,PC), which a 1.7.0 jar parses as no input.
Technical
- The Temple's Due's pay table draws its pool (ZiggfreedCommon 2.3.0).
temple_tributepaysMmo_Temple_Offeringthrough aLootablereward, which on ZiggfreedCommon 2.2.0 read only a table'sRolls. ZiggfreedCommon 2.3.0'sLootablereward resolves the whole table, so the offering'sPool(one Eternal seed bag) draws on every claim, and its 1% relic roll, aLootablereward naming the pool-onlyMmo_Temple_Relicthat threw before, pays one Ancient Steel piece. No MMO code or content changed. - The hookshot pull and NPC impulses. 0.7.0's
Role.processSetVelocityInstructionapplies aSettimes the role'sKnockbackScaleand skips the drag discount (preScaleKnockback), where 0.6.8 multiplied X and Z by about 0.044.MmoHookshotPullInteractiondropsNPC_DRAG_CANCEL_SCALE(25) and its NPC branch: every target takes the resolved speed throughSizing.pullVelocity(...).VelocityImpulseUtilnever scaled by caster kind, so its maths stays; it moved into the pureimpulseVelocity(...), and an NPC caster's dash or dodge covers its full authored distance on 0.7.0 (about four percent of it on 0.6.8). Tests:MmoHookshotPullSizingTest(the NPC-only pair replaced by the 0.7.0 contract on the same targets),VelocityImpulseUtilTest. - The ogre's XP tier moves to
Goblin_Glutton, and the Feastmaster boss gets rows of its own.MobKillXpDefaultsandDefenseDefaultskeyGoblin_Glutton(700 HP, its own model;Coffer_Goblin_GluttonandDungeon_Goblin_Gluttonmatch it by longest substring) where they keyedGoblin_Ogre, which Update 7 deletes. The final 0.7.0 split the pre-release'sGoblin_Feastmasterinto that overworld elite and the Deepworks boss, whose two phase rolesDungeon_Feastmaster_BossandDungeon_Feastmaster_Feedingtake 2,500 kill XP and 30 Defense per hit taken, the top boss rate; they are keyed in full, so theDungeon_Feastmaster_Scrapperadds never take a boss's row.Goblin_Guardian(90 HP, the goblin model at 1.1 scale) stays on the plainGoblinrow. An owner override onGoblin_Ogrematches nothing now. No test:HeavyGoblinXpKeysTest, which pinned the removed role, is deleted. - The Gobliterator gets boss rows of its own.
MobKillXpDefaultskeysGoblin_Gobliteratorat 5,000 andGoblin_Gobliterator_Phase3_HMat 15,000, andDefenseDefaultsthe same two at 30 per hit taken, the top boss rate (a Defense row pays on each hit a player takes from that role, so its number stays per hit while the kill XP carries the scale).Goblin_Gobliteratortakes every role of the fight, the_HMtwins included, and the longerGoblin_Gobliterator_Phase3_HMwins over it by longest substring, so a hard win pays 15,000 and never also 5,000. The phases swap role in place (ChangeTargetRole, invulnerable through each swap) and onlyGoblin_Gobliterator_Phase3or_Phase3_HMcan die, so a win pays once; the barrels, the legs andGoblin_Pilotare invulnerable, andGoblin_Pilotmisses the row. No test. - The shipped presets' ogre steps.
presets/balanced_rpg.mmop.json,hardcore_survival.mmop.jsonandadventure_explorer.mmop.jsoneach had twoKILL_ENTITYsteps (kill_ogres,trophy_goblin) onGoblin_Ogre, which no 0.7.0 role matches; they areENCOUNTER_DEFEATEDsteps onDungeon_Feastmaster_Encounter, the Deepworks boss's fight, with their ids (saved progress) kept and their amounts cut for a boss:kill_ogresfrom 5 to 1 in Adventure Explorer and Balanced RPG and from 10 to 3 in Hardcore Survival,trophy_goblin1, and 2 in Hardcore Survival. Their rawdisplayTextreads "Defeat the Feastmaster", with the count where it is above one (the import converter drops it). A server that applied one of these presets keeps the old target in its own quest files. Test:ShippedPresetKillTargetsTest(no shipped preset's kill step targets a role Update 7 removed; the removed set is the 0.6.8-to-0.7.0 role-file diff, 23 ids, frozen in the test). - New: boss ladders for Update 7's fights. Three ladders in Combat > Bosses, each at 1, 10 and 50.
Goblin_Feastmaster_T1toT3countENCOUNTER_DEFEATEDonDungeon_Feastmaster_Encounterthrough both phases, under a criterion renamed from the pre-release's elite kill so no such kill carries.Voidtaken_Grizzly_T1toT3countBear_Voidtaken(EXACT).Blight_Warden_T1toT3count Kweebec Nightmare'sKweebecNightmare_Warden_Encounter, hidden without it (hytale:mod_installed). Every rung pays Defense XP to collect. Each 50 rung grants a title on unlock and announces:Mmo_Goblin_Feastmaster(The Feastbreaker),Mmo_Voidtaken_Grizzly(The Voidhunter) andMmo_Blight_Warden(The Wardenbreaker). No test. - New: the Gobliterator's achievements. Two files in Combat > Bosses.
Goblin_Gobliterator_T1counts oneENCOUNTER_DEFEATEDonGoblin_Gobliterator_Encounter(EXACT; the script id, so it holds through the machine's phases), tier 1 of the chainkills_goblin_gobliterator, 20 points and 1,000 Defense XP to collect.Goblin_Gobliterator_Hardcounts one onGoblin_Gobliterator_Encounter_HardwithQualifierhard, which CommonLib 2.3.0's binding row for the hard script writes, 60 points and 5,000 Defense XP. The two scripts are separate, so a hard win pays the hard one only. Every player the fight credits earns them. No title. New en-US keysachievement.goblin_gobliterator_t1.*andachievement.goblin_gobliterator_hard.*(title and description), in all nine languages. No test. - New: the Gobliterator quest.
Quests/MMOSkillTree/Temple/The_Gobliterator.json, given and taken atMmo_Hub_Temple(TurnInIdgiver) behindmeet_at_the_temple: oneENCOUNTER_DEFEATEDstep onGoblin_Gobliterator_Encounter(CONTAINS, so the_Hardscript counts too), credited through CommonLib 2.3.0's binding rows for both scripts, and aClaimMmo_Lampof 5,000 overCOMBAT, pick 1. The step words its own line (TextKeyobjective.text.quest.defeat_gobliterator), since CommonLib's sharedobjective.encounter_defeatedsentence names the boss by its boss-bar name,Gobliterator MkIIII. The fight stays in the Deepworks; the temple only gives the errand and takes the report. New en-US keysquest.the_gobliterator.title,.flavorandobjective.text.quest.defeat_gobliterator. Test:ShippedQuestContractTest.SHIPPED_IDSgainsthe_gobliterator. - Damage types are Hytale's, plus CommonLib's three. Every hit keys an Update 7 type or one of ZiggfreedCommon 2.3.0's
Arcane,VoidandLife(moved from this jar to CommonLib'szc-core,InheritsElemental, each with itsDamageTextColorandDeathMessageKey). The jar'sServer/Entity/Damage/Lightning.json,Water.jsonandPoison.json, which replaced Update 7's own, are deleted, so those types keep the game's colours and death lines. - The 13 per-ability types retire:
SchoolCauseGenerator, itsMMO_Cause_leaves,LeafCauseDeathLines, theability.keys and.death Tags.MmoCauseAncestorare gone. Each ability'sCauseorBaseDamagekey names its school's type, so a kill reads that type's death line:Arcane,VoidandLifecarry their own,Poisonkeeps the game's,Bludgeoning(Ground Slam, Bull Rush, Sunder) keeps its own, and Update 7's other types takeElemental's orPhysical's throughParent(the only field that inherits that way). Vortex Pull (was Arcane) and Wind Dancer (was Lightning) dealWind. - Retired ids: every
MMO_Cause_, Seasons of Orbis'sHallows_Eve_Lantern_Burstand Kweebec Nightmare's fourKweebecNightmare_*types. An owner'sxp-maps.jsoncause:key naming one matches nothing (the shipped Artillery keys move totag:), and a pack's native damage node, projectile or resistance keying one fails to load, as any unknown damage type id does (an MMO damage step'sCausenaming one falls back to Physical, andAbilityValidatorflags it). The audit findings that watched the generated leaves,NATIVE_DAMAGE_CAUSE_UNBRIDGEDandSCHOOL_RESIST_LEAF_HOLE, retire with them. -
DamageSchools.SCHOOL_IDSreads Physical, Fire, Ice, Lightning, Water, Wind, Earth, Poison, Arcane, Void, Life, withCrushamong the Physical sub-types. Wind and Earth shipMMO_SchoolResist_stat files (their damage bonuses are CommonLib'sDamageBonus_Windand_Earth, below), and item enhancement's roll pool follows the roster. -
DamageSchools.displayColorreadsDamageCause.resolveDamageTextColorthrough the type's chain;FALLBACK_COLORis only for a type that is not loaded. - Hit tags: a damage node can carry native
Tags, read back through CommonLib'sdamage/HitTags. An XP map'stag:key (CombatWeaponUtil.TAG_KEY_PREFIX) is checked beforecause:keys, so a thrown bomb that deals Fire still pays Artillery (Hallows_Eve.json'stag:Hallows_Eve_Lantern_Bomb); an entity effect's own nativeTagscount on each tick of its damage over time too (CommonLib 2.3.0'sHitTags, zc 7758a2d27), so a bomb's burn pays the same row, and the native-chain abilities' damage nodes carry anMmoAbilitytag (AbilityCatalog.ABILITY_HIT_TAG, read byabilityIdForHit, which replacesabilityIdForCause) that ties the hit back to its ability for XP, per-ability damage channels and conditional damage, which retiring the per-ability types had given up.HitTagValidator(domainhit_tag) reports in/mmoconfig validatean xp-maptag:key no loaded interaction declares (UNKNOWN_HIT_TAG, info, so a server without Kweebec Nightmare or Seasons of Orbis sees no warning for the shipped Artillery rows) and anMmoAbilitytag naming no loaded ability (UNKNOWN_ABILITY_TAG, a warning); never an error, since the mod dealing the hit may not be installed. - Tests:
ShippedDamageTypeIdsTest(every type a shipped asset keys on is an Update 7 type or one of CommonLib's three, since an unknown id drops the mod at boot),DamageSchoolsRosterTest. The generator's and the leaf lines' tests go with them, as doAbilityCatalogTest,DamageCauseDeathLineTestand anItemEnhanceRollTestmethod;NativeDamageCauseResourceCrossCheckTest,CombatWeaponUtilCauseRoutingTestandAuthoredStatTypeDecodeTestshrink to the types and keys that remain. - Resistances, school bonuses and auras on native mechanisms.
- One resist path: a
SCHOOL_RESISTtree reward writesMMO_SchoolResist_through the tree's stat substrate, as gear and mastery do, andSchoolResistReconcileralone mirrors that stat into native resistance effects, through CommonLib's newRuntimeEffectGrid.ResistEffectRegistrarand its per-nodeMMO_Resist_*effects retire; any left on a player are swept at ready. - School damage bonuses are CommonLib's
DamageBonus_andDamageBonusFlat_. Mastery's school nodes and/mmoitemenhancewrite them, a Physical bonus landing onDamageBonus_Physicaland_Projectilealike so it still covers arrows. The 22MMO_Bonus_andMMO_BonusFlat_stat assets are removed, with no grace version: CommonLib'sStatAliasesmaps each to its successors, so an item enhanced before reads its bonus throughStackStatsunder the new ids and stores them on its next stamp, and theiritemTooltip.stats.*lines stay inclient.langfor the tooltips those items saved. A third party's nativeStatModifiersnaming an old id stop counting; anMmoChangeStatinteraction naming one without a duration changes the successors (a timed one is not aliased: its effect still names the old id).SchoolBonusStatsreplacesSchoolBonusSourcesand gates CommonLib's per-type step with the world's combat-bonus rule (TypedDamageBonuses.installAttackerGate). The hit tooltip, line colours and the stat-channel audit read the new ids. Tests that pinned the old ids or a weapon roll's one-to-three entry count go.CombatDamageEventSystemorders itself before CommonLib'sTypedDamageBonusSystem, so a school bonus now multiplies with the global and per-skill bonus after armor instead of adding into theHitDamage-clamped sum, with no cap. A damage-over-time or zone tick gets the school bonus from item enhancements too (HitDamageBonus.schoolTermsFor). - Buff auras:
MmoBuffAuraapplies a native timedOutgoingDamageeffect (AuraOutgoingEffects, idsMMO_AuraOutgoing_*) that raises every hit for the aura's duration, each aura's bonus retuned to match.ability.summary.mmobuffaura.valuegives way toability.summary.mmobuffaura.every_hit. - Clamps as safety rails: every other shipped
ModifierClampsdefaultMaxandMaxTotalPercentdoubles (everyMinandMinTotalPercentstays),DefenseReductionrises from 0.58 to 0.8 andSchoolResistfrom 0.3 to 0.8, andCritChanceis retired (a chance past 1.0 already crits every hit). An owner'sability-clamps.jsonkeeps applying; an override ofCritChancenow does nothing.StatCeilings.usablecounts defense picks for Power Level's StatRewards pillar up to the new 0.8, so a defense build past 58% now scores a higher Power Level, which mob scaling reads.ui.clamps.name.critchanceandui.clamps.about.critchanceare gone, andui.clamps.about.defensereductionand.schoolresistcall those caps safety rails, not balance settings.AbilityValidator's absurd-cooldown and absurd-cost warnings sit at theCooldownMsandCostclamps' ownMax(20 minutes, 200000). - Tests:
CritChanceClampAssetTest,ResistEffectReconcileTestandSchoolResistParseTestgo with what they tested;StatCeilingsTestandSchoolResistReconcilerTestdrop the cases the old mechanisms needed, andBodyMigrationFidelityTestdropsdxPassBody_isSemanticallyEquivalentToPreDxPassDigestandeveryNonExcludedLeafValueSurvivesIntoTheEmittedBody. - A per-hit XP row matches whatever its case. A
tag:orcause:row spelt differently from the hit only in case was routed but paid nothing (acause:row already did in 1.7.0) and pays now: the hit pays through the matched row's own key (CombatWeaponUtil.routingKey, the row the gate found), sinceSkillService.bestTriggerMatchmatches case-sensitively.NativeChainAudit.probeand itsProberecord, with no caller left, are gone. - The quest audit stays quiet about what the mod gate refused.
ValidationChecksgainsachievementKnownOrModGateRefusedandnpcKnownOrModGateRefusedbeside the quest one, andQuestValidatorreads its achievement requirements, its four giver checks and the probe it hands CommonLib's quest audit through them, so a quest naming an achievement or a character whose file the mod gate refused reports nothing, while an id nobody authored still warns. CodecDocumentationMarkdownTestkeeps its guard. 0.7.0'sDocumentation.stripMarkdownfollows CommonMark and never throws, which left the test's four fixtures failing and its source scans passing vacuously. The test carries its own copy of the 0.6.8 stripper's balance rule (emphasisUnbalanced, checked against that stripper on every fixture and two million random strings), plus two fixtures for runs longer than one.- Who owns E and R against Update 7's runes.
AbilityInputOverrides.TRIGGER_ROOTSgainsAbility4toMMO_Trigger_Ability4andPicktoMMO_Trigger_Pick(each a crouchConditionwithNativeInput {Defer, Fallback}). - Claims come from the pure
KeyOwnership.claims(...): the free keys a bound slot uses always (Picknever in Creative, re-run onChangeGameModeEventbyAbilityInputGameModeSystem), andAbility1-3only in stance mode's MMO stance, so a slotted rune and the weapon signature are natively predicted everywhere else. -
AbilityInteractionInjectorwrites onlyAbility1into item maps. 0.7.0'sInteractionContext.forInteractionsendsAbility2/Ability3with a main-hand item to the abilities section, so an item's own row for them runs only with an empty main hand and an offhand item. Without the MMO's rows that case is the game's, and a slot on E or R reaches its player through the claim. -
MmoCastAbilityresolves the displaced move throughDisplacedRoot: the slotted rune'sCastroot on the abilities section, the default unarmed map for an empty hand, else the item's own map.Defernever yields to a rune (its cast opens withUseAbilityCostand no cost map, whichNativeInputGatereads as not ready). ATriggeronABILITY_1-3casts nothing outside the MMO stance, the injectedAbility1row and pack roots included. - Tests:
TriggerKeyFormsTest,TriggerRootsTest,AbilityInteractionInjectorTest,KeyOwnershipTest,DisplacedRootTest,AbilityInputMigrationTest,InputChoiceTest. - New: stance mode. Two new
SkillComponentkeys,AbilityStanceModeandAbilityRuneStance, read through the one seamAbilityStance.of(skills). - Plain H with stance mode on flips the stance (
AbilityStanceService.flip, after theDefercheck, so a crossbow still reloads), and stance mode always claimsAbility4for it. - Turning stance mode on or off saves at once (
markDirtyAndFlush). A flip only marks the save (markDirty): the database backend writes it at its next sweep or at logout. - Turning stance mode on moves a slot on plain H to the first of E, R or Q that no slot uses and starts in the MMO stance. Turning it off moves every slot on a stance key to its free-key default, or to the first free key no slot uses when another slot holds that default (
StanceMoves). - The cooldown HUD's fourth pip is the stance badge (
StanceBadge). - Tests:
StanceMovesTest,AbilityStanceTest,StanceSurfacesTest. - New: XP boosts on CommonLib's buff bar.
BoostBootstrapregistersBoostBuffSourcewith CommonLib'sTimedBuffs: every live timed boostXpBoostService.getActiveBoostslists (the list the Boosts page reads), keyedmmoskilltree:boost:, a server-wide boost on theSERVERpool and a personal one onPERSONAL, ending at its start plus its duration.XpBoostServicecallsTimedBuffs.refreshAllOnline()after each activation (template, admin, token) and both clears; CommonLib re-syncs on its own at every ready and respawn, and a boost that runs out takes its tile with it. StandingBoostRulesstay off the bar. - A boost token can name its own tile.
Mmo_Boost_Tokendeclares an optionalEffectparam, and its command line readsmmoboost give --args={player}|{Skill}|{Multiplier}|{DurationMinutes}|personal|{Effect};/mmoboost givetakes the effect id as a sixth value (pipe form only, since ids carry underscores), read throughBoostGiveLine.effect, soBoostGiveLine.BoostGivegainseffect.BoostTokenandActiveBoostcarry it (saved aseffectonly when set, so a 1.7.0 jar reads either), andBoostBuffSourceshows it, or the scope's default tile when the id is not loaded (named once in the log). Boost templates and XP token items keep the default tile. ItemLineagemoves into CommonLib.item/ItemLineageis gone;ItemEnhanceCommand,ActionRequirementServiceandCombatWeaponUtilimport ZiggfreedCommon 2.3.0'sinventory/ItemLineage(the same walk, moved unchanged), and theActionRequirementsDefaultsandItemEnhanceRolljavadocs name it.ItemLineageTestkeeps the classifier cases (action requirements, the enhance roll's gathering skill) over the library's lineage; the walk's own cases moved with the class.- New: the content audit folds five more CommonLib audits.
ContentAudit(/mmoconfig validateand the boot audit) adds calendar events and their spawns (calendar), titles (title), Almanac pages (almanac), derived factors (factor) and NPC identity overlays (identity), each through its own fold beside the gear-set and reputation ones. The last two audits existed in CommonLib and nothing called them. A factor term is asked ofGateFactors.registry(), the vocabulary every requirement here resolves through, so only a term no mod answers reads asUNKNOWN_FACTOR. Test:ContentAuditLibraryDomainsTest. - Statistics fold into CommonLib's stat store.
StatisticsStateMigrationruns first inPlayerReadyFlow's player-data pass, right after the hydrate, and foldsStatisticsComponentintoPlayerStatsComponent(ZiggfreedCommon:Stats) once through itsimportOnce, claimedmmoskilltree:statistics@1: every key keeps its spelling and adds to what the store holds, swapped and claimed in one step.StatisticsComponentstays registered, attached and decodable and is never written again; databaseSTATISTICSrows are untouched. -
StatisticsService.handleEventcounts only this mod's own fires, throughPlayerStats.add:SPECSlost kill, break, place, craft, pickup, talk, encounter, death and the three station kinds, which CommonLib counts. Damage taken and the sprint and swim distances write throughadd;accumulatePlaytimeis gone (CommonLib counts playtime and deaths);MmoMomentReactionscounts nothing, and its encounter, talk and station reactions are gone. - Readers take
StatisticsComponent.viewOf(StatsView), a detached copy under the same keys:MMOSkillTreeAPI.getStatisticsreturns that copy (null without a record), so a write to it changes nothing. - 21 stat files ship under
Server/ZiggfreedCommon/Stats(theSkill_LevelandSkill_Xpgauges, this mod's own sums, and fiveStation_*stats CommonLib counts off RPG Stations' moments,_By_StationasTargetAtQualifier), plus theMmo_Skillssection inStatSections. Their names aremmoskilltree.stats.*(new, in all nine languages) or the Statistics book'sui.achievements.stats.*. - The skill leaderboard retires into Records.
LeaderboardDataStore,pages/hub/LeaderboardPageandPages/LeaderboardPage.uiare gone.SkillGaugespublishesSkill_LevelandSkill_XpthroughPlayerStats.gauge(a line per skill, and the totals): at ready, on an XP award (10 s throttle, at once on a level-up), on an admin XP edit and at disconnect.LeaderboardCacheImportpublishesleaderboard-cache.jsonatstart(), after the sections load, once:StatBoard.importOnceclaimsmmoskilltree:leaderboard-cache@1in the board's ownimports, saved with the rows, so a restart never imports again and an offline player's later level is never set back. The file stays. -
Mmo_Leaderboard(still behind the owner's switch),/mmoleaderboardand the class hub tile openRecordsDestinations.RECORDS; a refused open answers the click, and the command says nothing is ranked yet.MmoMenudrops its Leaderboard tab (mmo.leaderboard): CommonLib's own Records tab is on the rail, so the owner's switch (isLeaderboardEnabled) now gates onlyMmo_Leaderboard. -
Pages/LeaderboardRow.uiis gone with the page that drew it. -
pages/hub/SkillRecordsSplitsplits the Skills tab as the retired page did, through CommonLib'sStatSectionConfig.splitonMmo_Skills, registered insetup()so the linesLeaderboardCacheImportpublishes atstart()rank: the first choice is the tab's own columns (ui.leaderboard.filter_total), thenGATHERING,COMBAT,CRAFTINGandMISC(the oldfilter_*buttons), each the listable skills of its category (SkillFilterUtil.listableSkillIds, read as Records opens), named byMessages.skillNameMsg. Its columns areSkill_Level(ui.leaderboard.header_level, a category counting only levels past 1, asgetSummedTotalLeveldoes) andSkill_Xp(header_xp), so a tie on level goes to XP; the picker's first entry isability.bind_page.skill_filter_all. A viewer sees the skillsPermissionUtil.canGainSkillXpByNameallows (its newPlayerRefform), and a category with none of them is hidden. Both gauges are all-time only, so a category or a skill reads All Time with the period row hidden. No new lang keys. -
MmoFactorContributionssets noziggfreedcommon:countersource: CommonLib's store answers it./mmobulksetxp *queues offline players fromPlayerDirectory.knownIds(). - The Settings tab's Online status card is gone;
ShowOnlineStatusstays in the save, read by nothing. - Tests:
StatisticsStateMigrationTest.LeaderboardDataStoreTestandStationStatisticsTestare deleted, with the cases that named the retired page or card (RailPagesDryTest,PlayerNameTextTest,MmoSettingsTest). - A per-skill stencil reserves 16 sort positions a skill.
CommonAchievementFold.SKILL_SORT_STRIDEgoes from 6 to 16, so the Skill Mastery Pack's thirteen-rung Mastery Point ladder (2.1.1) reads skill by skill instead of running into the next skill's rungs. The levelling ladder's six rungs keep their order, and the server-first races atSortOrder10000 and up still sort last. - Three dead feedback moments are gone.
FeedbackEventdropsBOUNTY_COMPLETE,BOUNTY_REROLLandSHOP_PURCHASE, andFeedbackConfig.loadDefaultstheir rows: nothing in the plugin fired them once the boards and shops moved to CommonLib. An owner'sfeedback.jsonthat still names one loads as before (readOverridesskips a keyFeedbackEvent.fromStringdoes not know), andSCHEMA_VERSIONstays 1. - Achievements keep a criterion's
TargetsandQualifiers.CommonAchievementFoldrebuilds every shared-schema criterion as anObjectiveSpec, andAchievementDefinition.toRuntimehands it back to the engine; both dropped CommonLib 2.3.0's list leaves, so a criterion naming several targets or qualifiers counted any.ObjectiveSpecgainstargetsbesidetargetandqualifiersbesidequalifier(a constructor that omits a list keeps it empty), read byparseandCODEC, written bySharedSchemaJson.objectiveLeaves, carried byfromRuntime, and copied as authored byLegacyQuestConverterfor a kind this mod does not produce;toRuntimerewrites each target through a kind alias as it rewritesTarget.ObjectiveTextRendererandMmoObjectiveIconsname the first target whenTargetis blank (namedTarget()). The owner-file parser (AchievementConfig.parseAchievementandparseCriterion) reads atargetslist as it readsqualifiers, carried by the newAchievementDefinition.Builder.targets, so a legacy criterion naming onlytargetsno longer reads a blank target and counts every kill. Tests:CommonAchievementFoldTest.severalTargetsSurviveTheFoldTheRuntimeRebuildAndTheWriter,SharedLayoutExportTest.aLegacyTargetsListReachesItsCriterionAndTheExport. - Big Game Hunter and Apex Predator name their three bears. Their
kill-entity-bearcriterion tradesTargetBearunderCONTAINS, which also took Update 7'sBear_Voidtaken_TentacleandBear_Voidtaken_Tentacle_Marker, forTargetsBear_Grizzly,Bear_PolarandBear_VoidtakenunderEXACT. The criterion key is unchanged, so saved progress carries over. With noTarget, the definition's summarygetTarget()reads blank: only@skillreads it (TemplateArgs.expandMsg), and both files bind@amountalone. Tests: none new. - New: the Skill Lamp. One item,
Mmo_SkillLamp(the desert lantern's model, texture and icon), whose numbers ride the stack's metadata (lamp/SkillLampMeta, keyMmoSkillLamp):Amount(XP to each pick),Picks(1 to 10) andSkills(lamp/SkillLampPool:ALL, a category, or a comma list of skill ids, a skill id read before a category asMmo_Xpreads it). The pool is written in one canonical spelling, so equal lamps stack and different ones never do, and a minted lamp'sItemDisplayMetadatadescription names its picks, amount and pool (lamp.item.description,lamp.item.description_from). - The
Mmo_Lampreward kind is a jar-shipped kind file runningmmolamp give {player} {Amount} --picks={Picks} --skills={Skills}(Picksdefaults to 1,SkillstoALL), soRewardSpecs.MMO_KINDSholds four kinds and the mod still registers no Java kind.RewardSpecs.refusalReasonrefuses anAmountunder 1, aPicksoutside 1 to 10, or a list with a space, an empty or malformed entry, a repeat, or fewer skills than picks;ValidationChecks.checkRewardsreports the same for shared-schema quests, contracts and achievements (MALFORMED_LAMP_REWARD, an error) and warns on a listed id the server has no skill for (UNKNOWN_LAMP_SKILL). Its chip reads through the kind file'sreward.lamp.{Skills}key for the every-skill lamp, and throughMmoChipReading's newMmo_Lamparm (reward.lamp.from, naming the category or the skills) for any other pool. The kind file'sHands(Mmo_SkillLamp, CommonLib 2.3.0) puts one lamp in the ask-first room batch (LootRewardKinds.canAddAll), so a lampClaimwaits for bag room like any item reward. -
/mmolamp give(admin or console) hands a lamp over through[--picks=N] [--skills=...] SkillLampService.give: CommonLib'sInventoryGrant(hotbar, then bag), elseNativeLootService.spawnAtFeet. An offline player who has connected before gets the same line queued inPendingRewards. - Rubbing is
mmo_skill_lamp_rub(SkillLampRubInteraction) on the item's Secondary, a one-second charge that opensSkillLampPage($ZF.@ZigDecoratedFrame, cards fromSkillResetCard.ui, CommonLib'sConfirmArmfor the two-click confirm). A card is unavailable when its skill is at the player's level cap, gives no XP in this world or is held back by the player's class (SkillLampService.blockOf); the player picks the lamp'sPicksdistinct skills, or every available one when fewer are.SkillLampService.rubchecks the choice again, takes one lamp with the same numbers from the bag, then pays each pick throughSkillService.addXpByName, so boosts, class and world rules and level-ups apply. An award that throws stops the payout and is logged, andhandBackreturns the rubbed lamp when nothing was paid, or a lamp of the same amount and pool for the unpaid picks (a pick whose XP moved before the throw counts as paid, so nothing pays twice). Closing the page takes nothing. - Test:
SkillLampMetaTest(the metadata codec's round trip). - The content: every
Mmo_Xprow withSkillALLon an achievement (45) or an achievement-point milestone (Points_3000toPoints_24000) becomesMmo_LampwithAmount3X andPicks3; anMmo_Xprow paying one weapon skill on a kill achievement or kill quest (14), on Combat Master or the damage achievements Hard Hitter, Powerhouse and Force of Nature, becomesMmo_Lampwith the sameAmount,Picks1 andSkillsCOMBAT. Defense and Acrobatics rows, boost tokens and titles are unchanged, and criterion keys and ids keep saved progress. - Claim rewards added to an achievement already earned (CommonLib 2.3.0).
AchievementDefinitiongainsgetClaimAddedOn()andBuilder.claimAddedOn(LocalDate):CommonAchievementFoldreads CommonLib'sClaimAddedOn,toRuntimecarries it onto the runtime achievement,AchievementConfig's disabled-meta rebuild keeps it andSharedAchievementWriterwrites it back, so a pack achievement this mod folds offers its Claim rows to a player who earned it before they shipped, as one CommonLib folds itself does.AchievementService.claimAchievementRewardschecks bag room forAchievementEngine.claimRewardsOwed(what this collect pays) instead of every Claim row, so a collect reopened for added rows is never refused for rows already paid. A reopened achievement reads unclaimed until it is collected, so meanwhile it drops out of the achievements power pillar (BuiltinPowerPillars.achievementsContributioncounts claimed ones), onePillars.Achievements.Factorshare each. Tests: none new. /quest claimcollects a bounty whose board is gone.PlayerQuestCommand's claim, one quest orall, asksQuestEngine.canCompleteAt(subject, quest, null)where it readquest.turnInAt() != null, as the quest book's Collect does, so a bounty taken at a board that is taken down or out of season is claimed from chat instead of refused withui.quest.claim_at_site. Tests: none new.- The guard bash's knockback and the riposte's stagger are files of their own.
MMO_Guard_Bash_HitandMMO_Parry_Riposte_HitnameMMO_Guard_Bash_Hit_NextandMMO_Parry_Riposte_Hit_Nextby id, where each carried itsNextinline. An inlineNextis registered with the asset store's contained assets, after the guard and parry selectors' inline roots can already compile the hits, so on some boots the server warned "Missing interaction" and an early hit could skip its knockback or stagger until the post-load rebuild; a top-level file is registered in the main batch, before any root compiles. The numbers are unchanged. No test.
Removed
event/EntityCombatTextSanitizeSystemand its registration (SystemBootstrap.registerCombatTextSanitizeSystem): nothing ships an extraCombatTextasset to strip from an entity'sUIComponentListany more.Server/Entity/UI/MMO_CombatText_Red.jsonandMMO_CombatText_Gold.json: the crit and level-up colours ride theCombatTextUpdatepacket'scolor; acombat-text.jsonnaming either id reads as its colour (#FF3B3B,#FFD700).critStyleandlevelUpStyletake a colour (#RRGGBB), so one naming a pack's ownCombatTextasset falls back to the default colour with one warning at load (the game's ownCombatTextreads as white,#FFFFFF), and the crit and level-up text draw at the game's own size (48) where the two assets drew them at 56 and 52. For pack authors: on Update 7 the role builder puts every loadedEntityUIComponentasset on each role-built mob, so a pack that still ships aCombatTextasset of its own draws its own extra damage number on every mob; delete it and write a hex colour intocritStyleorlevelUpStyleinstead.ColoredCombatTextService.drainRestores(World, Store)andclear(), with the per-viewerUIComponentsUpdateswap and restore queue they drained;showToandshowAbovetake a#RRGGBBcolour where they took a style asset id.FeedbackEvent.combatTextStyle(), replaced bycombatTextColor()(a#RRGGBBhex, null for the client's white).CombatTextConfig.getStyle(FeedbackEvent), replaced bygetColor(FeedbackEvent).SkillDisplayUtil.combatTextCrit(SkillComponent), which nothing called.
v1.7.0 developer notes
Learns what RPG Stations 1.1.0 adds for a station that takes things apart: the consumed-piece objective kind, per-station statistics and a Stations section in the statistics. It also audits the library's gear sets and reputations, mirrors its reputation-rank kind, and lets an achievement criterion compare its qualifier by prefix. Beside the stations work it carries the player half of the power-vs-difficulty parity work (the mitigation and healing bounds that make a mob at difficulty N a fair fight for a player at power N, a ceiling on crit chance, and the power retune that anchors an all-L100 build at MaxPower instead of far past it), and a preventive fix ahead of the next Hytale update, which reshapes the floating-combat-text packet. Bonus drops gain a third moment, the hand harvest, so the lucky harvest that was coded in Java becomes a shipped row an owner can retune or switch off. It also ships the jar's half of Hallow's Eve: the skill layer over the seasonal pack, a boost-rule store and the Almanac in the menus. Its menu pages move onto ziggfreed-common's shared menu, its player settings onto the library's Settings tab, and COMPLETE_QUEST to the library's own producer. Its quest and achievement surfaces move onto the library's rebuilt Objective Book and its list-and-page kit: the statistics become a source for the book's Statistics view, the book's side column and the stat detail page retire, and the skill reward ladder and the hub guide's previews are rebuilt on the kit, and the Skill Overview's pinned and tracked panel retires. Built against ZiggfreedCommon 2.2.0 and RPG Stations 1.1.0 (api contract 10).
Breaking changes
- Requires ZiggfreedCommon 2.2.0 or newer.
REQUIRED_ZIGGFREED_COMMONand theziggfreedCommonVersionpin both read2.2.0; the pin feeds the manifest's^2.2.0floor. The qualifier comparison, the gear-set validator and the declared-metadata list below are all 2.2.0's, so the two jars move together. - The optional RPG Stations range is
>=1.1.0 <2.0.0. The old>=0.1.0 <0.3.0admitted neither RPG Stations 1.0.0, its last public release, nor the 1.1.0 held with this one. This jar compiles against the 1.1.0 api (rpgStationsApiVersion=1.1.0, contract 10); an older RPG Stations falls outside the range, and the MMO then runs stationless while RPG Stations runs standalone. The api artifact is versioned with the mod, not with its contract number (1.0.0 carried contract 9), and the stale "contract N ships as 0.N.0" comment ingradle.propertiesand the version facts inintegration/stations/CLAUDE.mdsay so now. - Player mitigation is bounded, and player health regeneration is gated. Three numbers move, all of them player-side ceilings, because parity is impossible without them:
ModifierClamps/DefenseReduction.jsonMax0.9 to 0.58,ModifierClamps/SchoolResist.jsonMax0.9 to 0.3, and skill-tree health regeneration now waits 15 s after the last damage taken. The arithmetic behind it: the MMO's defense cap composes MULTIPLICATIVELY with the engine's own armor percent (0.40 on the top vanilla rung) and, for elemental hits, with school resist, so a capped build kept six percent of an incoming hit and a fully-regenerating one healed 38.6 HP/s through the fight. 0.58 composed with top armor leaves about a quarter of the hit, which is the bound the mob curve is now derived against. An owner who wants the old feel raisesMaxviaability-clamps.jsonand lowers mob damage to match; the two are halves of one sum. - The regen gate reads the ENGINE's own
DamageDataComponent.getLastDamageTime()throughTimeResource, the same field and clock itsNoDamageTakencondition tests for NPC regeneration, so a player and a mob answer "am I in combat" identically and any damage source counts. Unreadable state answers false, so a missing component leaves regeneration exactly as before. Mana regeneration is deliberately NOT gated - mana is what an ability spends, so gating it would stop a caster acting rather than stop them out-sustaining a mob. - Every server's power numbers change:
PowerLevel/Default.jsonis retuned at source so a realistic all-20-skills-L100 character lands atMaxPower200 instead of 295 (tank) or 369 (damage), andMaxPoweritself stays 200. Power is recomputed live from a transient per-component cache and is persisted nowhere, so there is NO data migration: the first read after the update simply returns the new number, and a companion mod that scales mobs to power sees it at once. Four corrections, in order, each removing one measured distortion, and only the last one fitted:
1. The StatRewards pillar counts a saturated stat only up to the ceiling gameplay enforces on it (reward/StatCeilings.usable, the one per-stat ceiling table the pillar and the stat pages share): BLOCK_PCT up to the live DefenseReduction clamp (CombatDamageEventSystem.defenseReductionCeiling()), CRIT_PCT up to the new CritChance clamp (SkillTreeService.criticalChanceCeiling()), FALL_PCT up to FULL_FALL_REDUCTION (1.0, the semantic bound: a full reduction is no damage). The pillar read those ceilings from the same seams the hit uses, so retuning a clamp retunes what power pays for it. The raw sums it was paying full price for reached 3.58 defense (6.2x the usable 0.58), 3.05 (tank) to 11.12 (damage) crit depending on the pick order, and 1.65 fall. The gameplay clamps themselves are untouched and not applied twice.
2. Each scope-bearing factor is divided by its measured over-count ratio. sumAllScopesTreeOnly sums every weapon and skill scope at once, but a hit resolves the global channel plus ONE active skill's channel, so the pillar counted several times what any single moment can use. Every ratio is measured on the LOADED trees through the real claim path, as the all-scopes sum over the most ONE moment can resolve (the global channel plus the one skill's own channel, maxed over the roster that moment is attributed to: the combat roster for a hit, every skill for an XP award or a luck roll), each key on a build that picks it first: DAMAGE_PCT 4.2x (2.12 over 0.50, Axes) and CRIT_PCT 4.9x (11.12 over 2.29, Daggers) on the damage build, FLAT_DAMAGE 4.0x (432 over 108, Staves) there too; LIFESTEAL_PCT 2.4x (0.80 over 0.34), COMBO_PCT 7.0x (1.26 over 0.18), FLAT_LIFESTEAL 2.9x (49 over 17) and FLAT_COMBO 4.6x (123 over 27) on a build that picks lifesteal and combo first, since neither reference build ever picks them; XP_BOOST_PCT 14.7x (11.59 over 0.79, Crafting) on a build that picks XP boost first and LUCK_PCT 4.3x (5.37 over 1.24, Woodcutting) on one that picks luck first. The all-scopes READ stays (a Swords-scoped pick earns the same power as a universal one), so the correction lives in the factor. STAT_HEALTH / STAT_MANA / STAT_STAMINA / BLOCK_PCT / FALL_PCT / HEALTH_REGEN / MANA_REGEN are authored without a scope and are not divided.
3. The MaxHealth pillar is wired in and health is scored once. Modes.Balanced gains MaxHealth: 0.5 (the same weight as StatRewards: the health half weighs what the reward half weighs; CombatOnly 0.25 and Completionist 0.4 follow the same ratio), and STAT_HEALTH drops to 0.0 so a health pick is not paid for as a reward AND as live health. The pillar now counts health ABOVE BaseHealth (((max - BaseHealth) / BaseHealth) * Factor, Factor 9.0), so a fresh 100 HP character earns nothing from it and MinPower 1 still holds; class, gear and potion health count too, once, as they should.
4. What remains is scaled by 2.0 so both reference builds land on 200: post-correction factors STAT_MANA / STAT_STAMINA 0.09, DAMAGE_PCT 14.15, CRIT_PCT 9.27, LIFESTEAL_PCT 19.12, COMBO_PCT 4.29, BLOCK_PCT 30, FALL_PCT 6, LUCK_PCT 3.46, XP_BOOST_PCT 1.02, FLAT_DAMAGE 0.15, FLAT_LIFESTEAL 0.31, FLAT_COMBO 0.13, HEALTH_REGEN / MANA_REGEN 15.
- Result, measured through the real
PowerLevelService.computeWithfold over components built through the real claim path (every skill at the level named, one pick set per tier by the build's priority, then ability unlocks, then authored order), and stated as the CLAMPED number every consumer sees: fresh character 1; all-20 at L20 32 (tank) / 30 (damage), L30 52 / 46, L50 98 / 89, L80 165 / 161, L100 200 / 200 (the unclamped sums land at 204.2 and 201.7, so both reference builds sit onMaxPower, 2.5 apart; before, they sat at 295 to 369 and were held to the same 200). A three-skill specialist at L100 reads 92 (Swords + Defense + Acrobatics, tank picks) to 86 (three weapons, damage picks). Said plainly: a clamped scalar compresses the top of the range by construction, so the broad build read 200 before and reads 200 after, and the specialist did NOT rise against it. A crit-heavy three-weapon specialist read about 134 before, because 3.2 of summed crit chance was paid for at 22.5 a point; it reads 86 now. What the retune changes is what the top of the ladder is made of (a saturated stat and a scope over-count no longer carry it) and the spread between two L100 builds (2.5 where it was 74), not the specialist's share of the ceiling. Two figures in the plan's first distortion table came from a text parse of the Java tree defaults that mis-typed 23 flat-damage rewards as parry rewards: aFLAT_DAMAGEraw of 347 (the loaded trees sum to 267 tank / 432 damage at L100) and aCRIT_PCTraw of 10.22 (11.12 loaded). Every factor above is derived from a ratio measured on the loaded trees, not from that parse; the four percent ratios first taken from it moved when re-measured (DAMAGE_PCT7.1x parsed to 4.2x loaded,CRIT_PCT5.5x to 4.9x,XP_BOOST_PCT13.2x to 14.7x,LUCK_PCT4.4x to 4.3x, the last two because the measuring build now picks the key first rather than the reference build's leftovers), and the uniform scale stayed 2.0 because the re-derived pair still lands inside 190 to 205.XP_BOOST_PCT, 17.8% of the tank's stat pillar before for a stat that is not combat power, is under 3% of total power after. - Owner-facing: a
power-level.jsonthat re-authors a factor keeps working leaf by leaf, but its numbers were tuned against the old raw sums and want a second look;PowerLevel/Default.jsoncarries a$Commentexplaining every pillar and the health rule. - Critical-hit chance is capped, at 1.0 as shipped, by a new reserved
ModifierClamps/CritChance.json. The hit roll is onerandom.nextDouble() < critChance, so any chance at or past 1.0 was already a guaranteed crit and every point above it inert; the tree could sum to 3.05 (tank) or 11.12 (damage) across all scopes and up to 2.29 on a single weapon. The ceiling isSkillTreeService.criticalChanceCeiling()(fail-closed to 1.0 before the asset stores load): the hit roll applies it inresolveCriticalChance, whose one caller isCombatDamageEventSystem, and the Stats page and the power pillar read the ceiling itself throughreward/StatCeilingsagainst the summed chance, so what the three share is the ceiling, not one method; the number lives in the asset, never in Java, and an owner lowers it inability-clamps.jsonunderCritChance. The Stats page's crit label paints through the same capped-total renderer as the stat-sources page and the held-item panel (the summed chance, with the cap beside it once reached) and adds a hint line (ui.viewxp.buffs_hint_crit_capped, nine locales) so a player learns a further crit pick buys nothing.
Features
- Adds the
STATION_INPUTobjective kind. One stack a station consumed: target = the item id, qualifier = the station id, amount = the count, counted once when the consumption commits and never for a refunded input; an unattended settle has no worker and credits nobody. RPG Stations produces it and ships its kind file (ObjectiveKinds/RpgStations/Station_Input.json, not value-based, producible); this jar carries theObjectiveTypemirror so the quest and achievement folds accept it, joins it to the item-name family besideSTATION_OUTPUT(TargetNameResolver, the countable-target set) and shipsobjective.text.station_input/.anyin en-US. The MMO never listens toStationInputConsumedEvent: the moment reaches the statistics through the shared progression dispatch like the other two station kinds, so a listener would count every piece twice. - Adds the
USE_ITEMobjective kind mirror. Ziggfreed Common 2.2.0 seedsUSE_ITEMas a built-in kind (target = the item id, qualifier = how it was used, such asThrow,CrackorCostume, amount 1 per use), and a pack's item fires it through the library'sZigCreditProgressinteraction step. This jar carries theObjectiveTyperow (accumulating, quest-producible), so the quest and achievement folds keep aUSE_ITEMstep instead of dropping it and the content validator accepts one, and joins it to the item-name family inTargetNameResolver, so a step names the item by the name the item gives itself. The sentence is the shared vocabulary'sobjective.use_item/.any; this jar ships no key of its own and fires nothing. LOOT_RECEIVEDjoinsObjectiveType(quest-producible, accumulating;TargetNameResolvernames its target as an item), so this mod's folds and validator keep a step on an item a reward hands over instead of dropping it; ziggfreed-common'sZigLootReceivedProducerfires it.- The leaderboard paints a player's name through ziggfreed-common's
PlayerDisplayNames(pages/render/PlayerNameText.of(playerId, storedName), on#PlayerName.TextSpansand#YourName.TextSpans), so a player's shown title appears beside their name on every row, online or not, a player hiding their status included (zc'sActiveTitleskeeps and persists the choice): a decoration never tells a viewer who is online. Only a row with no player id reads its stored name. Test:PlayerNameTextTest. - Adds station input statistics, and both station stack tallies break down per station.
STATION_INPUTfilesStation_Inputs(total, and per item exactly likeStation_Items).STATION_OUTPUTandSTATION_INPUTalso file per station off the moment's qualifier, underStation_Items_By_StationandStation_Inputs_By_Station, each entry named(- @
StatisticsComponent.atStation); a moment with no station named files no station line, and a per-station count is the sum of that station's entries (StatisticsComponent.sumAtStations, ids matched without regard to case). Three persisted fields are appended to the statistics codec,PerStationInputs,PerStationInputsByStationandPerStationItemsByStation; a save written before them decodes them empty, and the goldenStatisticsBlobCompatTestbytes are unchanged and now pin that. The counting rule lives in one store-free seam,StatisticsService.file, whichStationStatisticsTestdrives. - @
- Adds a Stations section to the statistics (the book's Statistics view, through
MmoStatisticsbelow) with two rows: Pieces Unmade (every stack consumed atDisenchanting_TableorDisenchanting_Table_Greater) and Essence Drawn (Ingredient_Life_Essence,Ingredient_Void_EssenceandIngredient_Voidhearthanded over at those two tables, so the Sawmill's Essence of Life never counts). Both are sums across two tables rather than one raw breakdown (statistics/StationStatRows). The section shows only with RPG Stations installed and at least one of the two tables loaded (StationStatRows.sectionShown, over the bridge's newRpgStationsBridge.loadedStationIds(), ids matched without regard to case); a server without them shows no Stations section. Three new keys,ui.achievements.stats.stations,.pieces_unmadeand.essence_drawn, in en-US. - Achievement criteria carry
QualifierMatchMode. ZiggfreedCommon 2.2.0 gives an objective a comparison for its qualifier beside the target's (EXACT/CONTAINS/PREFIX, unauthored =EXACT, so every shipped file keeps its meaning). This jar rebuilds every achievement criterion twice (the shared fold intoObjectiveSpec, thenAchievementDefinition.toRuntimeback into a fresh engine objective), so the leaf rides both trips:ObjectiveSpecgains the component (its constructors without it default toEXACT),parse, the structuredCODECandfromRuntimeread it,SharedSchemaJson.objectiveLeaveswrites it when it is notEXACT, the legacy quest converter knows the key, and the legacyachievements/owner files readqualifierMatchModetoo. A rung authoredQualifier Disenchanting_TablewithPREFIXcounts both tables, exactly as the library would (CommonAchievementFoldTest). Quest steps run on the library's quest engine directly and needed no change. /mmoconfig validateaudits gear sets.ContentAuditregisters the library'sgear_setdomain overGearSetValidator.audit(), so a set naming an item nothing ships or a tier with no condition reads beside every other content domain./mmoconfig validateaudits reputations.ContentAuditfolds ziggfreed-common's reputation validator: it registers the library'sreputationdomain overReputationValidator.audit(), besidegear_set, so a companion naming a group nothing ships, a native group with noNPCGroupskey or a ladder of fewer than two ranks reads in the MMO's audit and its boot summary. The library also logs the validator's errors once at boot on its own, for servers without this mod.- Declares the ability-mod record safe to destroy with its item. A consuming station refuses a stack carrying a metadata key nobody has declared; the item domain's new setup step (
item/ItemBootstrap) declaresMmoAbilityModsinto the library's server-wide list of metadata keys that may go with their item (DisposableItemMetadata), so a station's fallback routes can unmake an enhanced piece that has no salvage recipe. The XP token's record is deliberately left undeclared: consuming a token anywhere but its own redeem path would destroy the XP it carries. - Adds a Modifier Clamps page to the admin hub (
pages/admin/ModifierClampsPage, theModifier Clampstile beside Power Level). The ceilings that decide fights (DefenseReduction,SchoolResist,CritChance,HitDamage's percent stack) and every otherModifierClampsasset had no UI: an owner hand-editedmods/mmoskilltree/ability-clamps.jsonand ran/mmoconfig reload. The page lists every clamp theParamClampTablefold actually holds (a pack's or an owner-declared one included, never a hardcoded roster), reserved combat ceilings first, each under a sentence saying what it bounds in game; the three fight-deciding ones bind their LIVE ceiling as typed numbers, so a defense cap of 0.58 reads as "leaves 42% of an elemental hit", with the sentence adding in words that a physical hit lands lighter still once worn armor has taken its own cut (the armor figure is game data the page does not carry, so it quotes no number for it). Each of the four axes (Min/Max/MinTotalPercent/MaxTotalPercent) is one owner leaf with Default (the asset layer) / Current (what a hit is bounded by) / Edit / Reset, written throughAbilityClampOverridesConfig.setOwnerLeafand cleared throughclearOwnerLeaf, so an untouched axis stays absent from the file and keeps inheriting the jar / pack value. A save re-folds the owner layer at once and bumps the clamp table's generation, one term of the power memo's epoch, so every player's cached power recomputes on its next read (AbilityClampOwnerLeafTestproves the write, the un-merge, the partial file, the owner-declared clamp and the epoch move). Fifty-eightui.clamps.*lines in nine locales. - The Power Level page edits the
StatRewards.Factorsmap. The sixteen factors the retune above moved were the one section the page left file-authored. They are a MAP, not a fixed leaf list, so the rows are theStatKeyroster plus whatever keys the resolved settings carry (a key the jar surfaces later lists itself with no page change), each its own owner leafPillars.StatRewards.Factors.with the same per-leaf override and reset as the rest of the page, and because the asset overlay merges the map per entry a cleared key reads the jar / pack value again, never a zero (PowerLevelOwnerLeafTest). A dash is a factor no layer names. - Adds a third bonus-drop moment,
PickupItem, and the lucky hand harvest becomes a shipped row. A row withWhen.Kind"PickupItem"answers a hand harvest, itsMatchread against the harvested item's id, and fires after the harvest itself has landed. LikeBreakBlockit fires only when the harvest paid skill XP, and with noChanceit is as likely as the player's own summed luck for that skill. A harvest consults onlyPickupItemrows, never aBreakBlockone, so a released"*"BreakBlock row keeps meaning only broken blocks. The lucky harvest that was coded in Java is nowServer/MMOSkillTree/BonusDrops/Mmo_Harvest_Luck.json(PickupItem, Match"*", noChance, oneMoment_Itemreward, the note on), with the same odds and the same amount as before: one more of the harvested stack, at the player's summed luck. An owner switches it off with"Mmo_Harvest_Luck": {"Enabled": false}underdropsinbonus-drops.json, retunes it per leaf, or adds a narrower PickupItem row for one crop; the most specific pattern wins, so a narrower row that should still hand over a copy writes its ownMoment_Item. A PickupItem row's commands take{player}and{item}(the item id the XP matched on). A pack shipping a PickupItem row needs this version: on 1.6.x the moment is unknown, so the row names no moment and/mmoconfig validatereports it as an error. - Adds
Moment_Itemto the bonus-drop reward vocabulary. Ziggfreed Common 2.2.0's reward for "another of the stack this moment is about": a true copy of the stack the game handed over (item, amount, durability, quality and metadata), handed over exactly like the harvest.Params.Countis in whole copies, fractional allowed (0.5is one copy half the time), 1 when unwritten, summed across the pass and resolved once, and at most 64 copies a pass. It pays only in a PickupItem row. The audit names where it cannot pay:PASS_ONLY_REWARD_KINDin a BreakBlock or KillMob row's inline rolls (a warning) and in a quest's or an achievement's rewards (an error, as for every pass-only kind; Ziggfreed Common's own shop and board audits warn about one there),LOOT_MOMENT_COUNT_INVALIDfor a Count that is not a number above zero,LOOT_MOMENT_COUNT_ABOVE_CEILINGfor one above 64. - Adds the
mmoskilltree:moment_skill_luckfactor. NoParam: it reads the player's summed luck for the skill the moment paid XP to, in whole luck points, so{"Factor": "mmoskilltree:moment_skill_luck", "Weight": 1}in a BreakBlock or PickupItem row'sChanceis exactly the unwritten default, and aBaseor a second term adds to it. Unlikemmoskilltree:station_luck, which reads the same kind of sum as a fraction (0.05 for five points). A kill names no skill, so the factor is unanswered there and its term adds nothing; a named skill's luck stayshytale:statterms overMMO_LuckandMMO_Luck_. - Adds a running count to the bonus mob-drop note, on a line of its own. Several kills inside the notification cooldown read as one "Mob Drop! (+N)" (
notify.mob_drop.stacked, new, nine locales), batched apart from lucky finds, on a feed line of its own so it never replaces a lucky-find note.Notify.Colorhas no single default any more: left out, it is#ffff00on a BreakBlock or PickupItem row and#ffaa00on a KillMob row. - Boost rules. New structured store
Server/MMOSkillTree/BoostRules/(asset/type/BoostRuleAsset:Text,Enabled,Skills,Multiplier,Requires), foldeddefaults < pack < ownerbyboost/BoostRuleConfig, which keeps the usable rules as one snapshot (no owner file feeds the owner layer yet).XpBoostService.getTotalMultiplierByNameadds each live rule to its GLOBAL bucket (stack mode and cap apply), gating throughMmoGates.passesover the earner's saved skills;getLiveRulesserves the boosts page's active list and the XP page's bar (pages/render/BoostRuleRows, an[EVENT]row per skill). Content-audit domainboost_rule(BoostRuleValidator): ERRORBOOST_RULE_NO_SKILLS, WARNBOOST_RULE_UNKNOWN_SKILLandBOOST_RULE_NO_LIFT, INFOBOOST_RULE_ALWAYS_ON. ObjectiveType.CALENDAR_ATTENDED(quest-producible, accumulating) mirrors zc-calendar's kind file;TargetNameResolver.calendarEventNameMsgnames the event by itsPresentation.TitleKeythrough zcContentKeys;objective.text.calendar_attendedand.any.ObjectiveType.REPUTATION_RANK(quest-producible, accumulating): the MMO's objective vocabulary mirrors ziggfreed-common'sREPUTATION_RANKkind file (ObjectiveKinds/Reputation_Rank.json; target = the rank id on the library's ladder, qualifier = the reputation id, amount 1 per credit), which the library's reputation service credits for every rank a player's effective standing has reached and this mod never fires. Without the row the achievement fold drops a rank criterion, Seasons of Orbis's Jack's Confidant among them.objective.text.reputation_rank("Reach {1}") and.any("Reach a new rank") match the library's own sentences, so a step reads the same with or without this mod. A step with noTextKeyof its own names the rank by its id ("Revered") throughTargetNameResolver's default, never a reputation's own rank name; Jack's Confidant words its step itself. Test:ObjectiveKindVocabularyTest.theReputationRankKindFileAgreesWithTheEnum.ObjectiveTextRendererrung 1b. A step's ownTextKeythat another loaded namespace ships renders as the sentence with the zone wrap and without the qualifier wrap, andcomposestops deferring such keys to zc's neutral composer (which wraps the qualifier a second time).- The Hallow's Eve jar layer (
Hallows_Eve/folders): XpMapsHallows_Eve(ARTILLERYcause:Hallows_Eve_Lantern_Burst), MobKillXpMmo_Hallows_Eve, LootablesMmo_Hallows_Eve_Geode_Crack(ContributesToHallows_Eve_Geode_Crack, rolls only, MINING), GearSetsMmo_Hallows_Eve_Hallowed(a second set over the pack's members: both helms,Hallows_Eve_Jack_HelmandHallows_Eve_Burning_Jack_Helm, and both shields,Hallows_Eve_Jack_ShieldandHallows_Eve_Flaming_Jack_Shield, the same four the pack's own set lists, so any helm with any shield takes its one Armor 1 / Utility 1 tier), BountiesMmo_Hallows_Eve_Daily_{Artillery,Harvesting,Mining,Crafting}(Hallows_Eve_Dailyin the Difficulty bandHallows_Eve_Skill, which only these four carry, so the board's optional Skill slot posts one of them every day; Weight 0.5 each sets the pick among the four; gatedHallows_Eve_Live;Rewards.ClaimpaysHallows_Eve_Board_Pay, the skill's XP and twoReputationrewards, collected at the board like every contract's, favor withHallows_Eve_Old_JackandHallows_Eve_Hallowedin the same amounts as the pack's own contracts; the band's badge isboard.grade.hallows_eve_skill, "Skill Work", which zc's board labels find under any namespace), RpgStations ExtensionsMmo_Hallows_Eve_CarvingandMmo_Hallows_Eve_Carving_Hallowed, BoostRulesMmo_Hallows_Eve_Harvest_Moon(1.25 on HARVESTING and ARTILLERY whilecalendar_livereads 1 for bothHallows_EveandHarvest_Moon). Seasons of Orbis (Ziggfreed:SeasonsOfOrbis), the yearly-events pack whose first event is Hallow's Eve, hard-depends on ZiggfreedCommon and RPG Stations and never on this jar, so the carving extensions always find their bench where the pack runs. The pack's quests, contracts and achievements pay this mod's rewards in their ownRewards(Mmo_Xpin the skill the work trains,Mmo_Boost_Tokenonhallows_eve_the_hallowedandhallows_eve_jacks_guest; the pack's developer notes list each row), so no jar file contributes toHallows_Eve_{Chain,Lantern,Daily,Board}_Pay: a roll there would pay the same work twice (SeasonsOfOrbisMmoRowsTest,HallowsEveJarContentTest). The four skill dailies'Hallows_Eve_Board_Paynow pays the pack's sweets and items only. On a server without this mod those rows are skipped at payout (one[grant]warning each) and dropped from every reward panel, and zc's boot audits warnUNKNOWN_REWARD_KINDonce per row. - The Almanac in the menus. The hub's Almanac tile and the menu's Almanac tab both follow zc's one rule (
AlmanacPages.menuTabVisible); the setting is zc's$MenuTabinmods/ziggfreedcommon/almanac.json, which the admin General page's dropdown edits (config/AlmanacTabSetting: "While a season runs" isOnlyWhileLive: true, "Always hidden"Show: false, "Always shown" the defaults).seasonalTabMode, which no release wrote, is dropped: a leftover key is ignored on load and gone at the next save. The hub tile drawsDeco_Scroll, the rail's Almanac picture, and its hint readsAlmanacPages.headline()while a season runs ("Hallow's Eve is on now"), elseui.hub.cta.almanac_hint(MmoHubPage). - The Seasons statistics are the library's. The book's Statistics view lists the Seasons section zc-almanac contributes (a row per season, a page with its tallies and a button to its Almanac page); this jar ships no Seasons section of its own, and
SeasonStats, added earlier in this cycle, is gone with its test. - The title picker is ziggfreed-common's, opened from the Title card on zc's Settings tab;
MmoSettings.registerthemes it (TitlePickerPages.deps), and its Back returns to Settings. MmoBounties.difficultyColorgives theseasonalband its own accent.- Known on servers without Seasons of Orbis: the content audit lists the set's four members as unknown and the loot audit notes the five contributions, both at INFO in the boot log;
/zigcommerce validatelistsUNKNOWN_BOARDfor the four dailies and/rpgstations validatethe two extensions' unknown target. - Tests:
CombatWeaponUtilCauseRoutingTest(two cases),HallowsEveMobKillXpTest,HallowsEveJarContentTest(among its cases, each skill daily posts in the Skill slot and earns favor with Old Jack and the Hallowed),DialogueContentResolutionTest(registers the library'sReputationdestination andGrantaction as a server does, so Old Jack's favor lines decode),ObjectiveTypeContractTest,TargetNameResolverTestandObjectiveKindVocabularyTest(one case each or two),MmoBountiesTest,ObjectiveTextRendererContentKeyTest,BoostRuleAssetTest,BoostRuleConfigTest,XpBoostServiceBoostRuleTest,BoostRuleValidatorTest,BoostRuleRowsTest,AlmanacTabSettingTest. - The menu pages use zc's shared menu.
pages/MmoMenufillsZigMenu.consumer: the Skills section (Overview, the four categories, Class, Boosts, Abilities, Mastery, Leaderboard, Admin), the landingMmo_Skills, the white-label branding, the theme's palette (ThemeRetint.menuPalette) and its frame paint. Each Skills tab carries a picture (MmoMenu.categoryIcongives a category its lead skill's icon, CombatWeapon_Sword_Crude).ViewXpPage,BoostPageandAbilityBindPagesit in@ZigMenuFramewith their nav blocks gone;MmoObjectiveBookDepsfills the book's seams and no longer paints a column (see the book entry below);MmoBookRail.ui,SeasonalTab,TabNavHelperandSeasonalTabModeare gone. New destinationsMmo_Class_Hub,Mmo_Leaderboard,Mmo_Settings(opens zc'sSettings) andMmo_Admin(refuses a non-admin);Mmo_Skillstakes an optionalCategory;Mmo_Quest_LogandMmo_Achievementsare aliases of zc'sQuest_LogandAchievements. Mmo_Quest_Bookretired.getting_startedandforgot_somethinghand outZiggfreed_Objective_Book(their flavor lines name it in every locale); the item andmmo_quest_menu_openstay, so an owned stack opens the menu on Quests.- Settings is ziggfreed-common's tab.
pages/settings/MmoSettingsfillsZigSettings.consumerwith three cards (Skill notifications, Skill HUD, Online status), shown before the library's own, and fills the library's HUD layout page (admin audience, theme) and title picker (theme);SettingsPage, its document,MmoHudSettingsandMmoTitleSettingsare gone;Mmo_Settingsand the Class hub's tile open zc'sSettings, and the Skills section has no Settings tab.player/SettingsStateMigrationmovesHudKey.QUESThidden andquestNotificationLevelinto zc's store once on the ready pass (milestones to Milestones, objectives and quests to Finishes, none to None; a choice already made there wins) and resets the fields./mmohud questandMMOSkillTreeAPI.isHudVisible("quest")read and write zc's store, and/mmohud show|hide questanswerscmd.hud.quest_lockedand changes nothing while the owner has locked the tracker's Show;MmoQuestSubjectno longer answersFeedbackAudience(this mod'sAchievement_UnlockedandQuest_Parkedtoasts carryPlayerLevel); the DB backend'sHUDdomain holdsPlayerSettingsComponent;HudPreferencePersistencebecomesPlayerSettingsPersistence. - No copies of zc's UI.
MmoFrames.uiis gone: the framed pages use zc's$ZF.@ZigDecoratedFrame(the same frame), and the bright tooltip the ability and skill-tree cards wear (@MmoTooltipStyle) moves toMmoButtons.ui.MmoButtons.uidrops the unused TurnIn, Neutral button-element, Option and chip-grid styles, and@MmoRowIconGridStylefor zc's@ZigIconGrid24. The admin pages draw zc's@ZigSectionCardand the hub zc's@ZigTile(Common/ZigCards.ui);MmoAdmin.uikeeps only its own styles.UIUtil's dropdown, field and button binders hand the call to zc'sSettingsUiUtil. - The rail pages. The Abilities page's filter, sort and hide chips are zc's rich tabs (
@ZigRichRowBtnStyle,SettingsUiUtil.setTabActive), it gains the title row for right-mode branding, and the toolbar labels and the XP page's section title take@ZigSectionHeaderStyle. The XP page's buffs bar fits the menu frame's column with#BuffHinton its own line (RailPagesDryTestsums every fixed row againstMenuFrame.BODY_WIDTH). Each page hands its own events to oneroute, and whatever it leaves unanswered is answered (PageExitsAnsweredTest,RowIndexTest), so a stale or refused click (item rewards while command rewards are off, a mastery link with no track, a boost that just ran out) no longer leaves the client waiting. - White-label branding on the menu pages. Right mode shows the owner's logo scaled into each title row's
AssetImage #BrandingLogo(134 x 50 on XP, Boosts and Abilities; zc's book and Settings size their own), reading the branding pack'sPages/logo.png(MmoMenu.LOGO_ASSET,MmoMenu.paintTitleRow), instead of appending the 105pxPages/branding.uiblock, which overflowed every title row; a pack withoutlogo.pngshows the slot's blank fallback. The server's name is the title (or the title hides) and the description sits under it, on XP, Boosts, Abilities, the Objective Book and zc's Settings page, which regains the right-mode branding 1.6.2's settings page had. The Leaderboard, which brands its title row in both modes, takes the same scaled logo (MmoMenu.paintTitleRow(cmd)). Left mode still appendsbranding.uiat the top of the rail. Under a theme, a White Label accent turns the rail's pane see-through (MmoMenu.railPalette, zc'sMenuPalette.BACKGROUND), so the accent-tinted frame shows through the rail as it does on the default theme;ThemeRetint.whiteLabelTintis the one reading of the accent for the frame, an external panel and the rail.RailPagesDryTestholds every title-row page, the Leaderboard included, to its four hosts and no page to appending into a title row, andMmoMenuBrandingTestthe paint;ui.whitelabel.position_helpsays where each mode shows the branding, in nine locales. - Every text size comes from ziggfreed-common's type scale. ZiggfreedCommon 2.2.0 names the family's sizes once, in
Common/ZigType.ui:@ZigFontCaption13 (the floor),@ZigFontSection13,@ZigFontBody14,@ZigFontEmphasis15,@ZigFontHeading16,@ZigFontSubtitle18,@ZigFontTitle20,@ZigFontDisplay22,@ZigFontDisplayLarge24, and@ZigFontMicro11, the one step under the floor;com.ziggfreed.common.ui.ZigTypeholds the same numbers for Java. Every one of this jar's 95 documents that sets a size imports it ($ZT = "../Common/ZigType.ui";fromPages/andHud/,$ZT = "ZigType.ui";fromCommon/) and writesFontSize: $ZT.@ZigFontBody,MinShrinkTextToFitFontSizeincluded, so no player text is drawn under 13 and one edit there resizes the family. Text at 9 to 12 px rises to Caption; row labels, names, buttons and fields on player pages rise to Body; uppercase section and table headers are Section; headings keep their sizes through same-size steps. The admin editors' row labels stay at Caption (13), since their narrow fixed controls would cut at 14. The hero sizes the scale has no step for (17, 26, 28 and 32 onClassHubPage.ui,MmoHubPage.uiandOnboardingPage.ui) stay numbers. One label in a box too small for 13 takes Micro, up from 10: the cooldown HUD's slot label (MmoSkillTreeAbilityCooldown.ui). The skill card's percent (SkillCard.ui) reads at Caption: its bar row is 17 high with the 8px bar centred in it and the label 40 wide, centred, the room taken from the two gaps around the row, so the header column still holds 54. Rows whose label no longer fit grew where the parent had room (the skill card's XP, meta and hint lines and the reset card's count, 14 to 17 high). The sizes Java sets readZigType:RewardListRenderer's compact title (BODY, from 13) and the four override editors' empty-list notes (CAPTION, from 12). Three admin pages (BoostTemplateOverridesPage.ui,XpOverridesPage.ui,XpTokenOverridesPage.ui) nameMmoAdmin.ui's@AdminRemoveBtnStyleinstead of the local copy the sweep had made byte-identical (LookNeutralDryTest). A document importingZigType.uineeds ZiggfreedCommon 2.2.0, which this jar already requires: on an older library the import does not resolve, and a client fails every Custom UI document's parse, so every player disconnects at load. COMPLETE_QUESTis ziggfreed-common's moment.QuestService.fireCompletionHooksno longer dispatches it to either engine (it keeps only the bounty tail) andAchievementService.onQuestCompleteis gone. The mod registersQuestService.completionQualifier(a quest'sclassifyQuestType, a contract's board cadence) throughquestCompletionQualifier, so the quest achievement chains count exactly as before, once per collected quest.classifyQuestTypedelegates toQuestCadence.qualifier(), andQuestCompletionCreditTestfails if a dispatch of the kind comes back.- The statistics and every book entry point ride ziggfreed-common 2.2.0's rebuilt Objective Book. The library's book is a quest journal and an achievement Overview, Browse and Statistics view on its list-and-page kit (its 2.2.0 developer notes).
pages/achievement/MmoStatisticsis aLedgerSourcecontributed toLedgerContributions.STATISTICSat setup (fromMmoNpcQuestDeps): sections Combat, Gathering, Movement, Leveling, Stations and Playtime read fromStatisticsComponent, a row per stat with its top entry as meta (ui.stats.row.most, "Most: {0} x{1, number}", the name throughTargetNameResolver) and a typed total, and a page whose lead line is the total ("N in all") and whose breakdown runs up to 100 lines, each with its rank, localized name and count, and the item's picture where the breakdown is items; every number binds throughMsg.numorStatValueText. Gone:pages/achievement/MmoBookStatsPanelwithMmoBookStatsPanel.ui,StatCategorySection.uiandStatCard.ui;pages/skill/StatDetailPagewithStatDetailPage.uiandStatDetailRow.ui;MmoObjectiveBookDeps' side-panel painter and the stat route in itshandleExt; theui.statdetail.*keys andui.achievements.stats_titlewhere nothing reads them. Quest categories ship as data,Server/ZiggfreedCommon/QuestCategories/MMOSkillTree/{Main,Misc,Daily}.json, each naming the library's ownbook.quests.category.main,.miscor.dailyas itsTitleKey, and each achievement category underAchievementCategories/MMOSkillTree/gains anAccent.AchievementsCommand,QuestPageCommand,MmoQuestMenuOpenInteractionandClassHubPageopen the book throughObjectiveBookPages.open(or a destination) instead of building the page. A section shows once one of its stats is above zero; Blocks Placed and Items Picked Up get their top entry and a breakdown too, and a breakdown cut at 100 lines says so (ui.stats.page.top, "Top {0, number} of {1, number}"). New keys, in all nine locales:ui.stats.row.most,ui.stats.page.caption,ui.stats.page.total,ui.stats.page.top,ui.stats.page.rankandui.stats.empty. Tests:MmoStatisticsTest(a section shows when it has data, names localized, the breakdown sorted and capped, numbers typed), the category files' decode in the content tests, andMenuRetiredNavTestandMmoStylesRetiredTestfor the retired documents. - The skill reward ladder and the hub guide's previews are built on the library's kit, and the Skill Overview drops its tracked panel.
pages/skill/ItemRewardsPageandItemRewardsPage.uiare a list and a page inside$ZF.@ZigDecoratedFramewith Back: one level perZigLedgerRowrow (state and level; the Next row carries the progress bar the header used to), the chosen level's rewards as the page, and Collect in its action bar taking the whole level (the per-card claim buttons are gone); a reward's authored name colour gives way to one accent per row; a currency or boost reward line keeps its custom name and description as a hover tooltip (a kitDetailLinecarries its own tooltip text); the previous and next skill buttons hide on the Total ladder. The purepages/skill/SkillRewardLadderturns the tiers into rows and a tier into aDetailViewwith its claim plan (SkillRewardLadderTest);RewardCard.ui,RewardCardGrid.uiandRewardTierSeparator.uiare gone.ViewXpPagehas no right-hand pinned and tracked panel (#SidePanelandpages/skill/TrackedPanelare gone): in its 320 px column a tracked quest's compact kit row left its two-line title a few pixels, and the client's label layout threw on it, so a player's quests and pins live in the library's shared Quests and Achievements menus;TrackedAchievementRow.uiis gone, andRailPagesDryTestkeeps the kit's compact rows out of the jar's pages.OnboardingPage's previews read through the library'sQuestReaderandAchievementReaderand paint with the kit, so their status words and the hidden-achievement rule are the library's (OnboardingPreviewTest: a preview row equals the reader's row for the same quest or achievement);PreviewMasterDetail.uiandPreviewRow.uiare gone. New keysui.rewards.ladder.*, in all nine locales;ui.quests.status_*and the retired preview and tracked keys leave all nine locales.
- Titles from this mod's own achievements. The jar ships its first title assets,
Server/ZiggfreedCommon/Titles/MMOSkillTree/Mmo_*.json(twelve, each id named after the achievement that pays it, so rewording a title never touches a saved unlock), withtitle.,.name .flavorand.displayinmmoskilltree.lang. Each is paid through ziggfreed-common 2.2.0'sTitlereward kind in the achievement'sRewards.Auto, so the title unlocks with the achievement while its XP and boost rows stay inClaim: the five metas (combat_master,legendary_warrior,gathering_master,legendary_gatherer,movement_master), the five server-firsts below, anddeaths_t5/fall_damage_t5, whose 50,000 Block XP and 2x token give way to the title (both farm off one cliff). The per-skill first (Skill_Level_First, a stencil) pays ONE shared title,Mmo_Skill_Level_First: a title per skill would need a file per skill, and a skill a pack adds would pay one nobody shipped. The picker lists an unearned title for everyone, so the server-first flavors read as feats. - The server-first achievements are feats.
server_first_all_50/75/100, thestencil and_100_first bounty_server_firstauthor zc'sListing.Feat: true(the fold already reads it besideMeta.mmoskilltree.Feat) andScoringPoints: 0,CountsTowardTotal: false, sinceFeatchanges only where a row is listed. Their XP, boost-token and announcement rows are unchanged. quest_daily_t1-t5andquest_weekly_t1-t5are live again.Meta.mmoskilltree.FeatandLegacySince: "1.4.0"are dropped andScoringcarries the pre-1.4.0 points (daily 10 / 25 / 75 / 175 / 400, weekly 10 / 40 / 150 / 300 / 500), counted. How they count is unchanged: a board contract still qualifies DAILY or WEEKLY by its board's rotation and aReset: Dailyquest DAILY (QuestService.completionQualifier), and progress was filed under the same ids and criterion keys all along.quest_weekly_t1drops itsTextArgsfor "Complete your first weekly quest." (anddeaths_t1for "Die for the first time.").QuestAchievementAssetsTest.dailyAndWeeklyAreLiveLaddersreplacesdailyAndWeeklyAreRetiredFeats.- The elite-kill rows hide without MMO Mob Scaling.
rare_hunter,elite_slayer,legendary_slayer,first_legend_killand thelegendary_warriormeta author a top-levelRequires{"Factor": "hytale:mod_installed", "Param": "Ziggfreed:MmoMobScaling", "Min": 1}, whichFeatureLiftlifts onto the hide axis (MmoFeatures.availableanswers aGroup:Nameid through the contributed mod-installed reading). Only the companion puts a rarity on a kill, so without it the rows could never be earned. Their descriptions name its tiers, Rare, Epic and Legendary. - Two ladders gate on the feature they count. The twelve
Quests/Bounties/rows authorMeta.mmoskilltree.Feature ["Bounty"]and the threeSpellsword_*rows["Abilities"], theSpender_*pattern. - Adds achievement ladders for Excavation and nine weapon skills. Thirty-one new files under
Server/ZiggfreedCommon/Achievements/MMOSkillTree/.Gathering/Excavation/Excavation_T1.jsonto_T4.jsoncopy the Woodcutting ladder (Gathering/Woodcutting/, chainwoodcutting) rung for rung with the target swapped:BREAK_BLOCKonSoil,MatchModeCONTAINS, amounts 500 / 5,000 / 25,000 / 100,000, points 10 / 40 / 150 / 300,ClaimMmo_XpinEXCAVATION500 / 5,000 / 50,000 / 50,000 plus a 2.5xEXCAVATIONMmo_Boost_Tokenfor 120 minutes on T4, T3 and T4 announced; chainexcavation, subcategoryexcavation.Combat/Weapons/to_Xp_T1.json _T3.json, forDAGGERS,POLEARMS,STAVES,AXES,BLUNT,UNARMED(subcategorymelee),ARCHERY,ARTILLERY(ranged) andMAGIC(casting), copy thecombat_damageladder (Combat/Damage/Hard_Hitter,Powerhouse,Force_Of_Nature) withDEAL_DAMAGEswapped forGAIN_XPEXACTon the skill id: amounts 10,000 / 100,000 / 1,000,000, points 15 / 50 / 150,ClaimMmo_Xp500 / 2,500 / 50,000 plus a 2.0xMmo_Boost_Tokenfor 60 minutes on T3, T3 announced, every row in the rung's own skill wherecombat_damagepaysSWORDS; chainsdaggers_xp,polearms_xp,staves_xp,axes_xp,blunt_xp,unarmed_xp,archery_xp,artillery_xp,magic_xp. The criterion is skill XP because a kill or a hit does not say which weapon landed it. Swords keepscombat_killsandcombat_damageand gets no XP ladder; Archery'skills_skeletonand Magic'sspellsword(ABILITY_CAST) stay beside the new chains.Gathering_Mastergainsexcavation_t3andLegendary_Gatherergainsexcavation_t4among theirMetaChildren(their descriptions name the fifth rung; a meta already earned stays earned);Combat_MasterandLegendary_Warriorlist no weapon rung. The ladders add 2,435 points to the pool (500 for Excavation, 215 a weapon). No backfill:GAIN_XPaccumulates (ObjectiveType.isValueBasedis false) and is credited only whenSkillService's award path firesProgressEventswith the XP it actually added, and the engine'sselfHealre-reads onlySTAT_THRESHOLDcriteria, so a rung counts XP earned from 1.7.0 on and existing skill XP never seeds it. Forty-four en-US keys at the end ofmmoskilltree.lang, under the block comment naming these ladders:achievement.excavation.desc("Dig {0} Soil blocks."),achievement.excavation_t1-t4.title,achievement.announce.excavation_t3.titleand_t4.title, the sharedachievement.skill_xp.desc("Earn {1} {0} XP.",{0}the skill's name off@skill,{1}the amount), and per weaponachievement.-_xp_t1 t3.titleandachievement.announce.; plus_xp_t3.title achievement.category.gathering.excavationbeside its siblings, andAchievementCategories/MMOSkillTree/Gathering.jsonlistsexcavationafterwoodcutting. No test names the new files and none pins the shipped achievement count; the tests that walk every shipped achievement cover them:NewShapeContentCorpusTest(everyAuthoredAchievementDecodesAndFolds,noTwoAchievementFilesLandOnOneId),CommonAchievementFoldTest(aFileNameIsTheAchievementId,noTwoAchievementFilesShareAName,criteriaRenderInAuthoredOrderAndEveryNonCapstoneHasSome),DefaultContentLocalizationTest(everyDefaultAchievementResolvesTitleAndDesc,everyDefaultObjectiveAndCriterionRendersLocalized,shippedAchievementAssetsSayTextWithKeysOnly),ShippedCategoryPresentationTest.everySubcategoryThisModNamesHasANameKeyandShippedAchievementContentTest.everyShippedAchievementCategoryHasAPresentationAsset. - The Temple's Due asks 75 essence and its table pays for it.
Quests/MMOSkillTree/Temple/Temple_Tribute.jsonoffer_essenceAmount50 to 75 (the fibre and kill steps are unchanged).Lootables/Mmo_Temple_Offering.json: the guaranteed essence moves from 6-9 to 8-12; Iron Ingots (20%, 2-3) and a Greater Essence of Life (4%) join the Copper Ingots (35%), the boost (8%) and the Warden's Longsword (2%); aPooldraws one of ten vanilla Eternal seed bags on every claim, weighted 3 / 2 / 1 toward the cheaper crops; and a 1% roll, cuedRare_Find, pays the newLootables/Mmo_Temple_Relic.json, a one-pickPoolover the fourArmor_Steel_Ancient_*pieces (Rare, item level 30, no working recipe on 0.6.8, otherwise dropped by Zone 4's burnt skeletons). Every id was checked against the installed 0.6.8Assets.zip. The quest'sLootablerow authorsNameKeyreward.lootable.temple_offering("Temple Offering"), which zc's reward chips read in place of the generic "Loot Bundle", and both files' comments stop claiming the table returns more essence than the tribute takes. - Six quest steps word themselves.
objective.text.quest.kill_boars,.kill_trorks(Trork Ground and The Watch Below),.mine_stone,.chop_logsand.clear_rubbleare stepTextKeys, so the steps read plural instead of the singular role or block name the renderer prettifies.
Fixed
- A mastery card's costs drew over the rows under them.
MasteryNodeCard.uiis 384x448 (was 332, andMasteryOrDivider.ui's cell follows):#NodeBodytakes its content's height with a flex spacer under it,#CostRowis aTopcolumn,#NodeLockReasonssizes to its lines and#ButtonRowshows only beside a visible BUY, REFUND or CONTRIBUTE, where before a flex body with fixed rows below let three or more 200 px chips, centred byLeftCenterWrap, spill over#NodeStatusand the lock reasons.MasteryPagewidens the card's chips to the body's 344 px throughCostChipRenderer.setAnchor(the confirm panel keeps 200, its#ConfirmCostRownowLeftWrap), andui.mastery.contribute_progresstakes the amount line as{1}.CostRenderer.currencyAmountMsgreturns ZiggfreedCommon'sCurrencyText.amountOf, so a wallet that shipscurrency.(the mastery pack's Mastery Points, the bounty pack's Bounty Token) reads in its plural on every MMO price line; an unknown id keeps the raw fallback..amount - The temple guide's mastery pointer could name a trainer nobody ships.
Mmo_Hub_Intro'stemple_pointersmastery option gains theziggfreedcommon:quest_knownmeet_the_mastery_trainercondition the sawyer option already carried: aQuestStateNOT_STARTED cannot tell an untaken quest from an absent one. - The statistics' distances read in blocks.
MmoStatisticsbindsui.achievements.stats.distance_blocks({0, number} blocks) for every distance, where it switched todistance_kmpast a thousand; the sprint and swim achievements ask in blocks too.distance_kmanddistance_mleave en-US, and the other eight locales still carry them, unread. - Ten buttons clicked silently. Their
TextButtonStyles carried noSoundsleaf, in 1.6.2 as before it:RewardCard.ui's@ClaimButtonStyle(an item reward's Claim),SkillCard.ui's@TreeButtonStyleand@RewardsButtonStyle(a skill card's Skill Tree and Items),SkillResetCard.ui's@SelectButtonStyle(the skill reset page's Select),AbilityCard.ui's@CardSelectButtonStyle,@CardSelectButtonDisabledStyleand@CardMasteryButtonStyle(an ability card's Select and Mastery),AbilitySlotRow.ui's@ChangeButtonStyleand@UnbindButtonStyle(the Abilities page's Change and Unbind),BoostRow.ui's@ActivateButtonStyle(the Boosts page's Activate; the document now importsCommon.uias$C) andHeldItemStatPanel.ui's@HeldStatSourcesBtnStyle(the held-item panel's Stat Sources). Each names vanilla's$C.@ButtonSounds(the four card styles with the type scale, the ability card's three in an earlier UI cleanup this cycle, the last four after it), and the Java that repaints those buttons sets colours and text only, never a wholeStyle, so the sound holds through every repaint.RewardCard.uiitself goes with the skill reward ladder's rebuild (below), whose Collect is a kit action-bar button with its own sound. - Seven buff-bar numbers on the Stats page were still server-formatted.
ViewXpPage's XP boost, damage, lifesteal and combo percentages and the three flat bonuses went throughString.formatintoui.viewxp.buff_*_formatwhile the three capped stats beside them already rode as typed params, so those seven printed one server-side spelling to every locale and a negative read "+-5%". All seven now bind throughpages/render/StatValueText.signed(the sign and the unit in the nested value, the digits the client's), and the seven keys drop their authored+/%in all nine locales. The other number-bearing sites on that page (a tracked achievement's "3/5", an active boost's "2x" and time remaining) are not stat numbers and are unchanged. - An owner clamp axis removed from
ability-clamps.jsonkept its last merged value until a restart. The owner layer was merged INTO the effective clamp table, and a per-leaf merge can only tighten, so a removed leaf had nowhere to fall back to.ParamClampTablenow keeps the asset layer (defaults < pack) apart from the effective table andfoldOwnerrebuilds the effective table from it on every owner fold, so/mmoconfig reloadand the page's Reset both bring the shipped value back at once. - An under-supplied typed
{N, number}token survived literally on screen where a bare{N}was stripped:LocalizationConfig.applyArgsfilled both shapes but stripped only the bare one. Both are stripped now (ApplyArgsTest). - A capped-defense player took exactly zero damage from an ordinary mob, permanently.
event/CombatDamageEventSystem.applyDefenseReductionreturnedbaseDamage * (1.0 - reduction)as a raw float with no floor, andDamageSystems$ApplyDamagethen appliesMath.round, so anything under half a point lands as zero while the swing still counts as a completed hit. The fraction gets small fast: the MMO's own channel capped at 0.9 until this release (ModifierClamps/DefenseReduction.json, 0.58 now, see Breaking changes above), the engine'sArmorDamageReductionmultiplies by(1 - armorSum)before it for a 94% total, and a 5-damage bite scaled by a mid-difficulty mob's 1.52 outgoing multiplier computes5 * 1.52 * 0.06 = 0.456.keepLandingHitsLandingfloors this reduction's own output at one point when the arriving hit was already worth that, exactly mirroringMobScalingDamageFilter's fix. The engine'sArmorDamageReductionhas ALREADY run by then (it declares no dependencies and no priority, the Filter group orders its members by registration, and the core registers before any plugin), so this floor is the LAST reduction in the chain and a hit still worth a point after armor lands for at least a point.applyFallDamageReductiondeliberately does NOT get the floor - a build reaching full fall reduction is meant to land for zero and trigger the safe-landing / dangerous-fall-survived XP, so its uncapped arithmetic is left exactly as it was. Found while measuring player power against mob difficulty, which also established that this same cap, at its old 0.9, engaging at skill-tier milestone L40 was the single cliff behind BOTH reported symptoms: mobs that cannot be killed below it and mobs that cannot win above it (at 0.58 the tree crosses it at L30 and the step is 1.65x, not 4.4x).
- The
MaxHealthpower pillar was dead on every default server.Modes.Balancednever named it and every builtin registersdefaultWeight0.0, whichPowerLevelService.computeWithtreats as "skip the provider", so the shippedBaseHealth 100 / Factor 10tuning did nothing;AchievementsunderCombatOnlyhad the same latent shape. Wired in as part of the retune above, and the class of bug now reports itself:PowerLevelValidatorWARNsOMITTED_PILLARfor a registered pillar the active mode never names and that brings no default weight,INERT_PILLAR(INFO) is reserved for a tuned pillar the mode explicitly switches off with0.0, and every shipped mode names all six builtins (PowerLevelValidatorTestholds that structurally). - The cached power could outlive a change of the player's health maximum. Wiring the
MaxHealthpillar in made power depend on a LIVE value no component mutation announces: gear, per-stack item enhancement and the nativeUtilitystats ziggfreed-common'sEquipStatBridgeapplies on a held / armor / utility change, a class change (ClassService), a potion or any engine effect, all move theEntityStatMapmaximum, and only skill, claim, mastery, ability and achievement mutations flipped the cache's dirty bit. Rather than an invalidation at each of those sites (and every one added later), the memo is fingerprinted on the live maximum at the ONE read:power/PowerLevelCache(the three transient fields it replaces onSkillComponent, plus the fingerprint) is valid while its dirty bit is clear, its config epoch is current AND the health maximumBuiltinPowerPillars.liveMaxHealthreads now equals the one it was computed against; a known maximum that differs is a miss, an unknown one (no store in hand) never invalidates, and a memo taken against an unknown maximum is replaced by the first real read. One stat read per power read, which a recompute did anyway, so the per-region-cross read the mob-scaling companion makes stays O(1). The ready-time drop this cycle first added is gone again: the fingerprint covers it. The config epoch the memo is pinned to is the SUM of every generation counter the fold reads off live config, each of which only grows, so any one of them moving is an epoch no memo was computed under: the power settings generation (PowerLevelService.setSettings), the pillar registry generation (register/unregister, so a sibling mod's pillar registered after players are online folds in on the next read), theModifierClampsfold generation (ParamClampTable.generation(), bumped by every fold; the StatRewards pillar holds a saturated stat at a clamp ceiling), the skill-tree generation (SkillTreeConfig.generation(), bumped by every load and every save; the StatRewards pillar walks the live trees), the skill-roster generation (SkillRegistry.rosterGeneration(); the combat pillar walks the live COMBAT category and the StatRewards pillar every skill) and the leveling-curve generation (skill/LevelCurve.generation(), counted the moment a curve setting differs from the table in force; a combat level is XP read through that curve)./mmoconfig reloadmoves the last four at once and touches no power setting, which is why the epoch has to see them; a reload, a refold, a tree edit, a custom-skill registration or a curve change now lapses every player's memo at once, for a handful of counter reads per power read. The leveling curve itself moved out ofSkillComponentintoskill/LevelCurve(the component'scalculateLevel/getXpForLeveldelegate to it), which is where its generation lives, andSkillComponent.addByNamedrops the memo the waysetXpByNamealready did, because the API'saddXpreaches it with no level-up fan-out afterwards. - The three fail-closed fallbacks MIRROR the shipped assets.
CombatDamageEventSystem.DEFENSE_REDUCTION_CAP_FALLBACK(0.9, now 0.58) andSchoolResistReconciler.MAX_PERCENT(90, now 30) stood in for a missing clamp asset with exactly the ceilings this release lowers, so a failed asset load would have reverted the server to the old behaviour without a word. Each constant now carries the shippedMaxand a comment saying the asset is the authority, and the newSkillTreeService.CRIT_CHANCE_CAP_FALLBACKfollows the same rule from the start;DefenseReductionClampAssetTest,SchoolResistReconcilerTestandCritChanceClampAssetTesthold each equal to the file it mirrors (read, not pinned) and prove the fallback never leaks while an asset is loaded. - A capped stat says so wherever its sum is shown. The stat-sources page (
StatBreakdownPage: rail totals, section totals, the drill-in grand total) and the held-item panel showed the SUM of claimed picks ("Crit % +221%" for Daggers) with nothing saying the effective chance is capped.pages/render/CappedStatTextis the one renderer for such a total: once the sum has reached its key's ceiling the ceiling follows in parentheses (ui.statsource.value_capped, nine locales, the total AND the ceiling bound as typed percent numbers so each client formats both), forCRIT_PCT,BLOCK_PCTandFALL_PCTalike, and under the cap the value is typed the same way through the newpages/render/StatValueText(ui.statsource.value_pct/_negative/_bareand thevalue_flattriple, nine locales: the sign and the unit in the key, the number a typed param, so on the stat-sources page, the held-item panel and the Stats page's buff bar every stat number is a typed param;StatSourceAggregator.formatStatValueis deleted). The stat-sources skill dropdown, a String-only engine sink, reads the same total throughCappedStatText.plain, the same key resolved server-side in the default language (LocalizationConfig.applyArgsfills a{N, number}token there like a{N}), so it carries the cap note too. The Stats page's block, crit and fall labels paint through it as well (fed the summed LIVE channel, so the bar agrees with the held-item panel beside it while nothing else feeds the stat and differs by the gear once it does; theBLK/CRITlang keys drop their own+{0}%frame for it, andFALLkeeps its minus,FALL -{0}over the bare number, since a fall reduction reads as less damage taken), and the value slots on all three surfaces widened to fit. The ceilings themselves moved out of the power pillar intoreward/StatCeilings(of/usable/reached), the ONE per-stat table the pillar, the two pages and the Stats page hint read, each entry still the live clamp seam (defenseReductionCeiling(),criticalChanceCeiling(),FULL_FALL_REDUCTION). It has NO arm forLUCK_PCT, by design: luck stays uncapped, andStatCeilingsTestpins that. - The Stats page crit hint fired for every combat-equipped player when an owner authored a
CritChanceceiling of zero, because a capped read equals a zero ceiling trivially. It now tests the SUMMED chance before the cap (SkillTreeService.summedCriticalChance, whatresolveCriticalChancecaps) throughStatCeilings.reached: "you have invested up to the ceiling", which a player with nothing invested has not. PowerLevelService.unregister(id)removes a registered pillar (copy-on-write likeregister), for a mod that unloads its pillar and for the tests, which used to park neutralised fixture pillars in the process-wide registry and madePowerLevelValidatorTestfilter around them; both now clean up and the validator test asserts the registry holds exactly the six builtins.- Three public-facing statements about mitigation were wrong and are corrected.
ModifierClamps/SchoolResist.jsonsaid school resist was a third multiplicative layer cut after defense and armor; the engine'sDamageSystems.addResistanceModifiersFromEntityEffectssums an effect'sDamageResistanceinto the SAME per-cause bucket as worn armor and takes one(1 - sum), so it now says that, with the elemental arithmetic (0.42 x 0.7 = 0.29).ModifierClamps/DefenseReduction.jsonsaid 0.58 composes to about a quarter of the hit; true for Physical and Projectile, which vanilla armor resists (115 of 115 pieces each), but nothing in the vanilla set resists Elemental, so an elemental hit sits at 0.42 and the comment says so.CombatDamageEventSystem.keepLandingHitsLanding's javadoc said the cap was 90% and that the engine's armor ran afterwards; both wrong (the cap is the asset'sMax, and armor runs first, see above). The same ordering sentence in this file's own zero-damage entry above, in the patch note, inCURSEFORGE.md's copy of it and inCombatDamageEventSystem's own Filter-stage comment, and the "90% defense cap" / "0.9 cap" wording inSkillTreeService(twice),MasteryService,SchoolResistReconciler, theskilltree/router, therewardsskill andCONTENT_PACKS.md, are corrected with it; a grep for the "90% cap" / "0.9 cap" wording over the main source, the assets, the skills and the docs finds no survivor (the one hit left is a test comment describing its own 0.9 fixture cap). CombatTextUpdateis built field by field, so crit and level-up numbers survive the next server update. The generated packet class gains a trailing nullableColorfield in the build now on the shared source'sreleasepatchline, and the two-argumentCombatTextUpdate(float, String)constructorfeedback/ColoredCombatTextServicecalled goes away with it, which would have made every crit and level-up float aNoSuchMethodErrorthe day that server shipped. Both the installed 0.6.8 jar and the incoming one carry the no-argument constructor and the publichitAngleDeg/textfields, so one privatecombatText(angle, text)helper builds the packet through those and both call sites use it; a field the newer build adds keeps its own default. No reflection, no version branch, no deprecated API.- A family-wide audit of the generated-Protocol constructors the family calls found no other such break.
MapMarkeralso gains parameters, butziggfreed-common'sworldmap/WorldMapMarkers.markerbuilds markers through the engine's ownMapMarkerBuilder, which only gains methods, so the library needs no change.FormattedMessage,PageandParamValuechanged without touching their constructors. - When that update lands, the same class also carries the per-cause damage-text colour the engine now resolves itself (
DamageCause.resolveDamageTextColor), which is the thingColoredCombatTextServicecurrently achieves by swapping a per-viewerCombatTextcomponent and restoring it. That is a simplification for the release that follows, not this one. - Shadowstep did not blink, and a blink fired mid-flight threw.
interaction/MmoTeleportInteractionbuilt its component with the rawnew Teleport(target, rotation)constructor and added it withstore.addComponent, the only structural component write in the wholeinteraction/package that bypassed the chain's ownCommandBuffer. Three corrections, all of them the first-party shape (TeleportInstanceInteractionandHubPortalInteractionare theSimpleInstantInteractionexemplars): the component is built throughTeleport.createForPlayer(position, rotation), which routes the full look rotation to the HEAD and only its yaw to the body so a caster who blinked while looking up or down lands upright instead of pitched at that angle; it is added throughcommandBuffer.addComponent; and the caster's archetype is checked for an outstandingTeleportorPendingTeleportfirst, because the engine throwsIllegalArgumentExceptionon a duplicate component andPendingTeleportsurvives until the client acks the previous blink. A blink refused for that reason resolvesFailedand refunds through the existingMmoCastFrame.failCastseam rather than dropping an exception into the chain, which had been taking the invulnerability window and the aggro drop down with it.createForPlayer,CommandBuffer.getArchetype,Archetype.containsandPendingTeleportwere each confirmed against the installed 0.6.8 jar before use. - Smoke Bomb did not drop aggro, which is the one thing its name and its smoke cloud promise. The ability's whole body was the two-beat armed strike (1.8x, then a 1.5x re-arm on the hit) plus the cosmetic
MMO_SmokeBombAura, so a player reading "Vanish in a billow of choking smoke" and a status effect called Smoke Screen got a damage buff and nothing else.Server/MMOSkillTree/Abilities/Daggers/Smoke_Bomb.jsongains anMmoAggroSuppressnode on the cloud's ownNext, atRadius12 andDurationMs3000: tight enough to stay a self-extraction tool rather than a camp-wide aggro wipe, and deliberately shorter than both the 5 s cloud and the 5 s armed window so the opening has to be spent rather than waited out. The tooltip picks the line up from the node, so no authored copy changed. Aggro suppression needs an AggroAPI provider on the server; with none present the node is a silent no-op and the cloud and the armed strikes are unaffected. - A skill-tree claim card hid the ability's hard casting gate.
ability/catalog/ChainDescriptionRenderer.renderCardMsgreturned the authored flavor sentence alone whenever an ability declared one, socastingRequirementLinesreached the hover tooltip and the Abilities tab but never the card. Mark of Death authorsCasting.SelfHp.Max0.5 and reads on its card as "Brand a target for execution", with nothing saying the cast is refused above half health; a tier pick is permanent, and on a server with skill reset switched off a player who claimed on the flavor alone could not undo it. The card now appends the same gate lines the full tooltip ends with, in the same position under the cooldown/cost envelope, so the two surfaces read in the same order.
- Display numbers on the surfaces this round touched bind typed, so each player's own client writes their digits.
RepoHygieneTest's rule 5b fails the build on an unmarkedString.format/NumberFormatter/NumberFormat/SkillDisplayUtil.format*call underpages/,ui/,i18n/,feedback/,event/andplayer/, and its rule 5c fails a line there that spells a number byString.valueOf, a boxedtoString, or a literal concatenated into a rawMessage, a translation param or a.Textsink. Both scan those roots line by line, so a number spelt in another package and handed in as a String, or parked in a local first, passes them; the test's javadoc says so, and the fix for such a site is at the point the number is spelt, not a wrap at the sink. Retyped: thereward.short.*chip keys and thereward.format.*/reward.desc.*sentences (Messages,RewardChipFormatter), the mastery card's modifier lines (MasteryModifierRenderer), the quest and achievement objective counts (ObjectiveTextRendererbinds the amount as a nestedMsg.num, so it groups whichever family's key it lands in: a pack's authored key, a kind file's, this mod's convention key or the shared vocabulary's, which carry a bare{0}), the tier and track counts (CountText), the boost multipliers, the boost and ability cooldown spans (DurationTextovertime.span.*), the class page's multipliers, the statistics' distance and playtime figures (then the achievement book's cards, nowMmoStatisticsrows, below), the combat notifications, the XP-token grants, the rested-XP notice, the item-requirement line behind a refused swing or block break (newpages/render/RequirementText, the same line the admin item-requirements editor lists), the boost activation refusal (XpBoostService.activateBoost/activateTokenreturn a client-resolvedMessage; the cooldown ridesDurationText, the counts bind onboost.limit.*, and three newboost.refused.*keys replace the English literals), the mastery lock line and toasts (MasteryService'sPurchaseResult/RespecResult/ContributeResultcarry aMessage; the amount slots ofui.mastery.locked_need_more,toast.contributed,toast.refundedandtoast.refunded_remainingare{N, number}, andui.mastery.toast.refunded_trackis new), the leaderboard ranks and level breakdown (newui.leaderboard.rank/rank_none), the Stats page's card XP line and the skill wizard's reward value column (Messages.rewardValueMsg/rewardValueWithTargetMsg, the value half of the sentence renderers made public). Marked// NUMBER-OK:with the reason on the line: the compact k/M XP totals, the admin editors' echo of a config value beside the field that edits it, the clock's zero-padded seconds, a hex colour, and the String-only console helpers. Still server-formatted, on pages this round did not touch, and pinned by exact line count in the test'sSPELT_NUMBER_DEBTratchet so the count can only fall: 37 lines across eight admin pages, two renderers and the DB schema manager, plusSkillTreeOverridesPage's reward value column (a helper-returned String the rule does not see), the ability summary sentences underinteraction/and the station summaries underintegration/stations/, both outside the scanned roots.
- A clamp read could land inside a fold and take the fail-closed fallback.
ParamClampTable.fold/foldOwnercleared the effective map and refilled it whilegetread unsynchronized, so a damage hit, an ability cast or a power read arriving in that window saw a miss for a key authored before and after, and every owner save from the Modifier Clamps page and every/mmoconfig reloadopened it. Both layers now live in one immutable snapshot behind avolatilefield, built in full and published in one assignment (the shape the mob-scaling companion'sDifficultyConfiguses for its index), so a reader sees the old table or the new one and never a half-filled one;ParamClampTableSnapshotTestraces a reader against four thousand folds and holds the miss count at zero, and proves a map handed out before a fold stays complete after it. - The Modifier Clamps page carried a copy of a balance figure. Its defense-reduction sentence multiplied the live ceiling by a hardcoded top-armor resistance to print "about 25% of a physical hit", a number that belongs to the game's armor set and to the asset's own comment, not to Java. The sentence now binds the live ceiling and the elemental share and says in words that a physical hit lands lighter still once worn armor has taken its cut; its school-resist line reads
SchoolResistReconciler.maxPercent(), the same seam the hit uses, instead of a second fallback of its own; and the unreadeffectiveparameter onaboutForis gone, so the sentence and the row under it read the same table. - A lucky hand harvest could cost the player the harvest itself, and an extra that did not fit the backpack vanished. The game fires its pickup event before it gives the harvested stack, and the bonus copy was added to the backpack inside that event, so with a nearly full bag the copy took the room the harvest needed; whatever of the copy did not fit was simply lost, and the "Lucky!" note counted the whole stack either way. The bonus is now rolled after the harvest lands. The copy goes where the player's own pickup settings route that item, topping up a partial stack first, so it stacks with the harvest; only what does not fit drops as one pile at their feet, and the note counts only what landed. The odds and the amount are unchanged.
- A slain creature whose facing could not be read dropped no bonus loot. The corpse drop needed both a position and a head rotation and handed over nothing without either. It now lands at the corpse as the game drops a dying creature's own loot, facing nowhere in particular when there is no head rotation; only a corpse with no position still drops nothing, now with a warning.
- The bonus mob-drop note ignored the player's luck-drop notification setting, and read in English in seven languages. It now follows the same setting as a lucky find, and
notify.mob_dropandnotify.mob_drop.hintare translated in all nine locales. - A kill row whose
Notify.Colorcould not be read showed no note at all. The colour threw and the note was skipped with a warning; it now falls back to the moment's own colour.
Technical
- Every reward the jar's quests, board contracts and point milestones author is collected. A
Claimrow, neverAuto, except an achievement's title or a boost token, which may land on unlock. Converted:Getting_Started(its two items; the guide's welcome and its skip finish it throughQuestService.complete, whose forced completion pays both lists on the spot throughQuestEngine.tryForceComplete, so a new player still holds the scroll and the book at once, andcraft_starter_tools, which requires it claimed, is still accepted in the same beat), the fourMmo_Hallows_Eve_Daily_*contracts (collected at Old Jack's board, which a contract'sClaimparks at: a job finished in the event's last hours waits there until the board posts again), and the fivePoints_*milestones (eachMmo_Xprow moves toClaim, collected throughAchievementService.claimMilestoneRewards; eachMmo_Boost_TokenstaysAuto). The titles this release adds pay fromAuto, the exception.ShippedQuestContractTestandHallowsEveJarContentTestassert theClaimbucket where they assertedAuto. - The content audit's prose pass changes en-US only. Achievement names and descriptions, the onboarding tour, fifty
ability.*.desclines, the guide's lines, the quest toasts (notify.quest_ready_to_claim,.contract_ready_to_claim,.quest_inventory_full,ui.rewards.inventory_full,.claim_success, which now say collect), the capes, the ability-slot items and the XP token sizes. Two keys lose their{0},achievement.quest_weekly_t1.descandachievement.deaths_t1.desc, so their eight translations are removed for the lang wave rather than left failingLangFileIntegrityTest's placeholder parity. The token sizes' other locales come fromtools/i18n/generate_xptoken_langs.py, whose per-localeadvwords for Piece, Token, Grand_Tome and Relic need the same re-rank.objective.text.calendar_attendedand.anykeep their keys with ziggfreed-common's new wording ("Take part in {1}"): the renderer's shared rung reads the progression vocabulary, not the calendar file that holdsobjective.calendar_attended, so without them the step falls to the generic line, whichObjectiveKindVocabularyTestfails. - The MMO registers only what it adds to the library's conversations. ZiggfreedCommon 2.2.0 now ships a default quest binding for conversations (
RuntimeDialogueQuests), the completion-conversation host, theHasOfferableQuestscondition and the TALK_TO_NPC credit, soMmoDialogueQuestsextends the library's binding and declares onlysubject(ctx)(the payload self-heal),MmoQuestDialogueHostandMmoTalkCreditare deleted,MmoDialogueConditionsno longer registersHasOfferableQuests, and the talk statistic ridesMmoMomentReactionson the library'sTALK_TO_NPCmoment (one count per conversation). A quest the pool no longer carries reads its stored state in the library itself, so the MMO's reader override is gone.MmoRegistersOnlyWhatItAddsTestfails a re-registration of any of them. Template_Mmo_QuestGiveris now a Variant of Ziggfreed Common'sTemplate_Zig_QuestGiver, which carries the whole body; it re-declares the same five parameters with the MMO's defaults and forwards them, so a pack Variant still modifies exactlyAppearance,NameTranslationKey,Weapons,OffHandandDefaultOffHandSlot. Quest givers built on it now show the library's press-F hint key, whose English text is unchanged.MmoRoleTemplateParameterTestreads the library template from disk and checks each Variant against the parameters of the role it references.NewShapeContentCorpusTestwalks every gear set a pack or RPG Stations ships (Server/ZiggfreedCommon/GearSets), decoding through the library's realGearSetAsset.CODECwithParentresolved and holding each to the validator's shape checks; no two of those files may land on one id (the store keys a set by its bare file name, so one would silently replace the other).ObjectiveKindVocabularyTestchecks the new kind file against the enum, andMmoMomentReactionsTestcovers the new kind's payload gate.- A reward kind that pays only inside a pass carrying its collector (Ziggfreed Common's
CollectingRewardKind; RPG Stations 1.1.0 registers its three station kinds that way,rpgstations:output_itemsamong them, and the library's ownMoment_Itemis one) is not payable at a quest, achievement, shop or dialogue site.RewardSpecs.isPayableanswers false for one (RewardSpecs.isPassOnly),ValidationChecks.checkRewardsreports it under the library'sPASS_ONLY_REWARD_KINDcode with a message that says what it needs instead of calling it unknown, andRAISE_TO_ERRORraises that code besideUNKNOWN_REWARD_KIND, so a folded quest or achievement authoring one is exactly as loud as when its kind still read as unknown (PassOnlyRewardKindAuditTest). The loot-table audit needs no change: the station kinds are known now, so the 13LOOT_UNKNOWN_REWARD_KINDwarnings the stations pack's tables raised at boot are gone. - A lucky block break and a kill's bonus drops hand their items and native drop lists over through Ziggfreed Common's ground-spill preset (
GroundSpillSinks) instead of two hand-built sink pairs:LuckUtilspills at the broken block and counts every spilled stack as found, as before, and the bonus-drop pass inMmoMomentReactions.handleMobDropsspills on the library's corpse ground,GroundSpillSinks.atEntity(the corpse's position read at drop time, lifted one block and facing as the engine drops a dying entity's own loot, identity facing without aHeadRotation), counting what that ground answers it dropped; the MMO's ownspillAtCorpse, which dropped nothing without a head rotation, is deleted. A drop list nothing claims is now warned once by the library's native roll rather than by a second check of the MMO's own. The corpse facing is the one change a player can see (see Fixed). - The bonus-loot changes landed pin-first. Pure seams choose a bonus note (
LuckUtil.noteSpecover each moment'sNoteDefaults, answering aBonusNoterecord), the kill row's chance gate (MmoMomentReactions.killRowFires), the pickup gate (pickupBonusConsidered, which now asks that the pickup PAID XP,xpAwarded > 0, where it read the result's presence) and the harvest pass (LuckUtil.pickupBonusPass,harvestOf), pinned byBonusNoteTest,MmoMomentReactionsTestandPickupBonusPassTest.build.gradlegains ziggfreed-common's tag split:engineItemTestruns the tests taggedengine-items(they build a real engineItemStack) under the engine's log manager, the plaintesttask excludes them, andcheckruns both. BonusDropsAsset.KindgainsPICKUP_ITEM, appended afterKILL_MOBso fold and listing order never move. Each constant declares whether an unwrittenChanceborrows the player's luck (unwrittenChanceBorrowsLuck,unauthoredChance), and the collectors its pass carries are ziggfreed-common'sBonusMoment.carries()(MomentItemsfor PickupItem, nothing for the other two), which the shared audit reads; a pass carriesMomentItemsonly when its site hands it the harvest.BonusDropsConfig.onPickupItemanswers only PickupItem rows.Entry.chanceFractionreads a NaN result as no chance (a min/max clamp alone kept NaN, which the kill gate read as firing).When.Kind's editor schema lists the three moments, andNotify.Colordrops its one declared#ffaa00and documents each moment's default instead.BonusDropsValidatoraudits each row's inline rolls through ziggfreed-common'sBonusRowAudit, which hands the four-argumentLootableValidator.auditRefits moment'scarries()set, so aMoment_Itemoutside a PickupItem row warnsPASS_ONLY_REWARD_KIND; a table the row names stays on the silent table path (BonusDropsAssetTest,BonusDropsValidatorTest).mmoskilltree:moment_skill_luck(content/gate/MmoFactorContributions) answers 100 xSkillTreeService.resolveLuckChancefor the skill aMomentSkillpayload names (content/gate/MomentSkill, riding theFactorContext's one payload slot), and is unanswered without that payload or a live subject (MomentSkillLuckFactorTest). The block and harvest passes build one moment context (LuckUtil.momentContext: the store, the player, the harvested stack as the item, the skill); a kill's context carries no payload. A context holdingMomentSkillcannot also hold aGateSubject, so the gate vocabulary's saved-record overrides fall through to their live reads there, as for a context with no payload.- The harvest bonus is one deferred pass.
MmoMomentReactions.onPickuppays XP inside the producer's dispatch and, when it paid,LuckUtil.tryLuckBonusForPickupqueues ONEWorld.executetask (a world refusing tasks while it shuts down means no bonus), because the engine firesInteractivelyPickupItemEventbefore it gives the stack. The task re-reads the event (harvestOf: a cancelled or emptied pickup pays nothing, and the stack copied is the event's FINAL one, whatever another plugin swapped in), rolls the row's chance against the moment context, and rolls its loot with aMomentItemscollector over that stack layered on the subject (subject.withFacets). The row's own items and the resolved copies hand over through ONEGroundSpillSinkspreset,pickupRoutedFirst(player)(the library'sGroundSpillSinks.pickupRouted, the engine's ownPlayer.giveItemrouting with partial stacks filled) and then one pile at the feet (NativeLootService.spawnAtFeet); the note counts the pass's landed items plus the landed copies. The event is never replaced or fired again,tryLuckBonusByNameis deleted, and the pass's source id ispickup:. - One bonus note for all three moments:
LuckUtil.notifyBonus(kind, ...)sends throughMmoNotifyunderNotificationCategory.LUCK_DROP, each moment'sNoteDefaultsnaming its keys, colour, throttle and feed subject. BreakBlock and PickupItem sharenotify.luck_drop*andNotificationThrottle.LUCK_BONUS; KillMob speaksnotify.mob_drop*under the newNotificationThrottle.MOB_DROPon its own feed tag (subjectmob_drop), andkillNoteCountcounts at least one item, since a kill row's note fires for a command or a reward too. An authored colour that does not decode falls back to the moment's colour.sendDropNotificationandkillNoteare deleted.notify.mob_drop.stackedis new, and both stacked keys bind the count as a typed{0, number}in nine locales. - Adds a deprecation gate: the build fails on a call to a deprecated API, not just one marked for removal.
gradle/deprecation-gate.gradlecompiles every source set a second time with-Xlint:deprecationand failscheckon any deprecation warning whose line, or the line above it, carries no// DEPRECATION-KEPT:comment; the kept calls each say why at the call site.compileJavakeeps-Xlint:removal -Werroras it was, and the api module compiles with-Xlint:removal -Werrortoo. Every sibling mod's build applies a byte-identical copy of the file, andRepoHygieneTestfails a copy that drifts, a build script with the removal flag that skips the gate, and any@SuppressWarningsnamingdeprecationorremoval(the oldDEPRECATION-OKexception is gone). RepoHygieneTestholds the type scale over every shipped.uiin the jar and every present sibling mod,ZigType.uiitself excepted.everySmallUiTextSizeIsATypeScaleStepfails aFontSizeunder 15 written as a number, and anyMinShrinkTextToFitFontSizewritten as a number, so nothing drifts under the floor and one edit resizes every page (a heading or hero size of 15 or more may stay a number where the scale has no step).theMicroTypeStepStaysInTheExemptHudBoxesallows@ZigFontMicroin four HUD documents only:ZigHudBars.ui,ZigHudBarGrid.ui,MmoSkillTreeAbilityCooldown.uiandMmoscalingMobInspector.ui.everyUiCrossDocumentReferenceResolvesfails a$Alias.@Namewhose document declares no$Aliasimport or, when the import is a family document, whose target defines no@Name; the client merges every mod'sCommon/UI/Custominto one tree, so the check reads them as one, and a reference into a vanilla document is checked for its import only. One unresolved reference fails the client's parse and disconnects every player at load, which no server-side check sees.MMOSkillTreeAPI.getSkillTypes()is marked@Deprecated, like the eightSkillTypemethods beside it: skills are string ids, so use theString skillIdmethods, andgetAllLevelsByName(every skill id, built-in and custom) orgetAllXpByName. Its signature is unchanged.StatSourceAggregator.sumAllScopesTreeOnlysums through a label-free walk (forEachClaimedTreeReward) shared with the breakdown page's row collector, so the power pillar no longer bakes a localized row label per claimed reward it only ever adds up, and the SHIPPED StatRewards pillar runs in a server-less JVM.PowerLevelServiceTestdrives that shipped pillar over picks claimed on a fixture tree (value x factor for an unbounded key, ceiling x factor past the cap, the aggregator read beside it, and a tree edit lapsing the memo through the pillar itself), replacing a drift check that compared an empty component's zero with zero and a tree-edit proof that substituted a fixture pillar.- The
hytaleServerVersionpin reads0.6.8, the version the installed jar reports, and the shared-source checkout moves to the 2026-09-24 sync. The whole family recompiles clean against 0.6.8 with tests green, and the headless boot check passes with zero family SEVERE. - The shared source's
releasesync can be AHEAD of the released server jar. The 2026-09-24 checkout describes a build that had not shipped, soEntityEffect.OutgoingDamage,DamageSystems.ScaleOutgoingDamageFromEntityEffects,DamageCause.resolveDamageTextColorand theIgnoreDisableNPCIncomingDamagecomponent are all absent from 0.6.8 (javapagainst the installed jar is what settles it). A new engine surface read from that tree is not available to shipped code until the build ships, and the same holds for ASSETS: the tree carriesCrush,Earth,Lightning,WaterandWinddamage causes that 0.6.8'sAssets.zipdoes not. This jar's ownServer/Entity/Damage/{Lightning,Water,Arcane,Void,Life}.jsonare deliberate early support for that vocabulary;Crushhas none yet, so nothing may key a resistance on it until either this jar adds one or the update lands. - The achievement fold reads ziggfreed-common's
Listing.FeatandListing.LegacySincebeside this mod's ownMeta.mmoskilltree.FeatandLegacySince(either marks a feat; the meta block's version wins), and leaves an achievement the library mints for one yearly occurrence of a calendar event to the library's own layer (CommonAchievementFold.foldsHere), whose availability and feat flag are live readings this mod's model would freeze. - Bonus drops run on ZiggfreedCommon's bonus-row engine.
BonusDropsConfigfolds and matches through zc'sBonusTable, reads a row's percentChancethroughBonusChanceand resolves its rolls throughBonusRolls; the break, harvest and kill passes run throughBonusPasses, andBonusDropsValidatorthroughBonusRowAuditwith its releasedBONUS_DROP_*codes. The MMO keeps its layer: the XP gate, the luck default andmoment_skill_luck, the notes,bonus-drops.jsonandmob-drop-commands.json; every released surface reads the same rows. The kill path's reward subject carries the killer's skills, as the break and harvest paths' do. A cue toast on any of the three follows no notification setting: the subject no longer answersFeedbackAudience(see the Settings entry), andRare_Findcarries noToast.PlayerLevel.BonusDropsConfig.patternedEntries(Kind)is gone:table()hands over the folded table (zc'sBonusTable) for the shared audit and any reader that speaks the library's moments, andentries()still lists every folded row. Tests:BonusDropsRebaseTest,KillBonusPassTest,KillSubjectTest, and the released bonus suite unchanged. DamageUtil.isCausereads the resolved cause only. The fallback toDamage.EnvironmentSource.getType()is gone: Update 7 removes that method (a 0.6.8-built jar would throwNoSuchMethodErrorpastisCause'scatch (Exception)there), and the branch was dead on both lines for its two callers, the fall checks inCombatDamageEventSystemandCombatXpEventSystem. Test:DamageUtilTest.
[1.6.2] - 2026-09-15
A hotfix over 1.6.1: the empty bounty boards, the gear-namespace mismatch in the stat taxonomy, and the warning storm at boot. The first and the third are ZiggfreedCommon 2.1.1 fixes this version rides.
Breaking changes
- Requires ZiggfreedCommon 2.1.1 or newer.
REQUIRED_ZIGGFREED_COMMONand theziggfreedCommonVersionpin both read2.1.1; the pin feeds the manifest's^2.1.1floor, which is what the engine enforces (it drops the mod at load and names the version it wanted, the server itself still boots; the in-jar probe only catches a missing library). The board fix is the library's, so the two jars move together.
Fixed
- Every bounty board rendered its empty state. ZiggfreedCommon has published the folded contracts (
CommerceCatalogs.publishBounties, off the Bounties load event) and the shared quest fold (ProgressionDefaults.publishAssetContent) under the SAME owner name since 2.0.0, and a publish replaces that owner's whole layer, so whichever ran second wiped the other. Before 1.6.1 the shared fold ran first and lost, invisibly, because this jar published its own quest layer then; 1.6.1 moved the shared publish onto the runtime build, so it ran last and the contracts were what vanished: the 2026-09-12 boot log readsquests=67 (ziggfreedcommon 67)with 88 contract files loaded, andZigBoardPageskips every contract the quest engine cannot name. 2.1.1 publishes the contracts as a named SLICE of the library's layer (ProgressionRuntime.publishQuests(owner, slice, layer), additive; the two-arg call is the default slice), so each fold replaces only its own slice on a reload. Nothing changes in this jar beyond the floor. - Enhanced gear was filed under Other, and a DOT's
GearEnhancement: falseopt-out removed nothing. The shared equip bridge is installed asEquipStatBridge.install("ziggfreedcommon")and writesziggfreedcommon:held:0/:armor:/:offhand:0/:util:, whilereward/MmoStats.KeySegmentsonly recognisedmmoskilltree:-prefixed keys, soisEnhancementBridgeKeywas always false andcategoryOfnever answered GEAR for a stamped stat. The taxonomy now reads the namespace off the installed bridge instance (KeySegments.bindBridge, called first thing inevent/SystemBootstrap.registerSystems, the one place the plugin already hands the installed bridge through, before any trigger system can fire a recompute; unbound, the taxonomy recognises no bridge key at all rather than guessing), a bridge key is GEAR and is what the DOT opt-out drops, andCombatDamageTrack3FoldTest,HitDamageAttributionParityTestandMmoStatsKeySegmentsTestuse the real spelling (they had themmoskilltree:held:0one, which is why they passed). The flat / percent partition was already right, since a non-MMO key folds before the percent multiply either way, and stays so. - About five hundred
'ziggfreedcommon' and 'mmoskilltree' both publish the achievement ...lines at every boot. The library's commerce wiring registeredziggfreedcommonat CONSUMER rank before its progression bootstrap could claim LIBRARY-DEFAULT rank (ProgressionRuntime.newRegistrarkeeps the first rank an owner asks for), so the MMO's reading of a shared achievement clashed with the library's instead of outranking it silently, and load order decided which stood. 2.1.1 registers the library at default rank everywhere and warns once when an owner comes back asking for a different rank.
[1.6.1] - 2026-09-12
Breaking changes
- Requires ZiggfreedCommon 2.1.0 or newer.
REQUIRED_ZIGGFREED_COMMONand theziggfreedCommonVersionpin both read2.1.0, and boot refuses an older jar by name rather than starting with enhancements that silently do nothing or a boss framework that is not there. strictPlacedBlockCheckandplacedBlockExpireMinutesare retired (hard break, one boot WARN each naming the change). A placement earns nobody credit whoever placed it, and it is remembered until broken - free now that keeping it costs nothing. Both are still read fromskill-config.jsonsolely to notice a server carrying them, and are never written back, so a save drops them. Migration: delete both keys fromskill-config.json; where the lenient setting was standing in for a builder putting ore up for players to work, grant that buildermmoskilltree.placedblocks.exemptinstead (OP and creative placements are exempt already).mods/ziggfreedcommon/placed-blocks.jsonis retired to.legacyon first boot with one notice. Rows are not carried across: a saved position cannot be put back onto an unloaded chunk, and holding the file in memory until its chunks arrived would keep the cost being removed. Blocks placed before the update stop being remembered; everything after is guarded as before. Migration: nothing to carry across; the.legacycopy is never read and can be deleted.- Your own quest files move to
mods/ziggfreedcommon/quests/, one file per quest in the shared shape, once, on the first start after the update (hard break for anyone still editingmods/mmoskilltree/quests/). Everymods/mmoskilltree/quests/*.json(sub-folders included, the retired_defaults/echo excepted) is converted intomods/ziggfreedcommon/quests/, the folder ziggfreed-common reads a server owner's quests from, and the original is renamed.json beside itself; one boot notice names every file, every quest and every leaf the shared shape has no home for (a raw.legacy description, which reads fromquest.now;.flavor npcDisplayName; apermissionorrequireLevelthat hid nothing). A file that cannot be converted (a template no pack provides, a quest with nothing to do, a body the shared codec refuses, a target file that already exists) is left where it is, whole, and named with the reason; a file already in the shared shape is moved across untouched. It is the rewrite the docs-site Migration Converter performs on the same file, so either route lands the same files. Migration: nothing to do for a file that converted; read the notice for one that did not, fix it, and restart. Author new quests straight intomods/ziggfreedcommon/quests/(a same-id file merges leaf by leaf over the quest the jar or a pack ships, so.json {"Enabled": false}alone switches one off); the old folder is not read any more. - A pack's
Server/MMOSkillTree/Quests/files still load, converted at every start; author new quests inServer/ZiggfreedCommon/Quests/. Each legacy pack quest (its/ extends/paramstemplates resolved against the pack'sQuestTemplates) is converted into the shared shape at load and handed to ziggfreed-common's quest store as a contributed layer, above the shared files and below the owner folder; one boot line per pack names it and points at the web converter and the shared folder. A quest the converter refuses is named and left out, never half-loaded. The pack-controlReplacemode for quests is not honoured any more (one boot WARN names the pack);/mmomigratereports each such file as handled by the jar, and aQuestTemplates/file as needing hands. Migration: a pack keeps working as it is; drop a"Quests": "replace"line from itsControlfile (the key is unused), and move each quest toServer/ZiggfreedCommon/Quests/through the web converter when you next touch the pack./ Meta.mmoskilltree.Featureon a quest is retired (one boot WARN per quest still carrying it, naming the replacement). A feature a quest needs is a top-levelRequiresfactor,{"Factor": "mmoskilltree:feature", "Param": ", which Ziggfreed Common's fold lifts onto the quest's own availability so the quest does not exist here while the feature is off, exactly what the knob did; the older owner format's", "Min": 1} requiresFeatureslist converts to that factor on its own. The knob stays on an ACHIEVEMENT'sMeta.mmoskilltree. Migration: move eachFeatureentry into the quest'sRequires.Factors; until then the quest is in circulation with the feature off, and the boot line names it.- A save's quest and achievement record is moved once, and the move is claimed on that first login whatever it could not re-key. The two legacy per-player records (
QuestComponent,AchievementComponent) stay registered and read-only and are never stripped from an entity; what changes is when the claim is spent (the first Fixed entry below). Migration: nothing to do. On a server that came up from 1.5.x before this version, each player gets one last copy of the old record at their first login after the update; an administrator who hand-cleaned a player's file on the shared side only removes the legacymmoskilltree:questsandmmoskilltree:achievementssections too before that login, or that last copy brings the old state back once. /mmopacksexports and the config presets carry a server owner's quests in the shared shape. The exporter writes whatmods/ziggfreedcommon/quests/holds, verbatim, atServer/ZiggfreedCommon/Quests/(a file that only retunes a shipped quest is named in the log, because on its own it is a partial body); a preset's/ .json questscarrier is a map keyed by id (schemaVersion2), and a legacy preset'squestsarray is still imported through the converter.- An item's description is authored on the engine's own key, never generated (hard break for a pack that used
.description.base). The mod wroteitems.into its runtime language pack as a copy of an authored.description items.key, and that pack registers at plugin start, after every item has loaded; the engine checks an item's.description.base TranslationProperties.Descriptionkey against the loaded languages as the item decodes (ItemTranslationProperties.SERVER_LANG_KEY_VALIDATOR, with no plugin hook before it), so every such item logged[LOC] Key 'server.items.at every boot, and the copy bought nothing once the generated stat lines it used to carry were gone (the client's native tooltip stats section rows.description' does not exist in server.lang! Utility.StatModifiersfor every carrier).ToolStatDescriptionGeneratorandLocalizationConfig.canonicalItemDescriptionBaseare deleted; the roll-entry parser, points formatter and family colors thatItemEnhanceDisplayandItemEnhanceRollread for a per-stack enhancement line move toitem/StatLineFormat; the generated-lang pack carries only the per-skill tooltip labels, and aserver.langan earlier version staged undermods/mmoskilltree/generated-langis removed at boot. Migration: in your pack'sserver.lang, rename everyitems.line to.description.base items., the key the item JSON already references; an item still on the old key has no description, and that.description [LOC]line names it at boot. The shippedexamples/phase-i-school-packis renamed the same way.
Features
- A placed character shows you what they have for you, over their head and on the map. Adds
quest/QuestIndicatorService: over every placed NPC (an entity carrying ziggfreed-common'sPlacedNpcComponent), each online player sees the picture for the highest of the four situations that character is in for THEM (a scroll for a quest they could take, a gold bar for an errand they can hand in, a small pile of gold coins for a reward waiting; a map for a quest they are still on exists as a look but ships switched off), and the world map and compass mark every character with a quest on offer under the newquest.marker.availablehover line ("Quest on offer: {0}", the quest's own title nested). The MECHANISMS are Ziggfreed Common 2.1.0's: the per-viewer picture is its newentity/overhead/OverheadIndicatorsprimitive (one transient, intangible, never-persisted marker entity per host and state, sent only to its audience inside the engine's own visibility pass, moved after its host on a quarter-second cadence), the availability answer is itsobjectives/indicator/QuestIndicatorsover the same liftedCharacterQuestListingthe NPC quest page lists by (so the cue over a head and the list behind a press-F can never disagree), and the map marker is a SECONDWaypointTargetSourceon the existingQuestWaypointService(the tracked-quest source stays first, so a character the player is already being sent to keeps that marker). This mod supplies what only it knows: which entities are hosts, the alias answer set (the page's ownMmoNpcQuestDepsseam), the hover wording, and when a viewer's answers are recomputed: on the six quest events and at player ready for the one player, and on two engine-throttledDelayedEntitySystems (one indexing the placed characters per world, one evaluating every player against that index) every five seconds for what no event covers; a disconnect forgets the viewer everywhere. Registered fromquest/ProgressionBootstrap, refreshed fromplayer/PlayerReadyFlow, cleared fromplayer/PlayerDisconnectFlow; the plugin file is untouched. Four look files ship underServer/ZiggfreedCommon/OverheadIndicators/(Quest_Availablea scroll,Quest_Ready_To_Turn_Ina gold bar,Quest_Reward_Readya small pile of gold coins,Quest_In_Progressa map, each three quarters size, lifted 1.1 above the head so it clears a tall headdress, spinning the way a dropped item does), each floating one of four MARKER ITEMS underServer/Item/Items/MMOSkillTree/Markers/(Mmo_Marker_Quest_*, aParentchild of the vanillaDeco_Scroll/Ingredient_Bar_Gold/Deco_Treasure_Pile_Small/Deco_Mapthat draws the same picture, sits in no creative category, and carries the coloured halo vanilla draws around a rare drop:ItemEntity.ParticleSystemIdDrop_Rarefor the scroll,Drop_Legendaryfor the gold bar,Drop_Epicfor the coin pile,Drop_Uncommonfor the map; the halo is read off the item asset and never off the entity, which is why each look needs an item of its own). A pack overrides a look or a marker by id, andShippedOverheadIndicatorsTestholds the set to exactly the four situation defaults and pins that every item a shipped look names is a marker item this jar ships. - Whether a quest situation shows, and how, is authorable at three scopes, merged per leaf. The shared
Indicatorblock (Ziggfreed Common'sQuestIndicatorSpec:Collect/TurnIn/Available/InProgress, each{Enabled, State, Overhead: {Enabled}, Map: {Enabled, Icon}}, plus oneEnabledover the block) is read at the server's globalServer/ZiggfreedCommon/QuestIndicators/Default.json(owner filemods/ziggfreedcommon/quest-indicators.json), on a quest, and on a step for the situation that step raises; the narrowest leaf wins. The block reaches the engine on the sharedQuestitself (indicator(),stepIndicator(objectiveId)), folded by the library from the file; this mod models no quest of its own, so nothing here carries it, and an older-shape quest file'sindicator(quest and step scope) is carried into the shared body bymigrate/LegacyQuestConverter. The shipped default enables the map forAvailablealone, withCoordinate.png; overhead is on forCollect,TurnInandAvailableand OFF forInProgress(the library'sDefault.jsonsetsInProgress.Overhead.Enabledfalse, so a character where only a carried quest applies shows nothing and any higher situation still shows; one leaf in a pack'sDefault.json, the owner file or a quest's block turns the map picture on). A block pointing a situation at a state no look file describes is aquest-domain WARNING (UNKNOWN_INDICATOR_STATE) in/mmoconfig validateand the startup audit, folded in byQuestValidatorbeside the pool audit. - XP gain is a progress bar, not a line on the notification feed. Every skill has a bar on Ziggfreed Common's new progress-bar panels: the moment a skill gains XP its bar comes up, the name and the XP gained since it came up sit on the first line, the fill underneath is progress to the next level with the level you are in at its left end and the level you are climbing to at its right (
MAXthere, and a full bar, once the skill is capped), and a bar fades a few seconds after the skill stops gaining. A gain that crosses a level paints its bar gold, and the next ordinary gain paints it back. Which panel the bar lands on follows what you are DOING, not where the XP came from: out in the world it comes up on the wide World panel, which sits top-right by default (above the quest tracker, six across and growing leftward, so a bar already on screen stays where your eye left it; the bottom-left spot is the other one shipped for it, three columns above the hotbar and filling upward, with a band left clear across the rows so a busy stretch straddles the player's own health and mana bars instead of climbing into them and a cut that steps the far column around the utility slot); while you work a station it sits on the tall Activity ledger in the top-left column beside that session's item, lucky and session rows, HELD with the rest of the ledger until the run ends rather than fading on its own clock, whatever skill earned it (Defense XP from a mob hitting you while you mill belongs on the panel you are looking at). Where a panel sits is a spot file, a player's own pick or the HUD settings page (see the next entry). TheXP gainsnotification toggle keeps its meaning: off, and no bar shows. Everything else on the feed (a first-gain welcome, level-ups, the rewards-waiting reminder, luck drops, quests, achievements) is exactly where it was. Nothing is authored for a bar and nothing is registered with the library:skill/SkillHudBar.moveForderives every skill's row from the skill registry and one read of the saved skills (the row is the skill's own id; its name, its icon, its category's colour, its place in the stack, how far the level is filled, and the two captions,ui.skillbar.levelbinding the level as a typed number andui.skillbar.maxin all nine languages), andSkillService.notifyXpGainasksRpgStationsBridge.isWorkingStationand hands all of it to the right panel with the gain itself, so a skill added by a pack orcustom-skills.jsongets a bar exactly like a built-in one and no award call carries a source parameter. The station answer is a live recordStationBridgeListenerskeeps off RPG Stations' session started / completed pair (player to running session, cleared by session id so a stale stop never clears a newer session), read through a plain-typed facade that is a safe false with RPG Stations absent or disabled. A server owner who wants one row different authors aServer/ZiggfreedCommon/HudBars/whose.json Sourceis the skill's id (MINING), or an entry inmods/ziggfreedcommon/hud-bars.json. The category colour every skill surface reads now lives once, onSkillCategory; the single-skill reset card's strip reads it too, so combat there is the XP page's red and a custom skill's category shade is the XP page's. - Where the bar panels sit is a spot a player may pick, and the page that edits it is the library's. Ziggfreed Common 2.1.0 names a panel's spot from its
HudSpotsstore (Top_Left,Top_Right,Bottom_Leftshipped; a pack adds one by dropping a file), keeps a player's own pick or hide on itsHudPreferenceComponent, and ships the HUD settings page (a Mine tab every player may open, a Server tab for whoever the consumer's audience admits) plus/zighud. This mod adds only what the library cannot know: theHUDPlayerDataDomainandplayer/HudPreferencePersistence(one listener on the nativeZigHudPreferenceChangedEvent, registered fromquest/ProgressionBootstrap, marking the domain dirty and flushing it so the DB backend carries a pick between servers;DbBlobCodecand both sessions know the domain),PlayerSession.hud()for the peek, a HUD bars button on the Settings menu and a Bar Panels button on the admin HUD page, each launching the shared page throughpages/hub/MmoHudSettingswith the admin audience, the theme and where Back goes (the paradigm: a setting about a shared surface is the library's page, a consumer's menu launches it;MmoNpcPlacementAdminandMmoProgressionSwitchesalready do the same). Seven new keys in nine locales (ui.settings.hud_bars*,ui.admin.hud_bars_*).HudPreferencePersistenceTestdrives the listener againstFakePlayerDataRepository. - Every jar quest with a report-back step keeps that step off its list until the work before it is done. All twelve quests naming
Npc.TurnInId(the starter chain, its four side quests and the four temple quests) author Ziggfreed Common 2.1.0'sFlow.HideLockedSteps, and their$Comments say what it does for the reader. The leaf reaches the engine on the sharedQuestthe library folds; an older-shape quest file spells ithideLockedSteps(orHideLockedSteps, like its siblings) andmigrate/LegacyQuestConvertercarries it into the shared body, which/mmopacks exportcopies out with the rest of the owner file;SharedLayoutExportTestrides it round the export. It is quest-level because it has to reach the report-back stepQuestAsset.toDefinitionappends one order after everything else, which no per-step leaf could carry. - A quest says who may see it before taking it, carries its own fallback prose, and reaches a level by name. Three shared-schema seams land with Ziggfreed Common 2.1.0 (its own changelog has the detail, this mod only consumes them):
Listing.ShowWhen, a block in the same shape asRequiresthat hides the quest until it passes, independent of the accept gate, the third visibility knob besideHiddenandRequirePrerequisites;Text.Lore{Incomplete, Active, Complete}, one raw paragraph per lifecycle state, outranked by thequest.key whenever one ships; and an objective-kind alias, which is how this mod's.md. REACH_LEVELruns asSTAT_THRESHOLDon the skill's level channel while every sentence and picture keeps the skill the author named. A server owner's own quests live inmods/ziggfreedcommon/quests/, one shared-shape file per quest, a same-id file merging leaf by leaf over the shipped quest (Breaking changes, above)..json - A boss fight is a quest step, an achievement criterion and a bounty objective. Ziggfreed Common's boss framework fires three shared step kinds,
ENCOUNTER_DEFEATED(once per credited participant when the boss falls),ENCOUNTER_ATTEMPT(once per participant when a fight settles, won or wiped) andENCOUNTER_PHASE(once per live member on each phase beat), and this mod's own fold, validator, icon and name paths know them: a step reads{"Kind": "ENCOUNTER_DEFEATED", "Target": "KweebecNightmare_Warden_Encounter", "Amount": 1},Targetthe encounter SCRIPT id (never the boss creature's, which a phase swap changes under the player),Qualifierthe run's difficulty label or the phase's own state name. The step reads as the boss does everywhere else (its binding row'sNameKey, throughTargetNameResolver) and, for a boss whose binding row puts a marker on the world map, is drawn with the same glyph the map shows (MmoObjectiveIcons); a boss with no marker keeps the kind file's own picture. The step's sentence is the library's ownobjective.encounter_*line, in all nine languages, read through the shared fallback every kind has since 1.6.1; this mod ships no copy of it. A world first over a boss kill is won by the whole party that killed it, not raced between its members (a library change this mod needs nothing for). Needs Ziggfreed Common 2.1.0. - Adds the
Encounters_Defeatedlifetime statistic, per encounter script id, fed by the shared defeat moment through this mod's one reaction listener (the statistic only: what a fight pays is the framework's own, and the boss creature's death still produces the ordinary kill moment with its XP and drops). It is one more line the newziggfreedcommon:counterrequirement reading can ask about ({"Factor": "ziggfreedcommon:counter", "Param": "encounters_defeated/KweebecNightmare_Warden_Encounter", "Min": 1}gates a shop row, a dialogue branch or a harder contract on having beaten the Warden), because this mod installs its statistics record as that reading's source: every tally the stats screen shows, plain total or per-thing line, is readable by content the same way. - The
encountercontent-audit domain./mmoconfig validateand the startup audit report the library's own encounter findings (a mistypedzc:beat, a binding naming a script nothing ships, a defeat beat with noClearEncounterBossBar) beside every other domain, folded throughValidationChecks.fold. - A generated family can fan out over every boss on the server. The library's
ziggfreedcommon:encountersaxis is installed into this mod's one generator registry beside the two skill axes, binding each row to the encounter script id and itsencounter_name_key, with{"Difficulty": "hard"}as the one filter, so a slay-every-boss chain or a boss shelf stays right as packs add fights. - Adds
Server/ZiggfreedCommon/Factors/Mmo_Counter.json, the naming overlay for theziggfreedcommon:counterreading, beside the jar's four otherMmo_*factor overlays, so a shop row, a dialogue branch or a contract gated on a lifetime tally reads "Requires a lifetime tally" on a locked surface instead of the generic requirements line, with the newfactor.counterline in all nine languages. It carriesTextalone and noParamNames: the reading'sParamis a plain total or acategory/namebreakdown line, and the name of the thing behind a per-thing key lives on the thing itself (a mob's own key, an encounter'sNameKey), where no key pattern can reach it, so a pack that wants one tally named ships its own overlay with aKeysentry.CONTENT_PACKS.md's Condition factors chapter gains the reading's paragraph: the two-formParamgrammar, what an unknown key reads (0) and what no source, noParamor no live player reads (null, and the gate stays shut). - The worked contracts ship in the bounty pack, the first fight is Kweebec Nightmare's, and
CONTENT_PACKS.mdgains the chapter that builds a boss. The bounty-contracts-pack 1.2.1 carries the Open Warrant and The Muster Roll, two any-boss contracts, and kweebec-nightmare 1.2.0 runs the Warden on the framework; each one's own notes carry the detail. The Scarak Brood Queen, an overworld boss a server owner places once (one prefab paste and one/zigencounter spawn, per world, ever), is authored in the zone-quests-pack, which is held back and not part of this release. The new "Encounter authoring (boss fights)" chapter is the how-to: the six files a boss is, thezc:beat grammar, the_Encounternaming rule, theOncerule and the three other engine facts, the fiveZigTypes with their flat keys, the binding row key by key with its owner layer, the participation rule, the four moment files, the three step kinds, the validator's findings, and placement and respawn on a live world.
- An authored mastery
Iconis the picture that renders, on the track card, the track header and every node card.MasteryPage.resolveTrackIconItemdiscarded the leaf outright and derived the art from the track's target, which left every GLOBAL track (each of the seven damage schools among them) drawing the sameIngredient_Bar_Iron, and made the two codec leaves that document themselves as "Item id shown as the track icon" and "Item id shown on the node card" both dead. An authoredIconnow wins, the ruleShopEntryIconResolveralready follows, and the derive-from-target paths stay exactly as they were for a track that names none. A node's ownIconwins over its track's through the newresolveNodeIconItem; node cards previously painted the track's picture unconditionally. - A currency step shows the wallet it is about.
Earn_CurrencyandSpend_CurrencydeclareTargetNames.Currency, so a step naming a wallet is drawn with that wallet's own icon - the same one its balance strip and every price already use - with no per-wallet authoring here.
- A quest or achievement step shows what it is about. Every listed step is drawn with a picture beside it now: the item a craft or gather step names, the creature a hunt step names, drawn from that creature's own generated portrait. It reads on the giver's screen, in both tabs of the objective book and on a board's contract detail, and the picture is decided once in ziggfreed-common so a step cannot look one way on one screen and another elsewhere. A step nothing can picture keeps reading as its text alone rather than borrowing an unrelated thing's picture.
- This mod ships a file for each of the seven step kinds it adds that a screen has to picture (
Server/ZiggfreedCommon/ObjectiveKinds/MMOSkillTree/*.json; the two station kinds are RPG Stations' and their files ship there), the same shared schema its quests and achievements already use: what the kind counts and how, what its target names, and what it is drawn with. A level target is the exception, being authored sugar for the shared level-threshold kind: it READS through that kind, and it draws as the skill it is about, since the runtime step names the channel carrying that level rather than the skill (MmoObjectiveIconsreads the skill back out throughSkillLevelChannels.skillIdOf, leaving the summed total, highest and combat channels to the kind's own picture). The SENTENCE is not in these files: a kind's wording stays in the per-type key the renderer resolves by convention. A step whose target is a SKILL or an ABILITY is pictured bypages/render/MmoObjectiveIcons, since those are ids in this mod's own registries and nothing outside it knows that a mining objective has a pickaxe beside it wherever else this mod draws one. Everything else is pictured by the shared reading off its own target, with no fill from here. - A step kind's wording can be re-tuned by a file.
ObjectiveTextRendererreads thePresentation.TextKeyits kind's file gives it, under an objective's own key (one step saying something specific still wins) and over the per-type convention template, so a server rewording every kill step writes one file instead of overriding a key it has to know the name of. A step naming no target reads that key's.anytwin where one is authored, so a base sentence with a blank for the target is never handed a step that has nothing to put in it - "Cast abilities 10 times", not "Cast 10 times".ShippedObjectiveSentencesTestfolds the shipped kind files the way a boot does and renders real quest and achievement content through them, so a file whose wording cannot be filled by the step it is drawn for fails the build instead of reaching a screen with a hole in it.
- Adds
STATION_OUTPUTas a step kind this mod accepts and renders. A quest step or an achievement criterion of that kind counts the stacks a station hands its worker (target = the item id, qualifier = the station id, amount = the stack's quantity; a console-command payout and unattended output never count, by RPG Stations' own rule). It is quest-producible, its target is named as an item, and it reads throughobjective.text.station_output("Collect {0} {1} from a work station", with an.anyform) in all nine languages. - Adds the
Station_Itemsstatistic: a lifetime total plus a per-item breakdown (PerStationItems) of everything a station has handed the player, besideStation_Cycles. A save written before the field existed decodes with it empty; nothing is migrated and nothing is regenerated. - Adds skill XP for unattended station work, paid at gather. RPG Stations' unattended flow lets an authored station keep converting its placed materials while nobody stands at it, with the accrued batch forwarded when a player collects the output; the stations bridge now listens for that moment and awards the batch's skill XP to the GATHERING player - a real present-player grant, per skill, behind the same permission gate every station cycle uses, at exactly the amounts the engine forwards (it already applied the idle rate and the gatherer's own scaling, so nothing is re-scaled here). No session exists at a gather, so none of the per-session summary or toast-coalescing state is touched. Needs an RPG Stations build carrying its api contract 6 or newer; RPG Stations 1.0.0 ships beside this version.
/mmonpc place --role=captures the sender's position, yaw and world into a new fixed-coordinate placement, writes it to the owner file, re-reads and sweeps. An alias over ziggfreed-common's own[--id= ] [--dialogue= ] [--world= ] /zignpc placeand its sharedNpcPlacementAuthoring, so the two cannot drift; only the gate and the wording are this mod's. It refuses an unknown or abstract role, and an id already in use.- An NPC Placements tile in the admin hub, opening ziggfreed-common's own placement admin page. The MMO supplies only what the library cannot know: the admin audience (the same gate the hub checks), the theme, and Back returning to the hub. No MMO placement logic lives in the screen.
mods/ziggfreedcommon/npc-placements.jsonaccepts whole placements beside itsenabledswitches, at schema 1 (the lower-case gate leaf and the upper-case asset fields cannot collide, so every existing file still reads). Entries are decoded against the folded pack layer and installed as the placement store's owner layer once the packs have landed.- Adds the
MMOSKILLTREE_AUDIT_ON_BOOTenvironment variable. Set to1ortrue(either case, surrounding whitespace ignored; the same environment-variable convention asMMOSKILLTREE_DB_*), it runs the startup content audit at plugin start, once every pack has merged and every fold has settled and before the "booted" banner, instead of waiting for the first player, and prints one line,content audit on boot:, after the usual per-domain reports and the aggregate. A headless boot-check is what it is for: the audit needs no player and no world, so nothing is lost by running it early, and a boot that audited at start does not audit again when a player joins.finding(s) across domain(s) dev-server.ps1exports it on its own;/mmoconfig validateis unchanged. Mmo_Xptakes anAmountMax. The skill-experience reward kind took a singleAmount, so every payout of it was a flat number while the loot layer beside it had expressed a ranged item quantity for as long as it has existed. It takes an optionalAmountMax(content/reward/RewardSpecs) and draws evenly between the two, decided once per grant, mirroringCount/CountMaxexactly. It is a command-backed kind, so the range travels as a declared parameter into the command template and the roll happens inAwardXpCommand, once, before the skill fan-out: anALLor a category award pays the same rolled number into every skill rather than rolling per skill. A reward that authors noAmountMaxsubstitutes the declared default and behaves exactly as it always did, which is everyMmo_Xpreward in the family before the stations pack's trim wages;ValidationChecksleaves anAmountMaxequal toAmountalone as an intentional flat payout.
- Adds
restedXpMinOfflineMinutes(skill-config.json, default 30): the shortest absence that earns rested XP at all.RestedXpService.creditOnConnectmeasures the gap between the storedlastSeenMsand the connect, and an absence under the minimum credits nothing; at or past it the whole stretch is credited, still clamped byrestedXpMaxOfflineHours. ThelastSeenMsstamp is untouched by the gate, so a quick relog resets the clock exactly as it always did, and a server that wants every gap to count writes0. The admin Rested XP page carries a Min Offline (mins) field beside the max, saved with the rest of that page and refused when it exceeds the max offline window;/mmoconfig restedxpwith no argument prints the window it is running. Defensive load and noCONFIG_VERSIONbump: a config written before the key existed reads the default. - Both quest screens say how often a quest comes round. A quest with a repeat rule is badged
Daily,WeeklyorRepeatablebeside its row, on the list at the character handing it out and in the quest log, through Ziggfreed Common's oneQuestCadenceBadgereading overQuestCadence(the longer of the rolling wait and the calendar window decides, the same bucket thefinish N dailiesachievement qualifier has matched on since this release). A one-shot is unbadged. This mod ships no Java for it. Needs Ziggfreed Common 2.1.0. - The shipped quests stop hand-labelling their own cadence.
Temple_TributeandForgot_Somethingdrop theRepeatabletag from theirListing.Tags, and so do the mastery, stations, taming and zone quest packs (Repeatable,Daily,Weeklyacross seven files) - the badge derives it now, and three of those tags were WRONG: a calendar daily authored"Reset": {"Period": "Daily"}was taggedRepeatablein the mastery and stations packs as well as here.ui.quest.tag.*keeps its cadence words, since an owner may still tag a quest that way for their own grouping and the label still resolves; nothing ships one.
Fixed
- A player's quests and achievements no longer wind back on every login and every world change. On a server that came up from 1.5.x, any player with a tally the achievement catalogue could not fully account for (an achievement from a pack that is not installed, or one no longer shipped) had their OLD quest and achievement record copied over the live one again at every login and every world change: a quest an admin had reset came back, a bounty already collected reappeared as collectable, a hand-edit of the record never stuck, and progress since the update on any quest the old record knew was wound back to what it held on upgrade day. The copy happened once per ready because the move withheld its "done" claim for those tallies, meaning to retry them, and the ready event fires on every world entry. The claim is now spent by the first pass that wrote every family without throwing, whatever it could not re-key: a tally the catalogue cannot yet place stays under its saved key, and a sweep at every login moves it onto the criterion's own key the moment the pack that ships it is installed. On an affected server each player gets one last copy at their first login after this update, the way the very first login after 1.6.0 was meant to go, and never again. If you cleaned a player's file by hand on the shared side only, that last copy would bring the old state back once: remove the legacy
mmoskilltree:questssection (andmmoskilltree:achievements) from the player's file as well before they log in again; that is safe for any player who has logged in on 1.6.0 or later, since the shared record already holds everything those sections carried. - A bounty taken before the update can be collected. A contract carried across from a 1.5.x save was refused at every board. The shared engine binds a contract to the board it was taken from, recorded in the contract's own progress the moment it is accepted, and a contract carried across from the old record had no board recorded, so it matched nowhere and could not be taken again while it was still held. The carry now stamps each contract with the board that posts it (the first board it is listed on), so it collects there like one taken today. A contract the pack no longer posts on any board is dropped from the player's quest log, its completion record kept, with one server-log line naming the player and the contract.
- A quest collected at one character is collected there, and nowhere else. The stations pack's "A Word with Marn" authors
"TurnInAt": "Mmo_Sawyer", and it still sat under "Ready to collect" at the mastery trainer with a working Collect button and the reward marker floating over their head. The shared quest asset decoded the leaf intoQuest.turnInAt(), but this mod re-projected every folded quest through a quest model of its own and back into the engine, and neither hop carried the site, so every folded quest reached the engine with no site, whichQuestEngine.canCompleteAtreads as "anywhere"; bounties kept theirs only because they never passed through that model. That model is gone (the Technical section below): the engine runs the one sharedQuestthe library folds, the site rides it by construction, andcanCompleteAtrefuses the trainer, so the stations quest is collected from the sawyer alone. The same leaf is readable on the older quest format (aServer/MMOSkillTree/Quests/pack file, and an ownermods/mmoskilltree/quests/*.jsonfile on the boot that converts it):turnInAt(orTurnInAt) takes exactly what the shared schema takes,trueor"giver"for the quest's ownnpcViewId, an npc id,"@accept"for wherever the player took it, an empty string orfalsefor anywhere;migrate/LegacyQuestConvertercarries it into the shared body verbatim and Ziggfreed Common's oneQuestAsset.resolveTurnInAtparser reads both, so the two formats cannot drift.ShippedQuestContractTest,QuestTurnInSiteTest,migrate/LegacyQuestConverterTestandSharedLayoutExportTestpin it. - The last XP of a run shows the moment it lands, not the next time you happen to gain.
SkillService.addXpByNamebatched XP notices throughNotificationThrottle.accumulate, which returns nothing while its window is open and only hands the total over on the NEXT call after the window elapses, so the final chunk of every run of gains sat in the accumulator, unseen, until a further gain arrived that might never come. The layer is gone from the XP path andNotificationThrottle.XP_GAINwith it: every gain reports to the progress-bar panel, whose own repaint window (an authoredRepaintMson the panel asset, 250ms by default, leading edge plus a trailing flush) coalesces a burst and paints the last one on its own. Every other throttle type (crit, block, lifesteal, fall, combo, luck, the rewards-waiting reminder) is untouched, and so is the world-luck notice. - Earning an achievement lists the rewards it earned. Every one of the 153 achievements this mod ships pays through
Rewards.Claim, and Ziggfreed Common's earn moment carried the receipt of the immediate grant alone, so the notice read "+10 achievement points" and nothing else while the achievement's own page listed the XP waiting to be collected. The library'sAchievement_Unlockednow carries what the earn is worth (the immediate receipt, then what waits), the shapeQuest_Parkedhas always had, and the shippedAchievement_Unlocked.jsonlists it as rows under its headline. The reading itself was never at fault:ShippedRewardChipsTestdrives every reward kind the family ships (Item,Lootable,Droplist,Effect,Stamped_Item,Commandin its three shapes,Mmo_Xpfor a skill,ALL, every category token and a skill nothing ships a key for,Mmo_Boost_Token,Mmo_Ability_Mod,Currency) through the SHIPPED kind files and the SHIPPED English catalogue and proves each one reads as a real line, the all-skills XP line included - so a kind file pointing at nothing or a lang line dropped fails the build, never a player. - A reward toast raised after a payout lists what was actually handed over. A quest, achievement, shop or boss payout that rolled a loot table painted one generic "Loot Bundle" row on every toast that followed it, so the player never learned what they got. Ziggfreed Common's grant pass now answers a receipt (
RewardGrants.GrantOutcome.receipt(), aLootablereported as the items it rolled and an empty roll as nothing), and this mod reads it wherever its own toast follows the grant:QuestService.completeanswers the outcome of the engine's force-complete and the onboarding page's completion toast paints that receipt through the newQuestToasts.completed(quest, skills, rewards)form;QuestService.tryCompleteQuestanswers the outcome of the settle it runs (the engine's newtrySettle), so a quest that finishes the instant it is accepted at a conversation or from the book toasts what it paid through the newQuestFeedback.accepted(..., settled)form (the book hands the outcome over on Ziggfreed Common's widenedActionFeedback.accepted); the character screen's hand-in lists the roll through the library's own toast; and the objective book's points-milestone Collect lists its receipt throughAchievementService.tryClaimMilestoneRewards(aClaimReceipt: the result plus what was handed over, over the engine's newtryClaimMilestone), filled into the book's widenedMilestoneClaim.tryClaim. A toast lists ONLY what was actually handed over: a quest that parks (a full bag, or it collects somewhere else) has paid nothing, so its completion toast carries the gold headline and no rows, never the authored list (which would show a player items they were not given) - the one rule is Ziggfreed Common'sRewardGrants.GrantOutcome.receiptOf, read byQuestFeedback.completionToast(pinned inQuestFeedbackTest) and the onboarding page alike, and theQuestToasts.completedforms that painted the authored list are deleted (only the three-argumentcompleted(quest, skills, rewards)remains). The mod's three shippedFeedbackMomentsfiles (Quest_Claimed,Achievement_Unlocked,Achievement_Claimed) describe their rows as what was paid. The library-side moments (Quest_Completed,Quest_Claimed,Achievement_Unlocked,Achievement_Claimed,Encounter_Defeated), the objective book's and the character screen's Collect toasts, the bounty board's Collect and hand-in toasts and the storefront's bought toast are Ziggfreed Common's own change and list the receipt with no work here. Needs Ziggfreed's CommonLib 2.1.0. quest/QuestTitleUtilis deleted: naming a quest is the library's job, whole. This mod kept a second title ladder beside Ziggfreed Common's (titleKey, thequest.convention, the authored.title displayName, then the first objective's sentence), and the library's ended one rung short, at the written key, so a key-less, name-less quest - a bounty contract with noTextblock, any pack quest shipped without a title - readquest.on every library surface (the objective book, a character's offer list, the board's "Untitled", a.title /zigprogresslisting) while this mod's own toasts, onboarding preview, map marker and/mmoquestadmin listcalled it by its first step; a now-deleted toast fill was papering over exactly that gap. The missing rung moved INTO the library (ContentText.title()reads the first step's line the fold already stamps, before the explicit key as written;flavor()gained nothing, it already had every rung this mod's had, and the library's extra "paint the convention key" rung is gone for both), so every surface on the server names a quest the same way and this mod resolves nothing itself:QuestToasts,OnboardingPage,QuestWaypointService,QuestCommandand the book's prerequisite line readProgressionTexts.titleOrUntitled/title,DefaultContentLocalizationTestreads the fold's ownruntime().text()overLangCatalog.overrideForTestsfed with this mod's English, and the four-rung precedenceQuestTitleUtilTestpinned is pinned in the library'sContentTextLadderTestinstead. The@amount/@skilltemplate arguments already rode the runtime object (MmoQuestEngine.textOf), so no key lost its blanks. The one member that was not title resolution, the quest-tag label (ui.quest.tag., else the skill the tag spells), isi18n/Messages.questTagLabelMsg, answering null for a tag this mod has no word for so the book's own tidied fallback applies (the library'sObjectiveBookDeps.TagLabelSourceis nullable for that);ObjectiveTextRenderer's qualifier label reads it too and drops its duplicate key probe. With the ladders agreeing,MmoNpcQuestDeps.CompletionToastsand the.completionToast(...)fill are deleted along with the quest-id twin ofQuestToasts.completedthat only it called;MmoNpcQuestDepsfills the theme and nothing else. The fills that remain each carry something the library cannot know: the theme retint, the menu rail, the statistics column, bounty presentation (the objective book cannot see zc-commerce's board catalogue), the total-level / skill-level / achievement requirement wording, the server-first store, the points-milestone ladder, thefeedback.jsonaccept/abandon sound channels, the room pre-check's wording (the engine'stryClaimrefuses with null, no reason to word), and the HUD layout file. Ziggfreed Common's hand-in toast also stops painting gold over nothing: a hand-in that parks the quest reads the green "Handed in." line, the gold "Quest complete" line is for a payout.- A quest that is out of reach says WHY, and one that comes back says WHEN.
LockReasonsmapped four of the nineQuestGatesrefusal tokens and dropped the rest ontolock.other, "Not available yet", so a daily finished today, a quest already carried, a spent lifetime cap and a switched-off quest system all read as the same non-answer - andperiod_spentis the daily case, which makes it the most common repeat refusal in the game. Each token now has its own line in all nine languages (lock.period_spent,lock.max_completions,lock.already_started,lock.system_disabled), and the two clock tokens quote the wait: the come-back instant was already computed byQuestLifecycle.RepeatCheckinsideQuestEngine.canAccept, which kept the token and threw the record away, soAcceptCheckcarries it aswaitMs(the 2-arg constructor kept,QuestEngine.offerableInMsadded) andLockReasonsgains wait-takinglines/bestLine/lineforms beside the token-only ones, which still answer the no-clock twins for a caller with no record behind the token (CommerceRefusals,MasteryPage,ZigBoardPage). The wait is its own translatablewait.days/hours/minutes/under_minutefamily binding whole numbers as typed{N, number}params, nested into the lock line as its{0}, so each client writes its own digits;LockReasonsholds no clock, the wait is always passed in. A repeatable waiting out its window classifies into a newNpcQuestSections.Section.COOLDOWN("Comes back", between PARKED and LOCKED), leaving Locked to mean what a player already reads it as, a requirement refusing them. The objective book's row readout quotesofferableInMsat both sites instead ofcooldownRemainingMs, which reads the ROLLING clock only and showed "Cooldown: 0m" for a daily held by its calendar window. Needs Ziggfreed's CommonLib 2.1.0. /mmoability reloadhung the server for good, and the same fault waited in ordinary combat and on three admin pages. Every engine asset-store mutator (loadAssets,loadBuffersWithKeys,loadAssetsFromPaths,loadAssetsWithReferences) funnels intoAssetStore.loadAssets0, which takes the process-wideAssetRegistry.ASSET_LOCKWRITE lock (AssetStore.java:774, oneReentrantReadWriteLockshared by every store in the JVM) and dispatchesLoadedAssetsEventinside it.World.tickholds that lock's READ side around the whole tick body, bothconsumeTaskQueue()drains included (World.java:462-485), so everyworld.executetask, everyAbstractPlayerCommandbody and every custom page handler runs holding it. AReentrantReadWriteLocknever upgrades: the reader asking for the write lock waits for every reader to release, itself included, and the world thread wedges forever (the engine flags exactly this with a TODO atAssetStore.java:773). Boot was safe only because the asset-load pass runs before any world ticks, and the ~109 writes that take a second there hung the same thread from a command. Six write sites in the jar could reach that state:ImplicitAnchorWrappers.registerWrapper(one synthesizedRootInteractionper wrapped ability, ~86 of 97, the reported hang) andOnHitRootBridge.register(~23 OnHit wrapper roots) on the/mmoability reloadfold;OnHitRootBridge.ensure, the in-cast lazy fallback for an OnHit child no fold walked, fired fromMmoArmNextHit/MmoBeam/MmoDash/MmoProjectilemid-tick;ResistEffectRegistrar.loadEffectsfrom the skill-tree admin pages'revalidateOnlinePlayersAND fromSchoolResistReconciler's per-equip bucket generation, which is a world-thread path on every first equip of a new school-resist bucket;StatSubstrateBootstrap's twoEntityStatTypebatches, reached from the config-preset import page throughMasteryConfig.load; andSchoolCauseGenerator's leaf-cause batch, boot-only today. All six now write through ziggfreed-common's newasset/AssetStoreWriter(the same shape as the engine's ownPathWatcherhot-reload thread, which holds no read lock and is why it can write): inline and synchronous when the caller holds no read hold, so boot ordering is byte-for-byte what it was, and queued on theziggfreed-asset-writerdaemon thread without waiting when it does./mmoability reloadruns its whole re-fold + owner re-read + validation pass on that thread viaAssetStoreWriter.offThreadand answers the player on their world thread when done (a newcmd.ability.reload.failedline in all nine languages covers a pass that throws); typing it again queues a second pass behind the first. A hit whose OnHit child is not yet registered skips that one hit's payload, queues the root, and the next hit fires it, with one WARN naming the child; a school-resist bucket written off-thread re-runs the player's reconcile on the tick it lands, so the effect arrives without waiting for their next gear change; a stat batch queued from a page exists from the next tick.ImplicitAnchorWrapperspublishes a wrapper id only once its root has landed, so the fire path never names a root the store lacks. Durable guard:RepoHygieneTestrule 7 fails the build on any of the five mutators called outsideAssetStoreWriter.java, family-wide (// STORE-WRITE-OK:is the escape hatch, zero today), and zc'sAssetStoreWriterTestproves inline-versus-deferred against the realASSET_LOCK. Engine evidence in [[world-thread-asset-store-write-deadlock]]. Needs Ziggfreed's CommonLib 2.1.0.- Building XP was paid for placements the world refused, once per click. The engine dispatches
PlaceBlockEventand only afterwards decides whether the placement is allowed at all:BlockPlaceUtils.tryPlaceBlockreads the world gameplay config'sAllowBlockPlacement(false in the shippedForgottenTempleandCreativeHubconfigs) and then, for a non-creative placer, the coveringEnvironment'sBlockModificationAllowed(false in the goblin-lair dungeons), returning false WITHOUT cancelling the event and handing the item straight back throughonPlaceBlockFailure. Nothing tells the event's listeners, so ziggfreed-common'sPLACE_BLOCKproducer fired for a block that was never placed:MmoMomentReactions.onPlaceBlockpaid BUILDING XP,StatisticsServicecounted a block placed, anyPLACE_BLOCKobjective advanced, and the placed-block ledger marked a position the player had not built on, which would then have refused them credit for breaking whatever genuinely stood there. A player in the Forgotten Temple could hold a stack of dirt and train Building forever without a single block going down. The library'sPlacedBlockRecorder.placementCounts- the ONE predicate the producer and the ledger writer share - now asks the engine's own two build permissions through the newworld/BuildPermissionbefore either counts a placement. Whether the block can PHYSICALLY go in the cell is deliberately still not asked: that is the engine's placement validation, and mirroring it wrong would silently deny credit for ordinary building. Needs Ziggfreed's CommonLib 2.1.0. - The first quest can be finished at the temple guide, not only the one at spawn.
Mmo_Hub_Intro's three temple beats sit in theStartladder'sFirstband, which outranks theQuestsband entirely, so at the temple guide the walk fell totemple_greet/temple_meet/temple_talkand never reachedgreet- the one node carrying the welcome screen and the Skip line that completegetting_started. A player who found the temple before meeting the guide at spawn had no way to finish it from there.Firstgains agreetrung at the very top, gatedNot QuestState getting_started COMPLETED, so the welcome outranks the temple beats until the first quest is done and is gone afterwards. It changes nothing at spawn, where theQuestsband routed to the same node. - A notice drawn into an open screen kept only its second line.
FeedbackEnginesends a moment's toast to the HUD when no page is open and INTO the page when one is, and the in-page build put the moment'sSecondaryin the panel's one headline slot and itsTitleinToastSpec.withTitle- a fieldToastRenderernever painted and nothing else read, so the sentence naming what happened was built, handed over and dropped. Finishing the first quest inside the guide's conversation therefore read as+5 achievement pointsand nothing else.ToastSpeccarries ONE headline and a stack of rows:inPageToastkeeps the title there, as the corner notice reads it, and puts the secondary in a text-onlyToastLineahead of the reward rows, budgeting the panel for its row and for theRewardToastLinesoverflow line so nothing lands past the last pre-baked row.ToastSpec.title()/withTitleare deleted with the bug. - The in-page toast draws the moment's picture.
inPageToastpassed.withIcon(icon(args))into the same unpainted-field trap, so an achievement's trophy showed on the corner notice and vanished on the in-page one.ZigToast.uigains#ZigToastHead, a Left-laid headline row whose#ZigToastIconcell and gap collapse to zero width for a moment carrying no picture (the centered label then takes the full panel width back, exactly as before), andToastRenderer.setHeadIconfills it.ToastSpec.iconPathis renamediconItemId, which is what it always held. - A quest's notices are illustrated by the quest.
QuestEnginecarried noicononQuest_Completed,Quest_ParkedorQuest_Claimed, so only achievements could ever have a picture on a moment; all three carryQuest.icon()under the fixediconname the feedback engine reads. All fourteen jar quests author aListing.Iconto go with it (Deco_Scrollfor the guide's first errand,Bench_WorkBenchfor the tool-crafting one,Ingredient_Bar_Copperfor the temple's copper, and so on), as do the stations pack's eight. Needs Ziggfreed Common 2.1.0. - A parked quest no longer tells you which page to open.
notify.quest_ready_to_claimended with "Open the Quests page to claim your rewards." in all nine languages; the rewards waiting list themselves as rows under that headline, so the instruction was restating the panel it was drawn on. The sentence is dropped in every locale, leaving the completion line;notify.quest_inventory_fullandnotify.contract_ready_to_claimkeep their own second sentences, which say WHY a reward parked rather than where to go.
- "Return to the Adventurer's Guide" no longer sits under "Craft a Workbench" as though both were open. The report-back step is one order after the work, and the engine already called it locked; the two screens that list every step simply did not ask. They read
QuestEngine.listedObjectivesin Ziggfreed Common 2.1.0, which honours theHideLockedStepsthe twelve jar quests above author. - A finished quest waiting to be collected is on the list of the character it is collected from, and nobody else's. A quest that names no collection site may be collected anywhere, which is what most content wants, but it also means a parked one sits under "Ready to collect" at every character in the game: the mastery pack's meet-the-trainer quest was offering itself at Sawyer Marn. Content says where a reward belongs with the quest asset's
TurnInAtleaf, and the two quests that are an introduction to somebody now name that somebody, so each is listed at the character it sends the player to. Migration: a quest of your own that parks a reward and belongs at one character wants"TurnInAt": "(or" "giver"); leaving it out keeps the collect-anywhere behaviour. - A finished quest waiting to be collected is on the list of the character it is collected from. The mastery pack's meet-the-trainer quest is given by the guide and credited by the trainer's own
MarkTalkedbeat, so at the trainer it was neither taken here nor an outstanding hand-in and vanished from "Quests here" at the very character its own comment says to collect it from. The library's NPC quest page asks a fourth question of every carried quest (finished and collectable here); the pack is untouched. - Picking a conversation line that finishes a quest takes the player to that quest, Collect button in front of them, instead of dropping them back into a menu with no sign anything happened. Only a quest that parked something for collection routes (an on-the-spot payout does not), the route wins over the option's own
Goto/Close/Open, and it opens the quest list, never the quest'sCompletionDialogue. - Pressing Collect plays the quest's completion conversation, which is what that authored beat is for: the guide's
Mmo_Hub_Introfollow-up plays when the reward lands in the player's hands at the guide, and stays silent from the objective book or out in a field by the shared routing's own rule. - Reset to Defaults puts the rested-XP flat rate back to 250/hr, the rate the mod ships. The 1.6.0 rebalance from 1500 moved the live field only, leaving
resetToDefaults()and theDataDTO default the loader falls back on both at 1500, so pressing Reset to Defaults on the admin page (or loading askill-config.jsonwith the key deleted) quietly ran rested accrual at six times the intended rate. All three read 250. - A dialogue
Gatecondition can answer aQuestsprerequisite.MmoDialogueConditionsevaluated one againstGateSubject.ofSkills(...), a subject carrying the skill component alone; the shared completion probe reads a player's status off the progress record the subject hands over (ZigQuestStore.componentOf, viaGateSubject.facet), so a null record read as "cannot tell" and the clause refused for everybody, always. A line gated on a finished quest was therefore invisible whatever the player had done, with nothing logged - the live case being the guide's two temple pointers, at the mastery trainer and at Sawyer Marn, both of which namemeet_at_the_temple. The condition builds the whole subject the payload already fetched for this render (skills AND progress, plus the live player and its ref), so a conversation decides a requirement the same way every other gated surface in this mod does. - A disabled meta achievement naming a disabled child is a note, not a warning. The meta check logged
Meta 'X' references disabled child 'Y' - disabling metaat WARN for every meta whose child was off, which shouted about a family an author had deliberately switched off together (the zone-quests pack'sorbis_campaigneroverhowling_sands_campaign, both disabled on purpose). The warning stays for an ENABLED meta naming a disabled child, the contradiction it was written for; a meta that is itself disabled logs the same line at INFO. - The per-player quest-notification setting decides again which step toasts a player sees.
all/milestones/objectives/questsall behaved asall: the grading compared the shared engine's moment id against a name that was close but not the one it fires, so every level exceptnonefell into the "some other quest toast" branch and saw every single tick of every step. The authored quarter marks amilestonesplayer is meant to see, and nothing else, were bypassed with it. The id is the one the engine announces, matched case-insensitively, andQuestNotificationLevelTestpins it against the moment file the shared jar ships so a rename cannot quietly kill the grading a second time. - The shipped NPC roles' environmental immunity applies.
Template_Mmo_HubandTemplate_Mmo_QuestGiverasked forImmunity_EnvironmentalunderCombatConfig.EntityEffect, a key the engine's role codec does not read (it readsEntityEffects, a list, the spelling the base game's own passive NPCs use), so the immunity never applied and every boot logged an unused-key warning per role. Both templates author the list now, as do the bounty pack's six brokers, the Mastery Trainer, the Kweebec roles and the council example roles. - Two hub dialogue lines ship whole in every language. The guide's temple brief and mastery brief had wrapped onto a second physical line in the shipped
.langfiles of nine locales; the engine skips a line with no=as malformed, so each of those sentences was cut in half in game (the English mastery brief included). The second half is joined back with the line break the rich-text renderer understands, andLangFileIntegrityTestfails the build on any non-comment line without=from now on (it only ever checked lines that had one). - "Server First lost" is said when the race is lost, not every time the player walks through a door. A player who had met the criteria for server-first achievements somebody else already holds was told about every one of them again on each login and on each world or instance entry, and again whenever they opened an achievement screen: six masters claimed by other players read as six lines of bad news about races settled days ago. The loss fires from the shared engine's earn gate, which is asked afresh by the self-heal sweep behind every one of those moments, because a lost claim deliberately leaves the criteria met so the decision can be revisited. The sweep still asks - an admin clearing a claim is how a player wins one back, and the sweep is where they find that out - and the announcement is kept for the occasion the criteria are actually completed on. Needs Ziggfreed Common 2.1.0.
- A claim card says what the ability does, and its CLAIM button stays on the card. An ability-unlock card in the skill tree and a mastery node card read out
ChainDescriptionRenderer's walk of the ability's body, qualifier trails and all, in a 280x280 card whose chrome leaves it about seven lines; Vortex Lunge needed ten, so the description pushed the status line and the CLAIM button past the card's bottom edge and the claim was unreachable. Two halves, one per cause: the description region takes the space left after the fixed rows have taken theirs (#RewardDescAreainSkillTreeChoiceCard.ui,#NodeBodyinMasteryNodeCard.ui), so no description length can push a card's button off the card, and short text renders exactly as before; and an ability that authors its own description (Identity.DescriptionKey, theability.all 98 shipped abilities carry) has THAT be the card text, the sentence written for a player weighing a permanent tier pick, with the walked summary standing in only for an ability that authors none, rendered without its qualifier trails there so two steps differing only in qualifiers collapse to one counted line. The cooldown/cost line still closes the card either way, and the icon-hover tooltip is unchanged: flavor plus the full numbered sequence..desc - The Archmage passive stops whiting out the caster's screen.
Staff_Passive_ArchmageplayedMMO_Arcane_Pulsethrough the nativeEffects.Particlesblock, which attaches the system to the caster, and that system composed vanillaMemory_ImplosionEndwith vanillaMemory_Projectile_Core. The second carriesInitialAnimationFrame.Scale.Xof 60 underScaleRatioConstraint: OneToOneplusCameraOffset: -0.2, and vanilla references it from exactly one place,Memory_Projectile_Sparks, on a flying projectile - where a 60-unit blue billboard is what keeps the bolt legible at range. Sitting on the camera it is the whole screen, and the passive triggers onHitDealtwithFilter: any, so it fired on every staff swing. The ability names its ownMMO_Archmage_Arm(vanillaSword_Signature_Status_GlowplusPotion_Signature_Sparks, thenSword_Signature_Status_Lines0.08s behind them, all atY +1.25,LifeSpan0.8 - the glow-and-rays wreathMMO_Vampiric_Auraalready pairs, shortened to read per hit), andMMO_Arcane_Pulsedrops the projectile spawner, which fixes the same flash onMagic_Capstone_Arcane_Resonance, whose damage zone plays that system as its 500ms tick.
- The Archmage cue reaches the caster, and it makes a sound. Two things kept the replacement above from reading at all.
Effects.Particlesis THIRD PERSON only (the engine's own wording: "particles to play when triggering this interaction", againstFirstPersonParticles' "while in first person"), so a caster saw nothing whatever the cue's size - the 60-unit billboard only reached them because a billboard that big engulfs the camera from any anchor. And the first replacement was a potion sparkle, a dozen specks a tenth of a block wide for a fifth of a second, which is imperceptible mid-fight; vanilla's ownPotion_Signature_Burstis that spawner at the sameLifeSpanand offset shape, so it rendered exactly as authored and simply could not be seen. The system is the wreath above, the ability lists it under BOTH particle keys (vanillaStatus/Poison.jsondoes the same, one system named twice), and itsEffectsgainsLocalSoundEventId: SFX_Staff_Flame_Consume_Charge_1_Local- a staff charge, caster-only, four random source files so a per-hit cue does not fatigue, and no music-ducking rules, matching the shape the sibling armed-hit passiveArty_Passive_Sniperalready authors.BodyMigrationFidelityTestcaught both passes against the frozen digest as designed; each was leaf-diffed against its baseline first (the id change was oneSystemIdstring, this pass two ADDED leaves, and no existing value, node or chain edge moved either time) and re-captured inpost-wave-fidelity.json, recorded in the fixture's javadoc beside the two prior partial re-baselines.dx-pass-fidelity.jsonstays byte-identical.
- An objective target is named by the item asset, not by a guessed convention key.
TargetNameResolver.itemNameMsg- what aTURN_IN,CRAFT_ITEM,PICKUP_ITEM,BREAK_BLOCK,PLACE_BLOCK,CATCH_FISHorSTATION_OUTPUTstep reads its target through - probedserver.items., then.name items., then prettified the id. The engine already answers this authoritatively:.name Item.getTranslationKey()returns the item's ownTranslationProperties.Nameand falls back to theserver.items.convention ONLY where that field is unset, and.name Item.getTranslationMessage()carries the item'sNameArgumentsacross with it. The guess is right for a vanilla item precisely because vanilla leavesNameunset, and wrong for any item naming a key of its own, which then read as a prettified id in a step while every other surface in the game showed it correctly. The fix is ziggfreed-common's, per the lift paradigm:NativeNames.itemNameMsgandNativeNames.targetNameMsgask the asset store first (guarded, so a unit JVM or a pre-registry lookup reads as "no item" and falls through), each gaining a probe-injectable overload, and this mod'sitemNameMsgdelegates to it while still supplyingLocalizationConfig::hasFullKeyas the probe so the answer is decided against the same catalog view as every other name on these surfaces.NativeNamesTestgains six ladder cases over the injected probe. Needs Ziggfreed Common 2.1.0, the floor this version pins.
- A mob is named by its NPC ROLE everywhere a server owner names one. Every mob-keyed surface in this mod matched against ziggfreed-common's
EntityIdentifierUtil.getMobId, which read the MODEL asset id:mob-kill-xp.json, the combat-XP deny list, kill XP, bonus drops, mob-drop commands and aKILL_ENTITYobjective's target were all keyed on a costume rather than on the mob. That passes unnoticed because the vanilla assets mostly name a role after the model it wears, but the two are not the same thing, and where they differ a rule governs mobs nobody aimed it at. The reported case: a server owner wanting a training dummy to train combat skills added the dummy tomob-kill-xp.jsonby the name the game shows, and nothing happened, twice over - the dummies are roles (Test_Dummy,Test_Dummy_Stationary,Test_Dummy_Damage) wearing theMannequinmodel, so the entry matched no mob, and the shipped"Mannequin": 0row they could not see matched every one of them.getMobIdanswers the role name now (library change; the model is read only for an entity no role drives), so naming a role inmob-kill-xp.jsonis what it always looked like it was, and a non-hostile mob given a positive value there awards per-hit combat XP exactly as a hostile one does. Needs Ziggfreed Common 2.1.0.
- An XP award no longer rebuilds the whole leaderboard row.
SkillService.awardXpcalledLeaderboardDataStore.updatePlayeron EVERY award - once per block broken, per craft, per kill, per pickup, per combat hit - and that rebuild reads a level for every skill twice over (getSummedTotalLevel, then the per-skill loop), so roughly 42calculateLevelcalls at ~21 skills. Each of those binary-searched levels 1..200 calling an UNCACHEDgetXpForLevel, whose hybrid branch loopslevel-1times with aMath.powinside: about 34,000 transcendental calls per award, some twenty times the cost of the award itself, spent entirely to keep a display cache warm. Three changes, none of which moves a number: the hybrid curve is precomputed into along[201]at the first read and rebuilt only when a curve setting changes (SkillComponent.LevelCurve), sogetXpForLevelis an array index andcalculateLevela binary search over it; the leaderboard refresh is throttled per player (refreshIfDue, 10s) with a level-up taking the unthrottled path andfinalizePlayerwriting the last row on disconnect, which is where the final seconds of a session now land (nothing wrote one on disconnect before, despite the javadoc saying so); andleaderboard-cache.jsonis written compact rather than pretty-printed.SkillComponentLevelCurveTableTestpins the table against the formula it replaces at every level, bit for bit, plus the band edges, monotonicity, the past-the-table fallback and rebuild-on-setting-change. SkillRegistry.allSkillIds()is derived once per change, not per call. It allocated a freshArrayListand re-raninsertCustomSkillsevery call, andisActiveSkillLINEAR-SCANNED another fresh one per ask - solistableSkillIds()was O(n squared) over n+1 throwaway lists, and an XP award triggered it twice. A cachedRosterrecord (ordered list plus an upper-case set) is invalidated by the four mutators that can change it (registerCustomSkill,clear,clearAll,applyPackLayer);isActiveSkillis a set probe,listableSkillIdswalks the cached order once, andallSkillIdsstill hands back a mutable copy because callers sort and filter it in place.- A server stop no longer logs two SEVEREs from the generated tooltip-label pack. Plugin shutdown unregistered
MMOSkillTreeGeneratedLangthrough the engine, andAssetModule.unregisterPackcloses the pack's zip file system BEFORE it dispatchesAssetPackUnregisterEvent, so the engine's own listeners (CommonAssetModule,NPCPlugin) answered every stop with aClosedFileSystemExceptioneach. On a server stop the engine closes every pack itself, so the unregister is skipped there (HytaleServer.isShuttingDown()decides, read through the newutil/ServerLifecycle); a plugin unload or reload that leaves the server running still releases the zip handle the way it did, since that is the path a stranded handle would fail the next boot's rebuild on Windows. - A step naming one of this mod's own characters draws that character. A
TALK_TO_NPCstep is pictured by the shared reading, which points anAssetImageatIcons/ModelsGenerated/- a folder keyed by MODEL, where the step's target is a ROLE. Vanilla names most roles after the model they wear, so the gap only opens on a character a mod authored:.png Mmo_Hub_TemplewearsKweebec_Rootling, and the guide's own id has no still, so "Speak with Adventurer's Guide" drew the missing-icon fallback. Fixed in Ziggfreed Common 2.1.0 (Portraitsasks a registeredRoleArtfor what the role actually wears before falling back to the id); every quest giver, trainer and broker this mod and its packs ship is pictured by it. - A hand-in that delivers nothing is drawn with the character it reports back to.
Turn_In's target is the thing being DELIVERED, so the shared reading pictures a delivery by its item and leaves a bare "return to" step - the one Ziggfreed Common's quest fold (QuestAsset.toDefinition) appends for a quest-levelNpc.TurnInId- to whoever hosts the hand-in. That is this mod, soMmoObjectiveIconsanswers a targetlessTURN_INwith the portrait of the character named on the step's ownturnInLockId, the same id its sentence renders; an unresolvedgiversentinel and a step naming nobody both keep the text-only row rather than painting the missing-picture fallback. It is the one step with no target this seam answers. - A step's item picture draws. Ziggfreed Common's icon seam, which every step, criterion and detail line in this mod's screens is painted through, drove an
ItemIconwidget the client does not have, so a craft or gather step naming an item - on the giver's screen, in both tabs of the objective book, on a board's contract detail - laid out its icon's width and drew nothing in it, while a creature's portrait drew as intended. The seam paints a one-slotItemGridnow, the widget every price chip and reward row here already uses. Fixed in Ziggfreed Common 2.1.0; nothing in this jar changes. - Hookshot Arrow launched a player instead of pulling them. The pull writes the target's
Velocitycomponent directly, and the horizontal component was pre-scaled by 25 to cancel a discount the engine applies on its way to the ground. That discount is not applied to every target. Three separate systems drain aVelocityinstruction, and only one of them touches the number: an NPC's goes throughRole.processSetVelocityInstruction, which zeroes its motion controller and re-adds the vector throughMotionControllerBase.addVelocity, cutting X and Z to about 0.044 of what was written, so the 25 brought a mob back to the speed the ability asked for. A player's instruction is forwarded to their own client as aChangeVelocitypacket with no scale, no drag and no cap anywhere in the pipeline, and a generic entity's is written into the component as-is. Both took the 25 literally, so the ceiling that caps the pull at 18 blocks per second delivered up to 450 and threw the player clean off the map. The pre-scale is now resolved per target: a target carryingNPCEntitygets it, everything else gets the clamped speed the ability authored. Mob pulls are unchanged. - A Crafting XP reward paid Enchanting the same amount.
/mmoawardxp, the command everyMmo_Xpreward runs, read its skill token against the fourSkillCategorynames before the skill roster, andCRAFTINGis both a skill of its own and the category Enchanting is filed under - so a quest, achievement, shop offer or loot roll asking for 750 Crafting XP paid 750 into Crafting AND 750 into Enchanting, while the reward line the player read named Crafting alone. Sixteen reward blocks authorCRAFTINGacross the jar (six crafting achievements, two onboarding side quests) and the stations pack; none of the family's content asks for a category, and no other name collides,GATHERING,COMBATandMISCbeing categories only. A skill id is read FIRST now, so an exact skill always wins and a category still fans an award out where nothing shares its name;allis untouched, and/setxpand/bulksetxpnever carried the branch. The behaviour dates to 1.0.1. Migration: a profile that collected the extra Enchanting XP keeps it, and/setxp enchantingtrims one back if you want it exact.
- A targetless step and a total-level step read whole. The seven kept objective-kind files named a
Presentation.TextKey, the very convention key the renderer already resolves, and because that rung fired first with only an amount and a target it hid the targetless and total-level sentences on shipped content: "Cast 10 times" for "Cast abilities 10 times", "Reach level 100 in Total" for "Reach total level 100". The leaf is gone from every shipped kind file (pages/render/ShippedObjectiveSentencesTestrefuses it), the renderer's kind-file rung gains the.anytwin, a new rung falls through toziggfreedcommon.progress.objective.*once this mod has nothing of its own to say, and two targetless sentences the mod's own kinds lacked are added in nine locales (the same commit as "This mod stops re-wording what the shared library already says", below).
Changed
- Feature ids and statistic counter ids are spelled
Is_Like_This, and either casing still resolves. The feature ids this mod declares readMastery,Abilities,Currency,Xp_Tokens,Command_Rewards,Bounty,Shop,Item_Requirements,Action_Requirements,Taming,Npc_PlacementsandStations(their aliases likewise:Active_Abilities,Token_Shop,Hub_Npc, ...), each a constant onMmoFeaturesthat every Java call site reads; the lifetime statistics readMob_Kills,Blocks_Broken,Damage_Dealt,Playtime_Seconds,Encounters_Defeatedand so on. The jar's own content (Mmo_Hub_Intro, theSpenderchain), theMmo_Counterfactor overlay's comment andCONTENT_PACKS.mdspell them the same way. A feature id was already matched through the library's one normalization, so a pack that wrotemasterykeeps resolving; a counter key is matched without regard to case by Ziggfreed Common 2.1.0'sCounterMap, so a save written undermob_killsreads asMob_Kills, takes that spelling the next time the tally moves, and is otherwise left exactly as it was: no migration, and nothing refused for its casing. The statistics drill-down page and the objective book's stat cards route on the counter constants rather than on copies of the strings. Tests:StatisticsCounterSeamTest(the entity codec reads a lower-case save and re-spells the one key it writes),StatisticsBlobCompatTest(the DB blob captured before the rename still decodes, is pinned to its old spelling, and re-encodes a written key canonical beside untouched ones),MmoFeatureFactorTest(a feature answers under either casing). - A notice that repeats itself keeps to one line of the seven. The corner feed shows seven entries, an eighth pushes the oldest out of sight, and the engine gives a server nothing to retire one early (the packet carries no duration and nothing in the interface family removes an entry), so the only say this mod has over what a player can still read is how many lines it asks for, and it was asking for a new one every time: a lucky streak while gathering plus a "rewards are waiting" note per level filled the feed and buried the rest. Which line a notice lands on is the CATEGORY's business rather than the call site's:
NotificationCategory.feedTagwalks to the category's umbrella, so everything one umbrella covers speaks on one rewriting line, and the two "rewards are waiting" popups, which have sharedREWARDSsince 1.1.7, now share its line too; a currency gain files under its own, so earning coin never wipes a standing note; the rewards-waiting reminder itself is one gate at one a minute, shared by both systems that raise one.MmoNotify's illustrated shapes arewithIconandwithItem, matching the names Ziggfreed Common'sNotifygives the same shapes, which also settles a real ambiguity between an icon path and a tag. - A step the quest tracker is already drawing says nothing in the corner feed, and a repeat merges on a tag. The native
Notificationpacket carries aTagthe client merges toasts on, and nothing in the family passed one, so every objective tick and every per-cycle station pickup landed as a fresh entry and buried the feed. A feedback moment authorsToast.Merge(Ziggfreed Common'sFeedbackMomentAsset), the station item gains tag per item and per luck row, and a step counting up on the tracker panel is claimed off the feed by that surface (FeedbackSurfaces), so a long sawmill run leaves the feed to the pickups and the finds; the tick that finishes a step still announces. Both are ziggfreed-common 2.1.0 changes this mod consumes. - No XP notice reaches the corner feed any more. The feed holds seven entries, the engine gives a server no way to retire one, and an entry only expires while nothing new arrives, so one XP notice per second per skill kept the whole feed pinned behind XP for as long as a player kept working. Both XP feed paths in
SkillService.notifyXpGainare removed (the station-session coalescingNotify.itemKeyedform and the plain icon-path banner); the method is now the one call onto the shared panel, and thenotify.xp_gain/notify.xp_gain.stackedlang keys are retired with it in all nine locales.feedback/XpNotificationStacking(which existed only to choose between those two forms), its"stations"provider registration inRpgStationsBridge, andintegration/stations/StationSessionActivity(which existed only to feed that provider) are deleted rather than left as a dead seam; theStationSessionStartedlistener stays, because which panel a skill's bar lands on follows whether the player is working a station (see the progress-bar feature above), and the live record it keeps is one map insideStationBridgeListeners. - The onboarding previews' objective and reward lines are ziggfreed-common's shared detail line.
Pages/RewardRow.uiandPages/QuestObjectiveRow.uiare deleted;RewardListRenderer.ROW_TEMPLATEandOnboardingPage.LINE_TEMPLATEboth name the library'sPages/ZigDetailLine.ui(loaded before this jar and merged into the one client UI tree, the way the hub already appends itsZigSelectRow.ui), so a reward on a getting-started preview is the size of a reward on the NPC quest page and a size change is that one file's edit. The selectors move with it (#RewardNameand#ObjTextare#LineText; the icon shows throughIconRenderer.applyItemSlot, tooltip kept, into the line's#LineIconSlot/#IcoItem, and the fallback icon throughapplyIcon), and the compact variant's font follows the new base (13, three below the line's 16). - The ability bind page's search is the shared search row. Its field, Search and Clear are Ziggfreed Common's
@ZigSearchRow(Common/ZigSearchRow.ui), wired throughZigSearchRow.wire, so the two button words come from the library's own nine-localeziggfreedcommon.ui.langinstead of English baked into the.uifile; theability.bind_page.searchandability.bind_page.search_clearkeys are gone with them. Nothing binds per keystroke any more: Search submits, Clear appears once there is a query to drop, and every filter, sort and skill-dropdown click carries the field's live text along (ZigSearchRow.carry), so what was typed but not yet searched survives the rebuild exactly as the per-keystroke stash used to keep it.UIUtil.bindTextFieldandbindSearchDropdownroute their key throughSettingsUiUtil.directive, which refuses a key without the@the client needs to resolve the value as an element path;UIUtil.bindDropdowngains anEventDataoverload for a dropdown whose binding carries more than its action. - Every admin screen the hub opens is a card. The twelve editor buttons crammed into the Actions block (XP values, rewards, the skill tree, tokens, mob kill XP, mob drop commands, item and tool requirements, custom skills, world rules, power level, plus the two entitlement-gated ones) sit in a second card grid under their own Editors heading, each carrying the icon, title, one-line description and accent bar the section tiles already had, described in all nine languages. Actions keeps only what acts in place: the two reloads, Award Boost and the Danger Zone below it. The page grows to 950x800, the tallest in the admin family, and both grids are SIZED from the tiles they actually draw rather than a hand-counted height, so a card added later cannot spill over the block beneath it and an entitlement-gated card takes its own box with it when it goes.
- This mod stops re-wording what the shared library already says. Three feedback moments (
Quest_Completed,Quest_Objective_Progressed,Achievement_Server_First_Lost) shipped a file whose every leaf restated the library's own default, so each was one more copy of a sentence to keep in step across nine languages for no gain; all three are removed and the library's default answers. A step ticking reads exactly as it did, and a quest finishing keeps its wording bar the exclamation mark the library's line does without; a server-first race lost to somebody else is the one that changes wording, to the library's shorter line. Fifty-three lang keys go with them: the objective sentences the shared vocabulary already carries at the same slot (Break {0} {1},Defeat {0} enemiesand the rest of that family), the two glue keys that duplicated the shared one, and sixteen keys nothing reads any more - thirteen that had gone unread for releases, and three the dropped files were the last readers of.ObjectiveTextRendererfalls through toziggfreedcommon.progress.objective.*once this mod has nothing of its own to say. Nineteen of those sentences are word for word what they were; the rest take the library's phrasing instead ("Consume" where this mod said "Eat or drink", "Hand in" for "Turn in", "Speak with" for "Talk to", "Reach" for "Raise", "Complete quest:" for "Complete", and "Play" for a minigame round this mod called finished, "rounds of" for the parenthesised name it put after); and five kinds that had been dropping their target on the floor name it, so a damage step reads "Deal 500 damage to Zombies" and the breed, feed, harvest and companion steps read the same way. What stays authored here is what only this mod can say - the level channels, the break verb a block's own skill picks (mine / chop / dig / harvest), the report-back hand-in line, and its own step kinds. What a moment file has to earn its place with is unchanged: an argument only this mod supplies, a surface only it has, a chime it wants where the library draws a toast. - A calendar daily counts as a daily.
QuestService.classifyQuestType, theNORMAL/REPEATABLE/DAILY/WEEKLYbucket a "finish N dailies" achievement qualifier matches on, delegates to ziggfreed-common's one cadence rule (QuestCadence, read off the quest's runtime repeat rule): the LONGER of a quest's rolling wait and its calendar window is what a player experiences as "how often", so a quest that resets once a day with no rolling cooldown at all isDAILY(it readREPEATABLEbefore, because only the rolling cooldown was bucketed), a two-weekEverywindow isWEEKLY, and a legacy flag-plus-seconds quest buckets exactly as before. The twenty-hour and six-day thresholds live on the enum, spelled once for every surface that buckets. A folded shared-schema quest's legacycooldownSecondsscalar is filled with the same longer-of-the-two reading. - The stations bridge no longer fires
WORK_STATION; RPG Stations produces both station kinds itself. The station engine firesWORK_STATIONonce per real attended cycle andSTATION_OUTPUTonce per stack it hands a worker, straight into the shared progression runtime, and ships both kind files and their text templates - so a quest or achievement authored against either advances on a server with no MMO at all. This mod REACTS to those moments inevent/MmoMomentReactions(the two station statistics) exactly as it reacts to a craft; its ownWork_Station.jsonkind file is deleted (RPG Stations' is the one that ships), and a second fire from the bridge would have counted every cycle twice. TheWORK_STATIONandSTATION_OUTPUTenum constants stay as fold mirrors, andMmoQuestEngine.register()now skips an id the shared registry already holds, so a kind another mod registered is never re-registered over. /mmostation flair grant|revoke|listis an alias over ziggfreed-common's flair write path. Same command name, same--player/--flairIdshape, same admin gate, the sender's answers keyed as they always were; every write goes through the library'sFlairUnlocks(the/zigflairfamily's), which announces each real change as a nativeZigFlairChangedEventand draws theFlair_Unlockednotice itself. This mod no longer writes the flair record, no longer marks its player-data seam from the verb (a listener on that event does, for every path at once, see Technical) and no longer sends a toast of its own: theui.station.flair_unlockedkey is retired from all nine languages, andui.station.flair_refusedis added for an id the save format cannot hold. The verb still needs the player online; a reward authored as a reward uses the library'sFlairkind, which rides the pending-reward queue for an offline player.- The shipped mob-kill-XP defaults are re-keyed off model names. Of the 128 patterns, an audit against the shared source's role files found four that match no role:
BossandEliteare deliberate catch-alls and stay;MannequinbecomesDummy, which covers every training-dummy role whatever it looks like and preserves the row's intent (a dummy is worth nothing until an owner says otherwise); andScarabis renamedScarak, the creature's own name.Scarakis already keyed further down the same file at 65 withScarak_Broodmotherat 85, and the later row wins, so Scarak XP is unchanged at 65 and the renamed row does not decide anything. Every other pattern, and every shippedKILL_ENTITYtarget (Trork,Skeleton,Bear,Boar) and fish pattern, already resolves against a real role name and is untouched. - A per-target statistic or objective progress recorded before the update keeps its old key for the handful of mobs whose role and model names differ. Nothing is migrated: the two names agree for effectively every mob a player fights, and the ones that differ are dummies and test fixtures.
- A placed block is remembered on its own chunk, not in a file.
PlacedBlockLedgerkept every placement the server had ever seen in onemods/ziggfreedcommon/placed-blocks.json, and every block break drove its beat: a full scan of every player's every row on the 60s cleanup (plus a second, redundant one from the 30s flush), a full deep copy of the state on the world thread, and a pretty-printed rewrite of the entire file - all proportional to the file, not to what changed, and unbounded becauseplacedBlockExpireMinutesdefaulted to never. It is now ziggfreed-common'sPlacedBlockSection, a plugin-registeredComponentholding one bit per block on the block's own chunk section, laid out like the engine's ownBlockPhysics(lazily allocated byte array behind a versioned singleCodec.BYTE_ARRAYkey, leading flag byte so an unmarked section costs one byte). The engine's chunk save carries it, only loaded chunks cost memory, a read is an array index, and no file, scan, snapshot, timer or boot parse remains. Spending a mark still records the position forREAD_GRACE_MSso every reader of ONE break is told the same thing - without it the second reader would pay out on exactly the exploit the first refused. - A builder can be exempted from the guard.
PlacedBlockLedger.Policy.guardsPlacementsBy(PlayerRef)decides whether a player's placements are recorded at all; the MMO answers it fromPermissionUtil.isPlacedBlockGuardExempt(OP, ormmoskilltree.placedblocks.exemptwhen permissions are on). This is the case the guard got backwards - somebody standing up an ore vein FOR players to mine - and it is settled at record time, so an exempt placement is indistinguishable from a natural block afterwards. It decides only whether the block is REMEMBERED: the placer still earns whatever placing is worth. Creative placements were already exempt. - This mod no longer registers a stamper; per-instance item enhancement is ZC-driven.
item/MmoStackStamperreplaced ziggfreed-common'sStackStatsStamperto get the enhancement tooltip, which made the item FORMAT this mod's rather than the library's: a server without the MMO stamped items nothing could read, and two consumers stamping one item would have meant two formats with every budget check reading half the history. The library owns the write, the tooltip (loot.stamp.StampTooltip, the lifteddescriptionFor/baseDescriptionKey/hasMarkupwith its markup gate) and the equip bridge; this mod registersitem/MmoStampPresentation's successoritem/MmoStatNames, aloot.stamp.StatNameranswering only forMMO_-prefixed ids and declining everything else, so a stampedHealthstill reads correctly here.ItemEnhanceDisplaykeepsstatLineMsgalone.EquipStatBridge.installand its three trigger-system registrations move to the library'sEntityBootstrap(a second set would recompute the same stacks twice);MMOSkillTreePluginreads the installed instance throughEntityBootstrap.equipStatBridge()to hangSchoolResistReconciler.ItemEnhanceDisplayTestkeeps its composition cases againstStampTooltipwith the real vocabulary registered; its pure gate cases move to zc'sStampTooltipGateTest. - A weapon keeps its own move on a key an ability is bound to. Binding a slot to an input the held item already uses writes a claim on the player's own
Interactionscomponent, and the engine reads that component BEFORE the held item and returns at once (InteractionContext#getRootInteractionId), so the claim did not merely win the contest, it swallowed the key: every signature weapon lost itsAbility1move and the crossbow itsAbility3reload, for as long as the binding existed. Worse where the claim is WIDER than the binding behind it - a claim can only nameAbility1/2/3, while a binding distinguishes crouch on top of each, so a slot onABILITY_3_CROUCHclaimedAbility3whole and an uncrouchedA3press reached a branch with nothing to cast and died silently. The trigger roots now authorNativeInput: {Defer, Fallback}on their cast branches andMmoCastAbilityInteractionroutes the press: the item wins whenever ziggfreed-common's newinteraction.NativeInputGatereads its own chain as opening with a stat cost it can currently pay (a full signature bar, a magazine with room to reload), and the item gets the press anyway whenever the cast does not happen at all, so its own no-ammo or empty-bar feedback plays instead of nothing. A chain that spends no resource - gated only on durability, or on nothing - never counts as ready, since it would win every press forever. - An NPC placed by any route other than the placement sweep answers press F. The stamp binding an NPC to its placement is attached only by that sweep, so one spawned by a command, an egg, a prefab or another mod carried none:
ZigPlacementInteractrendered its interact prompt and then returned false, logging atfine, which reads in game as a broken character rather than an unconfigured one. A base role may now author its ownDialogueorOpenon that action as a FALLBACK - a placement standing the NPC still decides - and with no placement the identity comes fromNpcIdentities.npcIdOfEntity, so@selfand aMarkTalkedbeat still resolve.Template_Mmo_Hubdeclares a sixth parameter,Dialogue, defaulting to the hub conversation and bindable per variant;Template_Mmo_QuestGiveranswers the same case with a literalOpenofQuests. Wheregoverns placement and nothing else. The interact path never consulted it; the codec documentation and the shippedMmo_Hubcomment now say so, so widening a selector is only needed to have one PLACED in another world.- Twelve ability-validation warnings were the audit's own drift, not content.
checkDescriberCoveragere-derived the renderer's suppress rules from a hand-synced copy that had fallen behind: it was missing the four condition nodes and the unnamed-cosmeticApplyEffectcaseChainDescriptionRenderer.isPresentationOnlyNodesuppresses, and it decided filler by probing for a staticability.summary.native.key even where the renderer describes the node from its own resolved fields instead. So a boot reportedNATIVE_NODE_NO_DESCRIBERtwelve times overApplyEffectInteractionnodes the renderer either never prints or prints a named, timed line for. The audit now asks the renderer directly (ChainDescriptionRenderer.rendersOnlyFiller, over theDescribed.fillerflag every describe path already returns), so it reports what the tooltip would actually say; the copies and the parallel key probes are deleted. What stays here is the wording, which still names the exact key an author is missing. PRESENTATION_ANIMATION_IMPLICIT_SETis a note. An animation authored with no explicit item-animation set resolves against whatever the caster is holding, which is a supported authoring choice and usually the intended one: Guard Stance, Parry Riposte and Javelin Toss all want the animation for the weapon actually in hand, and naming a set would pin each to one weapon. It cannot be fixed without changing behavior, so it reports what is actually true - the clip cannot be checked at boot, so that standing goes untested until it is played in game.- An optional mod this server does not run is not a warning. Every adapter for an absent mod answers false from
initialize(), which is the ordinary case, and it was logged at warn level twice a boot (setup, then the deferred re-resolution on first connect): a server with two of the nine optional integrations installed took fourteen warnings for working exactly as intended. It logs at fine now. The adapter still names the absent mod at info, the domain's own no-active-integration line closes the trail, and the resolution summary repeats the whole chain. An adapter that THROWS still warns, since that is a real failure. - The gate vocabulary no longer warns that it overwrote itself.
GateFactorsseeds the portable Hytale vocabulary into its registry and then deliberately answers five of those ids off saved records, which the ledger reported as'hytale:stat' was registered by 'MMOSkillTree' and is now overwritten by 'MMOSkillTree'. Those five use ziggfreed-common's newFactorRegistry.registerOverride; an id this registry does not already hold still warns. Legacy_Quest_Fishing's icon is an item. It namedFish, which is a resource TYPE and not an item id, so the audit reportedICON_UNKNOWN_ITEMon every boot. It isFish_Bluegill_Item, the icon the fishing reward kinds already use.- The guide points a temple visitor at the sawyer, and five of his lines are reworded. The temple's steady-state screen of
Mmo_Hub_Introcarries one more question beside the mastery pointer, about the character with the hatchet across the landing. It shows only when the shared quest catalogue holds a quest calledtimber_rightsat all (the library'sziggfreedcommon:quest_knownfactor, never a pack name, so a server without the stations pack never sees the line), the station engine is running (mmoskilltree:feature/stations, so the guide never points at a character who is not standing there), the temple meeting is claimed and that quest is untaken; picking it says who Sawyer Marn is and where he stands, and its own accept takes on the pack's introduction to him,meet_the_sawyer, so the player walks down with an errand to say hello rather than a timber run they have not been asked for; the second line simply goes back, and his own chain, Timber Rights first, he offers himself once the player has stood in front of him. The gate readsmeet_the_sawyerthroughout, so a server whose stations pack predates the introduction never sees the line at all. Five hub lines are reworded in the guide's own register, each saying what it said before (combat_to_ranks,ranks_brief,menu,bounty_introand the starter-tools quest flavor).
Technical
player/ProgressStateMigrationclaims on the first pass whose leaf families all wrote.ResultisMIGRATED/ALREADY/UNFINISHED/NOTHING, withwrote()saying whether the record changed;UNRESOLVEDis gone, because a claim withheld for an unanswerable achievement was a claim withheld forever on a server missing that pack, andPlayerReadyEventfires on every world entry. The unanswerable ids are named once (unresolvedNotice) and left under their saved keys. Newplayer/AchievementKeySweepruns fromPlayerReadyFlowright after the move, every ready, over the shared record alone: any(or bare# ) key the catalogue can now answer for moves onto, first writer wins where the target already holds a tally, the positional key is removed through Ziggfreed Common's new# ZigProgressComponent.clearAchievementProgress, and a key it cannot answer for (or whose index the definition no longer has) is left alone, so a ready with nothing positional costs one pass over the keys and no write. Both shareProgressStateMigration.rekeyed, which now tests a numeric suffix with a digit scan instead of aparseIntand a caught exception per named key. The move gains aContractBoardsseam besideCriterionKeys(Posting: not a contract, posted at a site, or a contract no board posts), answered live byMmoBounties.isBountyand the newMmoBounties.postingSiteOf(the first DECLARED board a contract is posted on, spelled asBoardAssetSpec.boardId()spells it, which is whatBoardEngine.acceptrecords); a posted contract's payload is re-packed throughQuestProgressPayload.serialize(deserialize(legacy), site)with a site the payload already records kept, and a retired contract skips the status, payload, cooldown and pin leaves while its completion record is carried (the survivorZigProgressComponent.clearQuestkeeps), named once bydroppedNotice.PlayerReadyFlowmarksPROGRESSdirty when the move wrote or the sweep moved anything.PlayerConnectFlowdrops its Citizens branch: it calledQuestConfig.load()wheneverdaily_npc_greetingwas absent, a quest the deleted Java defaults used to generate and nothing ships now, andload()is a full republish of the shared catalogue, so on a server with the Citizens integration on it would have re-resolved every quest at every connect for a quest that never appeared; a quest that wants an integration present says so in its ownRequiresblock. Ziggfreed Common's progress record reads a quest id under any casing and re-spells it on the next write (its own changelog), so a pre-1.6 owner quest authoredMy_Questkeeps its saved states after the converter keys the filemy_quest. Tests:ProgressStateMigrationTest(the claim-once rule replaces the claim-withholding case, the throwing pass, the three contract answers, the stamp), newAchievementKeySweepTest, and newProgressionUnclaimedMoveTestoverfixtures/progression-unclaimed-move-1-6-0.json, the three progression components of the player file attached to the September owner report, decoded through the real codecs: one pass spends the claim and an administrator's reset survives the next, the contracts collect at their board and nowhere else through the realQuestEngine.canCompleteAtoverZigQuestStore, a retired contract is dropped and named, the positional taming keys re-key once the catalogue answers, and a throwing pass still does not claim. Needs Ziggfreed Common rebuilt.- Adds
migrate/LegacyQuestConverter, the ONE reader of the older quest format, and deletesquest/SharedQuestWriter. A legacy quest body in (an owner-file entry or aLegacyQuestAssetpayload, camelCase or PascalCase, every polymorphic shapeQuestConfig.normalizeQuestBodyaccepted, which moved here whole asnormalize), theextends/params/objectiveOverrides/extraObjectivesDSL resolved first throughconfig/template/QuestTemplateResolver(a missing template is aRefusedoutcome naming it and the loaded templates, never a skipped entry), ONEQuestAssetbody out plus per-questNotes(dropped, warnings, preserved), every body proven by decoding it throughQuestAsset.CODECbefore it is handed back, and a refused body reported, never written. It closes the gaps the writer never did:Indicatorat quest and step scope,TurnInAt,HideLockedSteps, a structuredrequiresblock merged with the flat leaves (flat factors first, quests deduped case-insensitively, the permission carried),requiredAchievements, the report-back rule (emitNpc.TurnInIdverbatim and never aturn_instep, since the fold adds the step),autoClaimRewardsselecting theAuto/Claimbucket,repeatable+cooldownSeconds+resetsOnCompleteas theRepeatgroup,tagsasListing.Tags,category/sortOrderonListing, the convention keysquest./.title .flavoras theTextdefaults withtitleArgs/flavorArgsasTextArgs,npcRequiredToAcceptasMeta.mmoskilltree.NpcOnly, an authored boolean carried as authored (false included, since the owner folder merges leaf by leaf), andvisibilitymapped to the shared knobs (RequirePrerequisites, aShowWhenfrom the level floors or the permission node, neverListing.Hidden). Rewards are read by the liveRewardJsonreader withRewardSpecs' dialect, objectives byObjectiveSpec.CODEC, gates byGateJson.parse, and the shared spellings are written bycontent/SharedSchemaJson(whose rewardParamskeys carry the documented spelling,Skill/Amount/CurrencyId). An unknown key rides through verbatim and is named, never dropped in silence. QuestConfigreads no file and models no quest.load()isProgressionDefaults.republishAssetContent(), a republish of the shared store;loadUserQuests,loadFile,parseQuest,normalizeQuestBody,setQuestsDir,mergeCommonQuestLayer, the pack-layer merge, the template plumbing and the consumer-rank publish are gone, and every method left is a projection overProgressionDefaults.questPool()(getQuest/getAllQuests/getDefinition/getQuestsByNpcId/getQuestsWithTurnInAt,categoryOfdefaulting tomisc,isNpcOnlydecodingMmoQuestMetaonce per published pool).asset/LegacyQuestLayerowns the retired pack store:AssetMergeHandlers.onQuestTemplateAssetsLoaded/onQuestAssetsLoadedhand it the live layers, it converts each payload, callsQuestAssetStore.mergeContributedand thenProgressionDefaults.republishAssetContent();reapplyQuestPackLayers(the/mmoquestadmin reloadpath) does the same.config/LegacyQuestOwnerMigrationruns fromOwnerConfigBootstrapbeside the otherLegacy*Migrations, before the runtime builds, per file all or nothing (LegacyOwnerFile.renameInPlaceis extracted for its.legacyrename), and also retires the_defaults/default-quests.jsonecho thatQuestConfig.load()used to; its target folder isQuestOwnerLayers.folder(), so a test points both ends at temp directories.- The MMO's quest model is deleted; the engine's
Questis the one object every surface holds.quest/QuestDefinition,quest/CommonQuestFold,quest/QuestVisibility,content/objective/Objectiveandcontent/objective/ObjectiveLevelSugarare gone, withMmoQuestEngine.publish/toRuntimeand their helpers (effectiveRequires,textOf,repeatOf,composedLine,def). Every reader (PlayerQuestCommand,QuestCommand,ConfigPresetService, the dialogue actions and quest reader,MmoPlacementValidator,OnboardingPage,MmoObjectiveBookDeps,ObjectiveTextRenderer,QuestFeedback,QuestToasts,MmoBounties,MmoQuestListeners,QuestService,QuestValidator,QuestWaypointService) holds ziggfreed-common's runtimeQuest, itsObjectiveDefor its foldedquest/asset/QuestDefinitionrecord, reached throughQuestConfig's projections; a step's sentence and picture read the AUTHORED pair (ObjectiveDef.authoredKind()/authoredTarget()) while the engine dispatches on the run pair.MmoQuestEngine.register()registersREACH_LEVEL(value-based, not producible) as an alias ofSTAT_THRESHOLDwithSkillLevelChannels::channelForrewriting the target onto its level channel, registers the two MMO axes intoProgressionRuntime.questAxes()(MmoQuestAxes.registry()returns that one registry) and publishes NO quest layer;Server/ZiggfreedCommon/ObjectiveKinds/MMOSkillTree/Reach_Level.jsonis the kind's file.QuestServicekeeps policy only:isQuestVisibleis the NPC-only-not-started rule and thenQuestEngine.isVisible(the shared listing rule overavailable(),Listing.Hidden,ShowWhenandRequirePrerequisites),canAcceptQuestthe NPC-only rule and thenQuestEngine.canAccept;passesContentGates,passesPrerequisitesWithoutStateandfindObjectiveare gone.MmoQuestMetakeepsNpcOnlyalone and names theRequires.Factorsreplacement for aFeaturekey.QuestValidatoraudits the shared pool (QuestPoolValidatorwith this mod's npc-declares probe,QuestIndicatorValidator, the library'squestLoadFindings(), andUNKNOWN_FEATUREover the lifted feature list);VISIBILITY_NOOP/VISIBILITY_GIVER_GATEDare gone with the leaves they judged (the converter's notes name a legacyvisibilitynode that gated nothing).AchievementDefinition.toRuntimeapplies the same shared alias,CommonAchievementFold.addCriterionreads the authored pair,MmoBountiesanswers by quest id only, andZoneScopeIndexrecomputes off the engine's index. Tests:ShippedQuestContractTest,ReachLevelAliasTest,SkillLevelChannelsTestand a package-privatequest/QuestFixturesare new,CommonQuestFoldTestandObjectiveLevelSugarTestare deleted with their subjects, and the rest are re-expressed over the shared model. The headless boot publishes every quest from ziggfreedcommon alone, with no "both publish the quest" line and an audit of zero errors. The ACHIEVEMENT side still runs its own fold and twin (achievement/CommonAchievementFold, the MMOAchievementDefinition), so the boot log's "both publish the achievement" and milestone warnings are real and stay as the honest signal until that fold is retired the same way. /mmomigrateand the web converter agree on the new rows.BootHandledgains a FOLDER rule (isOwnerQuestFile,OWNER_QUEST_FILE_DETAIL,OWNER_QUEST_RESIDUE_DETAILmirrored byfamilies/progression.tsand pinned from both suites;canonicalNameOfResidueanswersquests/x.jsonfor the renamed file),MigrationClassifierfiles an ownerquests/**.jsonas boot-handled, its.legacyresidue and a packServer/MMOSkillTree/Quests/file as jar-handles-it (LEGACY_PACK_QUEST_DETAIL), andBootHandledCoverageTestlearnsrenameInPlacecall sites and folder tokens. The webconvertQuestBodyperforms the same mapping (visibility,Lore,Indicator,TurnInAt,HideLockedSteps, a structuredrequiresblock, authored booleans, the disable stub as{"Enabled": false}), routes every mods-folder quest tomods/ziggfreedcommon/quests/, moves a shared-shape owner file across as it is, and reports a.json quests/as boot's own leftover; pack zip quests are unchanged, and the converter page says so..json.legacy - Tests:
quest/QuestCodecDecodeTestis re-targeted asmigrate/LegacyQuestConverterTest(every case kept, asserted on the converted body and on the sharedQuestDefinitionit decodes to, plus the new gaps); addsmigrate/LegacyQuestCorpusTest(every legacy quest fixture in the migration corpus converts, decodes and folds) andconfig/LegacyQuestOwnerMigrationTest(temp folders throughQuestOwnerLayers.setDirectory: the ordinary move read back by the shared store, the idempotent second run, a sub-folder, a refused file left whole, an existing target never overwritten, an unreadable file named, a shared-shape file moved byte for byte, the echo retired);SharedLayoutExportTest's quest fixtures ride the converter andbuildPackEntriestakes the owner bodies by id;QuestNpcAliasTestbuilds its legacy quests through the converter. The in-game help page's quest card (ui.help.quests.how, nine locales) names the new folder. Mmo_Hub_Intro's temple pointers find their screens by tag, and its footer includes its tail. Thetemple_pointersgroup carries"On": {"Tags": ["Temple_Landing"]}and the three landing screens (temple_talk,mastery_brief,sawyer_brief) carry that tag, so none of them names the group any more and a landing screen added later gets both pointers by carrying the tag;hub_footerwrites its two quest lines and"Include": ["open_menu"]instead of restating the temple line, so that twelve-line option exists once in the file. What a player sees is unchanged: the pointers still sit above the footer on every landing screen (Ziggfreed Common 2.1.0 splices a placed group after a screen's own lines and before the groups it names), pinned label by label in Ziggfreed Common'sDialogueAuthoredFixtureTestover a mirror of this file. Needs Ziggfreed Common 2.1.0.pages/render/MmoChipReadingkeeps only what no kind file can say. The all-skills XP branch is gone (the kind file's own template landsSkill: ALLand the four category tokens on the shippedreward.xp.all/reward.xp.family, soreward.line.xp_allretires from all nine locales), the/givebranch is gone (Ziggfreed Common's item form now reads aCommandreward's give line through the sameCommandRunner.readGivethe fit probe uses), and the ability-mod icon is read off theMmo_Ability_Modkind file instead of a Java twin (ABILITY_MOD_ICONis deleted). What stays each carries its reason: a custom skill's XP line off the skill REGISTRY, the boost sentence (a key family of differing arity), an ability-mod title out ofAbilityModCatalog, and the/mmoboost giveline plus the neutral label for a command nothing parses (this mod's policy over the library's drop rule). The/mmoboost givereader moves off the command class intoboost/BoostGiveLine(aRewardPreviewBuilder,MmoChipReadingandValidationChecksshare), so a chip, a preview and the audit read a line with no server, and the RewardSpec command reading is a live test instead of a code-inspection note.ShopEntryIconResolverderives an offer card's icon from the shared chip reading rather than a per-kind ladder of its own, so an all-skills XP offer with no explicit icon now shows the crystal every other surface draws it with.- The RPG Stations api pin moves to 0.8.0 (contract 8 fires
StationOutputProducedEventonce more per grant pass, for the paid stacks; the stations bridge does not listen to that event, since the engine's own dispatch already carries the moment into the runtime the MMO reacts on). The manifest'sZiggfreed:RpgStationsbound is unchanged; RPG Stations 1.0.0 ships beside this version, so the bridge is live from the first boot on a server that installs it. - Adds
player/FlairPersistence, the flair twin ofProgressPersistence: one listener on ziggfreed-common'sZigFlairChangedEvent, registered once fromquest/ProgressionBootstrap, that marks theSTATIONSdomain dirty and flushes it for the player the event names.PlayerDataRepositorygains amarkDirtyAndFlush(PlayerId, domain)overload (thePlayerRefone delegates to it), the shape a listener holding a uuid picks.FlairPersistenceTestdrives it againstFakePlayerDataRepository, whose sessions now remember the domains their lastflush()committed. SharedSchemaJson.durationGroupdecomposes a duration intoWeeksbeforeDays, so a fortnight is{"Weeks": 2}wherever one is written (a legacycooldownSecondsbecomes theRepeatgroup'sCooldownthat way). The two runtime-Periodtest sites move toReset.daily()/periodMs()with their assertions kept.MmoMomentReactionsgains one station reaction for both kinds, gated on a payload being present (the station payload types are RPG Stations' own and are not linked);StatisticsServicemapsSTATION_OUTPUTtostation_items;TargetNameResolverandObjectiveTextRendererknow the new kind.- Tests:
i18n/LangFileChecksis the one set of.langrules, extracted fromLangFileIntegrityTest(which keeps its cases and adds balanced/pairs on thedialogue.*family) and shared with the newPackLangFileIntegrityTest, which walks every checked-out pack'sServer/Languages/against that pack's OWN en-US (skipped when no pack is checked out; the deprecated translations pack is excluded)./*.lang DialogueContentResolutionTestdiscovers every pack'sServer/ZiggfreedCommon/Dialogues/**(theunreleased/folder and the translations pack excluded; the quartermaster shape test stays pinned to zone-quests-pack).ObjectiveKindVocabularyTestpins that every producible kind has anobjective.text.template in en-US and that each RPG Stations kind file agrees with the enum's flags (skipped when the sibling is absent).QuestServiceClassifyTestcovers both authoring shapes and every threshold edge,ShippedQuestContractTestfolds a calendar daily and anEvery {Weeks 2}window through the shared fold to their classification, andStatisticsBlobCompatTestasserts the fourteenth field decodes empty off the untouched golden blob. - Adds
dev-server.ps1, a headless boot-check of the built family on the installed server: it stages every built jar and pack zip (plus the live Mods folder's third-party companions) into an isolatedrun/mods/, boots with no client, waits for theHytale Server Booted!banner, confirms every staged plugin and pack loaded, sorts SEVERE / WARN / stack traces into family and engine, can type console commands, stops cleanly and exits 0 or 1 (run/last-check.jsonkeeps the verdict;-ValidateAssetsruns the engine's own validate-and-exit pass;-Jdwpopens a debugger port). Addsdoctor.ps1, a one-screen environment diagnosis (JDK, install and version pin,HYTALE_MODS_DIRduplicates, submodules, shared source, pinned jars, built artifacts), andtools/dev-env.ps1, the resolver both share. The first boot found the two split dialogue lines above, a third-party mod broken by server 0.6.3, and an engine ordering bug on pack unregister at shutdown. build.gradleandapi/build.gradlefall back tocom.hypixel.hytale:Server:from the publicmaven.hytale.comwhen no launcher install exists athytaleHome(the published jar is byte-identical to the installed one), so a fresh clone or a CI runner compiles; the installed jar keeps precedence, and the doctor warns when the pin drifts from it.MMOSkillTreePlugin.setup()is an ordered call list, not a 2,000-line body. Splits the plugin's boot wiring into per-domain bootstraps on the existingAbilityModBootstrap/DbBackendBootstrap/StatSubstrateBootstrapprecedent -config/OwnerConfigBootstrap(the two owner-config phases),content/reward/CommerceBootstrap,dialogue/DialogueBootstrap,quest/ProgressionBootstrap,asset/AssetTypeBootstrap,boost/BoostBootstrap,integration/IntegrationBootstrap,ability/AbilityBootstrap,interaction/InteractionBootstrap,command/CommandBootstrapandevent/SystemBootstrap(component types + systems) - and the four inline player/world lifecycle handlers intoplayer/PlayerConnectFlow,player/PlayerReadyFlow,player/WorldLifecycleFlowandplayer/PlayerDisconnectFlow. The anonymous placed-block policy and the granted-ability-mod producer lambda become the namedworld/MmoPlacedBlockPolicyandability/GrantedAbilityModProducer, and the debug asset dump moves toutil/AssetDebugDump. Every moved body is verbatim and the boot order is unchanged; the in-method ordering constraints are restated at thesetup()call sites, so a leg that adds a config, command, page or system edits the owning bootstrap instead of the plugin file (2,799 lines down to under 500, 270 imports down to 49).RepoHygieneTestnow guards the plugin file's shape as well as its content. Two new rules holdMMOSkillTreePlugin.javato the wiring-root discipline the split above established: the whole file stays under a 700-line ceiling (roughly 1.5x its post-split size, stating the current size when it trips), andsetup()'s own body stays free of decision control flow - the samefor/while/do/switch/else/non-guard-if/non-null-default-ternary lexicon ziggfreed-common'sRootRegistrationOnlyTestuses over its own wiring root,try/catchand guard-ifcarved out the same way, escaped by a// ROOT-LOGIC-OK:marker (reason mandatory) rather than silently. Nothing else stopped a later leg from inlining straight back in.SkillComponentstops growing at 2,692 counted lines (BUILD-ENFORCED).RepoHygieneTestholdsdata/SkillComponent.javato that ceiling, so a new bit of per-player state is a codec-persisted component of its own in its domain package (ziggfreed-common'sHudPreferenceComponentandZigFlairComponentare the shape: one component, onePlayerDataDomain, one persistence listener); a member that genuinely cannot live anywhere else is headed by// SKILL-COMPONENT-OK:, which exempts that one member from the count. Moving fields OUT needs no marker.- The build and the boot check both read the
.uifiles the server never will. A malformed Custom UI document reaches a client because nothing in the pipeline looks at one: the server treats a.uias an opaque path and ships it, the engine's validate-and-exit pass walks only the registered codec asset types, and the failure lands on the first player to join (the client refuses to load and the server logs a cancelled join).RepoHygieneTestsweeps every.uiin this jar and in every sibling mod for illegal element ids (a letter followed by letters and digits; an underscore ends the parse and fails the whole document) and unbalanced braces, anddev-server.ps1reads the archives it staged underrun/modsand refuses to launch on the same two faults, which catches what a source-level test cannot: a third-party companion, a reused-Stage:$falsedirectory, a build output that drifted from what produced it. Thetesttask declares the sibling mods' sources as inputs, so editing a sibling and building here no longer leaves:testUP-TO-DATE with every family-wide sweep silently skipped. hytaleServerVersionreads0.6.4, the version the installed jar reports (the pin had said0.6.3, so a machine with no install compiled against a different server than this one runs, anddoctor.ps1had been warning about the drift); the shared-source checkout moves to the 2026-09-07 sync. Verified on the fresh-clone path:compileJavaagainstcom.hypixel.hytale:Server:0.6.4frommaven.hytale.combuilds with no install found.- The displaced root is read off the item's own interaction map rather than through the context, which answers with the player's claim; the injected trigger roots are recognised and skipped so a press can never route back into the node that fired it, and a carried block is skipped because the engine reads a separate carry map there.
WaitForDataFromis deliberately unchanged - the node has noNextfor an outcome to gate, and the item root it may fire arrives as its own synced chain, so waiting would stall every ability press for nothing. RepoHygieneTest's number rule caught the coordinate readback in/mmonpc place; it carries a// NUMBER-OKmarker as an all-English operator console line, which is the case the escape hatch exists for.MmoRoleTemplateParameterTestasserted one parameter set across both role templates, which is no longer true of either. The expectation is per-template rather than loosened, so each stays exact.ObjectiveKindVocabularyTestguards the other direction too: every built-in kind the shared registry seeds must have anObjectiveTyperow here, named by id when one is missing, because the folds drop a step of a kind this enum lacks with nothing but a boot line.StatisticsComponentgains aPerEncounterDefeatedbreakdown field beside the twelve; an older blob decodes with it empty, and the golden-fixture test is untouched.- The placed-block move needs ziggfreed-common rebuilt and reinstalled alongside the jar: the
Policyinterface dropsstrict()/blockExpireMinutes()and gainsguardsPlacementsBy, andPlacedBlockSectionmust register on the chunk-store registry BEFORE any world loads (PlacedBlockBootstrap.setupPlacedBlockLedger, ahead of the recorder). A registration failure leaves the type unset and every read answers "not placed" - the wrong answer, but the safe one, since the alternative refuses every break on the server. PlacedBlockLedgerTestis replaced byPlacedBlockSectionTest(11 cases: bit-per-block including both ends of a section, spend-clears, lazy allocate and release, clone isolation, and the serialize/deserialize round trip a chunk save and load puts it through). The ledger's BLOCK path now needs a live world and a loaded chunk, so it lands in in-game smoke like the rest of the ECS plumbing;PlacedGuardProducerTestkeeps its item half fully asserted and its two block cases are gone. A bare unit JVM cannot drive the engine's chunk save and load, so that round trip is an in-game check rather than a test here.- Twelve pieces of provably dead code are gone, each re-verified by grep at delete time:
integration/JarClassIntrospector,integration/PluginDetector,integration/PluginIdDump(zero-caller private-ctor classes),pages/render/PinIconRenderer,config/AbstractPackLayeredOverrideConfig(zero subclasses; its one javadoc mention incontent/gate/GateFactorsnow namesMasteryConfig#applyPackLayer, where a track id is actually folded to lower case),reward/effects/ComboFinisherEffectand its unused import inreward/RewardEffectRegistry, four unreferenced.uifiles (AbilityPickerRow,CombatStatRow,RewardItem,RewardTierRow), andMMOSkillTreePlugin'srunSync(Runnable)andequipStatBridge()getter (theequipStatBridgefield stays; the getter's javadoc claim thatCombatDamageEventSystem's DOT branch reads it was false). - Four names line up with the rest of the tree:
command/MMOConfigCommandisMmoConfigCommand(the/mmoconfigcommand name and every subcommand are unchanged),io/AbilityInteractionInjectormoves intoability/and the emptiedio/package is gone,util/MmoFactorContributionsmoves besidecontent/gate/GateFactors, the vocabulary it is the other half of, andpages/render/SkillDisplayUtilsisSkillDisplayUtil. ability/is seven subpackages, not 76 files at one level. Pure package moves - no class renames, no behaviour change:cast/(the activation and dispatch pipeline),input/(which key, slot and item fires what),modifier/(the per-cast modifier framework, its sources and its read side),effect/(stateful runtime an ability spawns),damage/(schools, causes, damage sources, attribution),catalog/(everything that reads the whole authored catalogue) andutil/(stateless helpers), beside the pre-existingevent/; the package root keeps onlyAbilityBootstrap. Six members widen to public, each javadoc naming its cross-package caller (ChainDescriptionRenderer.composeLine,HitDamageBonus.DISPLAY_CATEGORIESplus itsfixturefamily,SchoolCauseGenerator.ABILITIES_DIRandinstallForTest, and theAbilityJsonFixturestest helper, which stays at the test-package root); the 49-test package moves with its subjects, andAbilityModifierService.classifyProducerClasskeeps itsmastery./skilltree.-only FQN match, pinned byModifierSourceAttributionTest.ToolStatDescriptionGeneratorTestis replaced byitem/StatLineFormatTest(the composition and Utility-map-resolution cases go with the deleted path; the parser, formatter and color cases are kept as they were).StatTooltipLangGenerator.labelFor, whose only caller was the deleted generator, is gone too. ziggfreed-common'sStampTooltipdrops its.description.basebranch, so a stamped item always nests its own description key when the catalogue has it and it carries no markup; that jar needs rebuilding alongside this one (no separate version bump: it rides the 2.1.0 this cycle pins, and the publicapplysignature is unchanged).- The hyDungeons bridge is deleted.
integration/dungeon/{DungeonIntegration,ProvingGroundsRegistrar,ReflectiveDungeonBuilder}andcommand/ProvingCommand(/proving,/pg) are gone with their boot wiring: they reached a separate, never-shipped dungeon engine over reflection and registered one ungated, empty arena nothing could enter. Instanced content builds on Hytale's own Instances builtin through ziggfreed-common's instance layer instead, and bosses on the Update 6 EncounterManager.
[1.6.0] - 2026-08-31
Held. The 1.6.0 work is landed andgradle.propertiesreads1.6.0, but nothing ships until the combined in-game smoke pass clears. Landing is not releasing.
Breaking changes
- 1.6.0 requires a Hytale Update 6 (0.6.x) server, with updated content packs to match. The jar's manifest targets
>=0.6.0-pre.13 <0.7.0, so it does not load on an Update 5 server. Every pack this project ships is already rebuilt for Update 6; an older pack zip still carrying the retired usable-block flag still loads, with one harmless per-asset warning in the server log (/mmomigrateand the web Migration Converter both point at the exact line to delete). mods/mmoskilltree/abilities.jsonis retired (hard break). The legacy JSON-blob ability override format is not read at all. On first start after upgrading,LegacyAbilitiesFileMigrationruns at plugin setup (before the asset pipeline exists), moves any file found atmods/mmoskilltree/abilities.jsonto a timestamped file undermods/mmoskilltree/backup/, and logs a warning naming the replacement. It never reads the file, never deletes it, and never overwrites an existing backup on a repeat restart. Re-author customizations asability-overrides.jsonentries (a partial per-leaf overlay) or as a content-packServer/MMOSkillTree/Abilities/*.jsonasset; the server-wide ability toggle plus dodge/counterattack timing moved verbatim to the newability-settings.json.config/ActiveAbilitiesConfig.javais deleted.- The shared progression schema hardens before its first release (hard breaks made inside the 1.6.0 surface before it shipped; no public content authored it yet). An achievement's
Criteriais a MAP keyed by criterion id, exactly like a quest'sObjectives: the key is what progress is stored under, aParentchild retunes one criterion by key and keeps the rest, and re-authoring a file can never silently re-point a player's tally (the old positional array made any insert or reorder a data migration). Every progression schema pays through ONERewardsgroup with two buckets -Autolands the instant the content settles,Claimwaits to be collected - replacing an achievement'sRewards/ClaimRewardspair and a quest'sFlow.AutoClaimswitch: a quest with anything inClaimparks for the player, one paying onlyAutosettles on the spot, and the point milestones already speak exactly this shape. A quest'sVisibilitygroup folds into the sharedListing(Hidden,RequirePrerequisites, now on achievements too, where hidden-until-qualified is honored by the shared engine), quests gainListing.Icon, and an NPC placement'sRequires.Conditionsis spelledRequires.Factors, matching the requirement block everywhere else. All 167 jar achievements, 10 jar quests, 5 milestones and the shipped pack content are rewritten in place, behavior-preserving;/mmomigrate's exporters emit the new shape (a converted legacy pack keeps its positional progress keys, so deploying an export starts nobody over). - Objective matching is ONE forgiving rule for every kind of content (a deliberate reversal of the earlier both-dialects position, taken while nothing shipped depended on the strict one): targets compare case-insensitively, an empty target matches everything under every mode, and an empty qualifier matches only an unqualified event. Quest targets become strictly MORE permissive (no content can lose a match), the two jar quests that leaned on the contains-empty-string accident now mean match-all in so many words, and the per-engine
MatchFlavorknob is deleted end to end. DEAL_DAMAGEcounts damage, not hits, and both damage statistics record the same unit. The combat dispatch fires the real post-mitigation damage of each hit as the amount, so a criterion authored "50,000" means fifty thousand damage - the unit its numbers were always written in.damage_dealtanddamage_takenare both recorded at the same pipeline stage with one rounding, each totalling the HP a health bar actually lost, so the two lifetime numbers can be compared. The five shipped criterion amounts (10k/50k/100k/250k/1M) stand as authored: the unit fix realizes them (retune factor 1).Scoring.CountsTowardTotalis the ONE does-not-count switch. A feat of strength no longer forces it silently: feat is a LISTING treatment (its own earned-only shelf, unpinnable, hidden until earned) and the 16 shipped feat files authorCountsTowardTotal: falsebeside the flag./mmomigratewrites the leaf out for a converted legacy feat, keeping the totals its pack was balanced around.- A custom skill id may not contain
|or:(refused at every registration point). The per-player save format packs the custom-skill maps with exactly those delimiters and no escaping, so such an id would corrupt every later entry of a player's custom-skill data on save;CustomSkillContentValidatorreports it as an error andSkillRegistryrefuses the fold with a WARN naming the fix. The mod-side reserved set gains#too, aligning with the shared stores' five so the two authorities can never disagree. - Every offline queue and cache is keyed by UUID, because a username is not an identity. The leaderboard cache re-keys itself at load (legacy name rows resolve through the player directory; what cannot resolve is dropped with one INFO and refills on that player's next login - a renamed player keeps their totals, a reused name no longer inherits a stranger's position), and
pending-xp-resets.jsonfollows the pending-rewards doctrine verbatim:/bulksetxpresolves an offline target at queue time and refuses OUT LOUD a name no player has ever connected under, delivery drains by the connecting player's own UUID, and a legacy name-keyed file carries across once with a WARN per unresolvable name. The reset spool also writes itself under a version envelope now ({"version": 1, "players": {...}}): a bare 1.5.x map still reads as version 1, and a file declaring a NEWER version than this build reads is left unread with one warning naming the file and both numbers, instead of being half-parsed and clobbered on the next save. - The player-database schema is version 2, and it refuses orphaned pre-1.6.0 rows. The never-read
player_leaderboardtable andplayers.online_instancecolumn leave the DDL (disconnect stampslast_seen_at, which a staleness read can build on), and a database still holding rows under the retired QUESTS/ACHIEVEMENTS domains refuses to start, naming the count - starting anyway would burn each player's one-time progression-migration claim against empty legacy components and strand the real data forever. No such database is known to exist (the backend shipped dormant in 1.5.x), so the guard is the whole story: no automatic carry. - Boot loads the player directory before the leaderboard cache and the pending XP resets, so a 1.5.x name-keyed
leaderboard-cache.json/pending-xp-resets.jsoncarries across on the first start instead of being emptied; a pending reset whose name nobody resolves is preserved in a timestampedbackup/copy with one notice. Every offline name resolves through the player-data seam (resolveUuid), so a database backend answers from its own players table; the leaderboard page keys its lines by UUID. Casting.HeldWeaponCategoryis replaced byCasting.HeldWeaponCategories(hard break, no shim). The singular field is gone; the array-only ANY-of field replaces it, swept across all 27 authoring sites in one pass with no dual-support alias, matching this cycle's schema-rework posture. A pack or override still authoring the singular key fails to decode that leaf.- An
Effectskey inside anability-overrides.jsonentry is a load-time error naming the replacement field,Body- abilities compose their whole cast chain as one native interaction chain referenced by a singleBodyid. A cooldown/cost/presentation-only override needs no change. - Mastery tracks are a structured asset, and the mastery template DSL is removed. One PascalCase file per track at
Server/MMOSkillTree/Masteries/decoded by the one.json MasteryAssetcodec: the sharedCostprice group, the sharedRequiresgate grammar, aNodesmap merged per node id, and nativeParentreuse againstAbstractbases. A whole family is oneServer/MMOSkillTree/MasteryGenerators/file expanded over the shared generator core (token substitution reaches node-id keys, gate Params and typed values; an authored file of a member's id always wins). The.json extends/paramsmastery template DSL, theMasteryTemplatesstore and the hand parser are removed; a pack still shipping template files gets a boot warning naming the replacement. mastery.jsonschema v3 (hard break). A track override is now a partial body in the pack schema, merged per leaf over the loaded track of its id ({"Enabled": false}switches one off); a pre-v3 file still carrying old-format tracks is moved to a timestampedbackup/copy with one boot notice naming the replacement. The owner-export serializer writes the authored schema and is lossless for every modifier field, the TRIGGER payload and ADD_STEP splice fields included.- The interactive work-station engine does not live in this jar. It ships as the standalone RPG Stations mod (its own first public release; see its own docs and changelog). The MMO jar keeps only the soft
integration/stations/bridge (XP, luck, mastery, statistics, summary-panel enrichment); the per-player unlocked-flair set is ziggfreed-common'sZigFlairComponent, persisted by the library so a server running RPG Stations without the MMO still remembers what a player unlocked. Neither mod hard-depends on the other. Stations stay fully dormant without RPG Stations plus theskill-stations-packcontent pack installed. - SMITHING is no longer a built-in skill. It is removed from
SkillRegistry.BUILTIN_SKILL_NAMES/BUILTIN_SKILL_DATAand ships as a pack-authoredCustomSkillAssetin the MMO Skill Stations Pack (skill-stations-pack) instead. A one-way, idempotent legacy read path inSkillComponentpreserves any Smithing XP a player already earned;data/SkillTypekeeps its SMITHING constant so the frozenMMOSkillTreeAPI.getSkillTypes()signature is unchanged. mods/mmoskilltree/spawn-hub.jsonandquest-givers.jsonare retired (hard break). Both shipped in the 1.5.x public line, so they exist on live servers.config/LegacyNpcPlacementMigrationruns at pluginsetup()(purejava.nio.file, before the asset pipeline exists, never throws), renames each present file IN PLACE to(disambiguated with a numeric suffix so a repeat boot never overwrites an earlier copy), and logs ONE warning naming the replacement. It never reads and never deletes them. The internal.legacy spawn-hub-placements.jsonspawn/idempotency registry is retired the same way. Migration: an NPC's position, the worlds it stands in, what pressing F on it does, and whether it appears at all are now ONE asset,Server/ZiggfreedCommon/NpcPlacements/; owner on/off overrides live in.json mods/ziggfreedcommon/npc-placements.json({",": {"enabled": false}} "*"for all) and/mmonpc list [--arg1=placements|structures|markers|channels] | enable | disable | reconcilemanages them live (one listing verb, four nouns, defaulting to placements).Server/MMOSkillTree/QuestGivers/*.jsonkeeps a WARN-only stub store for one cycle (asset/type/LegacyQuestGiverAsset), because an unregistered store logs NOTHING at all and a third-party pack still shipping them would fail in total silence. SeeCONTENT_PACKS.md's NPC placement authoring section.- The conversation screen is the library's, and there is no per-mod dialogue bundle any more (hard break for a mod that registered one).
NpcDialogueDepsRegistry,DialoguePageDepsandDialogueContextFactoryare deleted, along with theDepsKeyfield onZigOpenDialogueandZigPlacementInteract. They existed so each mod could hand the page its own engine, resolver, namespace and providers; on a server running two talking mods the un-keyed default slot was a genuine collision, the later mod to start won it, and every conversation on the server - including the other mod's own - was then painted through the winner's namespace, so every authored key missed and every line read as a raw key. The page composes from process-wide state instead: the oneDialogueEngine.shared(), the one conversation store, payloads resolved by class throughDialoguePayloads, the character's name from its role'sNameTranslationKey, and authored keys resolved throughContentKeysin the namespace of whichever mod ships them. Nothing is registered to make a conversation work, so nothing can be registered wrongly. Migration: a consumer deletes itsDialoguePageDepsconstruction and itssetDefaultcall and keeps only itsDialogueEngine.registerShared/DialoguePayloads.registercontributions, which were already additive. mods/mmoskilltree/dialogues.jsonis retired (hard break). Conversations are authored in one folder every mod on the server reads back, so the owner layer over them belongs beside it:mods/ziggfreedcommon/dialogues.json, a bare map keyed by conversation id whose entries merge LEAF BY LEAF over the stored conversation (restate only the nodes you change, exactly the way its four sibling owner files andmastery.jsonoverlay), with$-prefixed keys reserved ($SchemaVersion, absent means 1).config/LegacyDialoguesFileMigrationrenames the old file in place to.legacyatsetup()with ONE boot WARN naming the replacement, and never reads it. Nothing is carried automatically: the two files hold the same bodies in the same shape, so moving them is a copy an owner can check rather than a server's conversations quietly changing hands.config/DialogueConfiganddialogue/CommonDialogueFoldare deleted;/mmomigrateand the docs-site Migration Converter both know the retirement, and the converter carries an uploaded file's entries across as shared-store pack assets.- A conversation says what note it shows and what its own exit row reads. Two new authored leaves on a dialogue asset.
"Header": ["ActiveObjective"]names the sources for the one-line note under the speaker's name, in order, first one with something to say wins; naming none - the default - shows no note at all, so a character who should never mention quests does not.ActiveObjectiveis the library's own, computed from the shared quest engine and worded exactly the way the quest tracker words it, sodialogue/MmoDialogueHintis deleted and no mod ships a hint provider."Chrome": {"FarewellKey": ..., "MissingKey": ...}gives a character its own words for the exit row and the nothing-to-say line; unauthored takes the library's, which now ships in all nine locales asziggfreedcommon.dialogue.*. The MMO's threeui.dialogue.*keys are removed from all nine.langfiles, since the library carries the same wording. WorldRulesAsset's flatMatchstring is retired (hard break). A rule targets aWheregroup instead:"Where": {"Match": ["*Dungeon_*"]}for a world-name pattern (a BARE word being an exact name) or"Where": {"GameplayConfig": ["ForgottenTemple"]}for the world's own stable config key. Both jar rules and any ownerworld-rules.jsonare rewritten. A file still carrying the retired key is decoded solely so it can be reported: ONE boot WARN per rule id quotes the old value, prints the replacement to paste, and states plainly that until it is rewritten the rule matches no world.WorldRulesMatcherkeeps its precedence ladder verbatim and sources each rank fromWorldSelector.match, so the winner is unchanged for every pattern kind and for every subset of a mixed rule set (asserted by a subset sweep inWorldRulesMatcherTest).- A pre-release naming sweep straightens three internal vocabularies (all inside the 1.6.0 surface before it shipped, or invisible to saved data). The
/mmohudreply keys move fromcmd.mmohud.*tocmd.hud.*in all nine locales (the command name and its permission node are unchanged). The two mastery stat-mirror modifier keys spell their segment with an underscore -mmoskilltree:mastery_loot/mmoskilltree:mastery_damage- and both sit in the reserved-segment registry; these keys live only on runtime entity-stat maps that are rebuilt idempotently every boot, so nothing persisted carries the old spelling.HudKeymoves from the frozen-residuedata/package tofeedback/besideNotificationCategory, the descriptor family it parallels (a compile-time move; the string-keyedMMOSkillTreeAPI.isHudVisibleseam is untouched). And the jar's optional-dependency range onZiggfreed:RpgStationsnarrows from<1.0.0to>=0.1.0 <0.3.0, the range the compiled api contract actually supports. - A second pre-release sweep settles the ability-family schema spellings (every one inside the 1.6.0 surface before it shipped; the 1.5.x formats these fields descend from retire wholesale above, so no shipped file authors them). An ability's display keys are
Identity.TitleKey/Identity.DescriptionKey, the same pair anAbilityMods/*.jsonasset carries, and an ABILITY_MOD reward names its display key with thetitleKeyparam (the 1.5.xnameKeyreward-param spelling still converts on the sanctioned legacy read); XP routing filters held-weapon resolution withXpRouting.ExcludeSkills; a passive trigger'sOnvalues are PascalCase (HitDealt,Kill,Finisher,DodgeArmed) and its weapon gate is the ANY-of arrayWeaponSkills; a cast node's per-chain cooldown leaf isCooldown.LocalMs, milliseconds like every other duration; the cast event carriescastAtMs; and theCostgroup is exactly{Stat, Amount}(no side list of secondary costs). A class'sGrantsnests one{Allow, Deny}group per governed surface (Abilities,Mastery,SkillRewards) instead of six flat whitelist/blacklist leaves, so a pack overlays one list per leaf. ThexpGainmodifier paramKey retires from the canonical vocabulary (XP routing owns that job; the mastery XP mirror goes with it), and three validate-report domains spell themselves like their siblings:item_stats,damage_school,station_bridge. - A world rule's gameplay knobs move into one nested
Rulesgroup. AWorldRules/*.jsonfile (and an ownerworld-rules.jsonbody) now reads WHERE / WHETHER / WHAT:WhereandEnabledstay top-level, and the six gameplay knobs (XpMultiplier,AbilitiesEnabled,CombatBonusesEnabled,StatBonusesEnabled,DisabledSkills,EnabledSkills) sit together under"Rules": { ... }. Both jar rules and the World Rules admin page write the nested shape. A file still authoring a knob flat at the top level KEEPS WORKING this release: the flat leaf is honored (a nested leaf wins over its flat twin), with one boot WARN per rule naming the group to move it into - the nested spelling is the one a future release will read. - Authored dialogue FLAGS are retired (hard break).
SetFlag,Flag, andNotFlagleft the dialogue engine's pre-seeded generic types. Migration: a first-visit greeting (aNotFlagguard plus a matchingSetFlag) becomes oneOnceknob on theStartentry; an option guarded by a hand-composedreward:key becomes: : "Once": trueon the option; any other named state becomes a declaredMemoriesentry read by bare name. The flag STORE and itsq:/w:key composition survive as internal plumbing only, so/mmoquest resetclears exactly what it cleared before and no author ever sees a composed key again. The MMO reward sugar's own once-guard is re-keyedRewardOnceso it sits beside the engine's option-levelOnce; a bare"Reward"still defaults to once. The per-consumer engine builder means this is not a ban: a consumer that genuinely wants raw flags registers its own types. - A placement with no
Identity.NpcIdIS its role now, not its file name (hard break). The id content binds a character by used to default to the PLACEMENT id, so two standings of one character had to be tied together by hand with an alias. The default is theIdentity.Rolethe placement names, which says the true thing: two placements of one role are two standings of ONE character, and a quest bound to it is offered, credited and handed in at either with nothing authored twice. Authoring anNpcIdstill opts out and makes a standing a character of its own. Migration: a placement whose authoredNpcIddiffered from its role id keeps working untouched; one that merely restated its file id should drop the leaf, and any content binding that file id re-points at the role id. The jar's own placements did exactly that - onlyMmo_Hub_Forgotten_Templestill authors an id, because it is the only one that has to be a character of its own. A placement naming neither an id nor a role now answers to nothing rather than to a character that was never in the world. - Character ids and the
npcs.keys built from them are PascalCase (hard break). Authored ids spell the Hytale way, matching the file they come from:.name Mmo_Hub,Mmo_Hub_Temple,Mmo_Mastery_Trainer,Guide_Wilds. The jar's base role files spell theirMmo_prefix Pascal too (Mmo_Hub.json,Mmo_QuestGiver.json,Mmo_Mastery_Trainer.json, and the trainer'sServer/Models/Mmo_Mastery_Trainer.jsonmodel), one spelling with theTemplate_Mmo_*files, the placement ids and every alias; role lookup is case-insensitive, so a placement or pack still naming the oldMMO_*spelling keeps standing its NPC up. Looking a character up ignores case everywhere (the giver index compares ids case-insensitively now, where it used to demand an exact match), but the DISPLAY key read from an id does not, so the jar'snpcs.mmo_hub.name/npcs.mmo_hub_temple.name/npcs.mmo_mastery_trainer.name/.hintkeys are renamed to match, in all nine locales. Migration: a pack overriding one of those keys renames its own copy; a pack shipping a giver renames itsnpcs.key to match the id, and a.name TALK_TO_NPCobjective target must spell the character id exactly as the placement does, since objective targets are compared case-sensitively like every other asset id. - An unregistered
Requiresfactor now fails a bounds-less placement condition. A placement condition naming a factor nobody registered used to resolve0.0and therefore PASS a condition with noMin/Max, so a typo'd factor id silently gated nothing. Every factor site now fails closed on an unresolvable value, which makes a bounds-less condition a genuine presence check ("only where that mod is installed"). A condition that already carried aMinbehaves exactly as before. - The achievement listener registry is DELETED, not deprecated (
api_version1.6.0).MMOSkillTreeAPI.addAchievementListener(Consumer),removeAchievementListener(Consumer),fireAchievementEvent(PlayerAchievementEvent)and the api-module typecom.ziggfreed.mmoskilltree.api.events.PlayerAchievementEventare gone; a call to any of them does not compile against 1.6.0. Migration: achievement moments are ordinary Hytale events now, dispatched on the world tick thread through the engine event bus, so a subscriber registers them exactly the way it registers any other server event and needs nothing fromMMOSkillTreeAPIat all. The three replacements live in ziggfreed-common'scom.ziggfreed.common.achievement.eventpackage, which every server running this mod already loads:AchievementProgressedEvent(achievement id, criterion id, current and required amounts, whether that criterion just finished),AchievementUnlockedEvent(achievement id, its points, whether rewards are still waiting to be collected), andAchievementClaimedEvent(how many rewards were granted, queued for a later attempt, and failed). All three name the player byUUIDrather than aPlayerRef, and all three carry the achievement's authoredtags()- its category, its subcategory, and the two flagsserver_firstandfeat_of_strength- so a listener can branch on what KIND of achievement moved without asking this mod for its catalogue. Before:MMOSkillTreeAPI.addAchievementListener(event -> { if (event.getPointsAwarded() >= 100) announce(event.getAchievementId()); });After:getEventRegistry().registerGlobal(AchievementUnlockedEvent.class, event -> { if (event.points() >= 100) announce(event.achievementId()); });. The old payload's running-total field has no direct replacement on the event, because it was only ever a convenience: read it when you need it withMMOSkillTreeAPI.getAchievementPoints(store, ref), which is unchanged. Every achievement READ on the API is kept verbatim (getAchievementComponent,isAchievementUnlocked,getAchievementProgress,getAchievementPoints,pin/unpin/getPinnedAchievements,getServerFirstClaim), andPlayerGainXpEventplusaddXpListenerare unaffected. The full recipe also ships in the jar's own sources atapi/src/main/java/com/ziggfreed/mmoskilltree/api/events/package-info.java. - A reward's
typeis an open kind id, not a fixed enum (behavior change). Any doc or content sayingtypemust be one ofCOMMAND/XP/BOOST_TOKEN/CURRENCY/ABILITY_MODis stale. An unrecognized kind is no longer silently dropped at parse: it parses and is reported by the startup audit and/mmoconfig validateasUNKNOWN_REWARD_KIND, because the mod that owns that kind may simply not be installed. A second new validator code,UNKNOWN_FACTOR_ID, reports an unresolvable factor id on arequirements.factorsentry the same way (a blank factor id warns too, since it is skipped rather than enforced). mods/mmoskilltree/luck-loot.jsonandmods/mmoskilltree/mob-drops.jsonfold into ONEmods/mmoskilltree/bonus-drops.json(hard break, converted in place). What a broken block gives on top of its own drops and what a killed mob gives on top of its own were the same question asked in two vocabularies, so they are one table where every row names the moment it answers for.LegacyBonusDropsMigrationruns at pluginsetup()before the table is read (purejava.nio.file+ JSON, never throws) and carries EVERY override across: a row that named a drop still names it, a row that stated its own odds still states them, and a pattern switched off is still switched off, including the pre-loot-formDropList/Drop/Lootable/Item/ bare-string spellings and theirChanceblocks. One INFO line per converted row names what it became; a luck pattern's minted id (and a mob row's own id) is re-filed under the shipped row'sMmo_*id when it names one (Ore_Copperlands onMmo_Ore_Copper,Trork_BonusonMmo_Trork_Kill), so the per-leaf overlay keeps landing on the row it re-tunes, and every other id carries verbatim. A legacy key was a bare substring matched anywhere in a name, so each one is carried across as the anywhere-in-the-name pattern (Trorkbecomes*Trork*) to preserve exactly what it covered. A row already present inbonus-drops.jsonis never overwritten,"__DISABLED__"is accepted on both sides, and both old files are moved into a timestampedbackup/copy rather than deleted, along with the generateddefaults-luck-loot.jsonreference echo.mob-drop-commands.jsonis untouched and remains the SIMPLE kill-only surface beside it: it stays a SEPARATE opinion consulted first for kills, so an owner who only wanted to add a command still gets the shipped table's drops beside it, and disabling a pattern there still silences both.mods/mmoskilltree/mob-drop-commands.jsonmoves to schema version 4, and two fields retire. A pool'sdelayis no longer read: a pool's commands run immediately, in the order they are written, and a file still carrying the field gets ONE boot WARN naming it.requiredTierretires outright - it was dead schema that silently disabled any pool setting it, because the built-in elite-mob system it keyed on was removed in 1.5.0 and mob difficulty lives in the standalone MMO Mob Scaling mod. Existing overrides are converted in place, and the admin page drops both inputs.mods/mmoskilltree/pending-quest-rewards.jsonretires; the live spool ispending-rewards.json, keyed by player UUID only. On first start it is drained into the new spool and retired to a timestamped backup with one notice naming what moved and what did not. A queued reward filed under a player NAME cannot be carried across (a name is not an identity), so those entries stay in the backup copy and the notice names each one: hand them out yourself, or have the player join once and re-run the command that owed them. Going forward, an offline grant with no resolvable uuid REFUSES LOUDLY instead of queueing into the dark:/mmoquestadmin completeon a player who has never connected to this server answers with a red refusal and queues nothing, and a no-command context logs a SEVERE line naming the player, the source, and the command that would have run. A player who HAS connected but is offline still queues and still receives it on next login./givepositional quantities are honored wherever this mod runs the command (behavior change). An authored command reward written/give Bob Wood_Planks 32now delivers 32 rather than 1, everywhere in the mod, because every console dispatch runs through one command primitive that normalizes the quantity. ThePOSITIONAL_GIVE_QUANTITYvalidator finding stays as a PORTABILITY warning (the engine's own/givestill needs--quantity=N), not a data-loss one.mods/mmoskilltree/elite-mobs.jsonis retired. It has been inert since 1.5.0 (nothing parses it and nothing constructs the config it fed). It is now probed for at startup and retired with one notice; it is never read and never deleted.- The three generated
_defaults/echo files retire (hard break for anyone who edited one).quests/_defaults/default-quests.json,achievements/_defaults/default-achievements.json, andachievements/_defaults/default-point-milestones.jsonwere rewritten from Java on every boot and then read straight back in as the owner layer whenever no user file existed. The built-ins are overridable assets now, so each file is renamed in place toon first start, with ONE notice naming its replacement; it is never read and never deleted, and a repeat boot never overwrites an earlier copy. Migration: read the built-ins at.legacy Server/ZiggfreedCommon/Quests/MMOSkillTree/,Server/ZiggfreedCommon/Achievements/MMOSkillTree/, andServer/ZiggfreedCommon/AchievementMilestones/MMOSkillTree/inside the jar, then override one by dropping a file carrying the same quest or achievement id intomods/mmoskilltree/quests/ormods/mmoskilltree/achievements/(a points milestone re-tunes by authoring the sameThreshold, in a pack or inachievements/point-milestones.json). Keeping a leftover echo on disk would only shadow the current built-in with a stale copy, which is exactly why it retires. - Authored dialogue
Talkis retired (hard break). The no-opTalkaction type and itsTalkoption sugar are gone, so a file still authoring"Talk"fails the read naming itself rather than silently doing nothing. Migration: credit a conversation explicitly with the newMarkTalkedaction ({"Type": "MarkTalked"}credits the NPC in front of you, alias set included;"Target": "@self"or an explicit npc id names one). There is no implicit talk credit left anywhere: the Citizens adapter's credit-on-interact hook is gone too, and an NPC with no dialogue at all credits through the newZigTalkCreditNPC action ({"Type": "ZigTalkCredit", "Npc": "on the role's own interaction instruction)."} - The default Trork kill bonus ships ENABLED.
Server/MMOSkillTree/BonusDrops/Mmo_Trork_Kill.jsonis live default content, overridable like every other shipped asset: a server that wants it off writes{"drops": {"Mmo_Trork_Kill": {"Enabled": false}}}inmods/mmoskilltree/bonus-drops.json. Its odds are deliberately conservative and luck-scaled, and its$Commentdoubles as the authoring tutorial for the kill half of the surface. - The shipped
Server/MMOSkillTree/BonusDrops/rows are keyed under ONEMmo_*convention (Mmo_Ore_Adamantite,Mmo_Vortex_Charm,Mmo_Trork_Kill, ...), since a row's id IS its owner-override key.LegacyBonusDropsMigrationre-files abonus-drops.jsonoverride still keyed by an old row id under the row's current id (one INFO per re-keyed row), and the web Migration Converter applies the same map; the nativeMMO_*stat and drop-list ids are unchanged, since gear and drop tables reference them. /mmoconfig trim's luck half is retired, and/mmoconfig diff's luck section is re-pointed (behavior change). Trimming existed to strip an owner row that merely restated a Java default, and there is no Java default luck table left to be redundant against; the owner layer is a per-leaf overlay by id rather than a full-row copy, so the concept no longer applies. XP trimming is untouched, and the same half of the XP overrides admin page's trim button goes with it.difflists the bonus-drop owner bodies by id instead and answers toluck,dropsorall./mmoconfig export|import's config key follows the file:luck-lootbecomesbonus-drops.StatisticsComponent's shape changed (its thirteen public per-breakdown maps are gone in favour of one flat counter bag; the persisted wire format is unchanged, including the empty-map""round trip and long-width totals). The frozen API reads over it are untouched:MMOSkillTreeAPI.getStatisticsis byte-identical andgetStatTotal(store, ref, canonicalKey)remains the third-party read.- Named world selectors are gone; a
Wherecarries its own patterns (hard break). There is ONE world vocabulary:Where.Matchtakes a world-NAME pattern (Fooexact,Foo*,*Foo,*Foo*,*),Where.GameplayConfigtakes the world's own authored config key, andWhere.ExcludeMatchtakes name patterns that reject a world even when a positive axis matched. TheWorldSelectorsasset type, its owner file, and everyWhere.Namesreference retire with the indirection. Migration: rewrite"Names": ["as the patterns that selector held -"] "Match": ["default"]for the ordinary world (a bare word is an EXACT name),"GameplayConfig": ["ForgottenTemple"]for an instance family,"Match": ["*KweebecNightmare_*"]for a name-token family. If your main world is not nameddefault, override that file's ownWhereby id from a pack or through the asset type's owner layer - it is a handful of files, and each states what it targets on its own page. AWherematching no loaded world is reported asMATCHES_NO_LOADED_WORLDand one with no positive axis asEXCLUDE_ONLY, so a missed rename is a validator finding rather than content that silently never appears. - A dialogue world scope authors
Where, not a selector name (hard break). The scope leaf onOnce, on aMemoriesdeclaration, and on a dialogue flag scope is the sharedWhereworld-selector group (the same{Match, GameplayConfig, ExcludeMatch}group an NPC placement carries), not a named selector and not a bare string; the retiredWorldstring leaf refuses decode and names the replacement. Migration: replace{"WorldSelector": "forgotten_temple"}with{"Where": {"Match": ["*Forgotten_Temple*"]}}, using the pattern that names the world family rather than the selector that named it (an instance family is better keyed{"Where": {"GameplayConfig": ["ForgottenTemple"]}}, which carries no per-run uuid). AMatchpattern keys state by its literal core, so the handle is stable across an instance world being rebuilt under a fresh uuid name, and a narrower core scopes tighter. A dialogue{"Type": "World"}CONDITION nests its axes under the sameWheregroup ({"Type": "World", "Where": {"GameplayConfig": ["ForgottenTemple"]}}), matching every other world-targeting field; an omittedWherefails closed. Server/MMOSkillTree/LuckLoot/andServer/MMOSkillTree/MobDrops/are gone; a pack authorsServer/MMOSkillTree/BonusDrops/(hard break). Both stores, their asset types, their validators, their owner-override configs and theirLuckLoot/MobDropspack-control keys are deleted. One row per file, the filename is its id, and the body is{When: {Kind, Match}, Chance?, Loot, Notify?, Enabled?}- a structured codec decoded before the mod sees it, so an unknown key or a malformedChancefails the read loudly rather than surviving as a table that quietly hands nothing over. Migration: a luck row{"becomes a file whose body carries": {"Loot": {...}, "Chance": {...}}} "When": {"Kind": "BreakBlock", "Match": "*alongside the same*"} LootandChance; a mob entry{"Mob": "Trork", "Loot": {...}}becomes"When": {"Kind": "KillMob", "Match": "*Trork*"}alongside the sameLoot. A pack still declaring"LuckLoot"or"MobDrops"in aServer/MMOSkillTree/Control/*.jsonfile is declaring an unknown key: the new store merges natively, so a pack overrides a row by re-authoring its id and switches one off with{"Enabled": false}.mods/mmoskilltree/currencies.json,bounty-board.jsonandtoken-shop.jsonare retired (hard break). Wallets, storefronts, rotating shelves and boards of contracts are ziggfreed-common's economy (zc-commerce): the content isServer/ZiggfreedCommon/{Currencies,Shops,ShopPools,ShopEntries,Boards,Bounties}assets, the owner layer ismods/ziggfreedcommon/{currencies,shops,shop-pools,boards}.json(override BY ID, leaf by leaf, through the asset's own codec), and/zigcommercereads and edits what a player holds.config/LegacyCommerceMigrationruns at pluginsetup()and renames each present file IN PLACE towith ONE boot WARN naming the replacement; it never reads and never deletes them. A player's balances, lifetime spends and per-offer purchase counts move onto the shared economy's own record ONCE, at their first ready after the update (.legacy player/CommerceStateMigration, claimed so it can never run twice; the reroll allowance is reset rather than carried, since the two sides number their periods differently). This mod keeps only what the library cannot know (pages/MmoCommerceDeps: theme, naming, reward chips) and thin/mmocurrency//mmoshop//mmobountyaliases over/zigcommerce; theTokenShopPage/BountyBoardPagescreens,bounty/,shop/,currency/,rotation/andcontent/cost/are DELETED, and theMmo_Currencyreward kind is spelledCurrency.mods/mmoskilltree/placed-blocks.jsonis retired (hard break, nothing migrated). The placed-block anti-exploit ledger is ziggfreed-common'sworld/placed/PlacedBlockLedger, kept atmods/ziggfreedcommon/placed-blocks.jsonand read by XP, statistics, quests and achievements alike; the old file is renamed in place to.legacyat boot with one notice, never read and never deleted. Blocks placed before the upgrade are forgotten once (one expiry window of exposure); the four knobs stay inskill-config.json.- The legacy owner-file achievement leaf
announcementCommandis retired. How an announced achievement's server-wide banner is drawn is authored ONCE, for every achievement, inServer/ZiggfreedCommon/FeedbackMoments/Achievement_Unlocked.json; a file still carryingannouncementCommandgets one boot WARN naming the replacement (aCommandgroup on that moment file, or aVariantsentry scoped byWhen: {achievement:) and the leaf is otherwise ignored.} - The optional database backend's
QUESTSandACHIEVEMENTSdomains are replaced by ONEPROGRESSdomain (hard break). Quest and achievement state now live on a single shared per-player record, so they persist as a single blob under a single domain name; the two old names are gone fromPlayerDataDomainand nothing reads or writes a row filed under them again. A row a pre-release build left behind simply sits in the table, unread. This costs no player anything: the state itself lives in the world save and is carried across by the one-time migration below, whichever backend a server runs.
- The Mastery Trainer leaves the jar for the MMO Skill Mastery Pack (hard break for a server running the jar without it). That character is the pack's end to end now: their model, their role body, their nameplate and press-F hint keys, their Forgotten Temple placement and their conversation all ship there, and the jar's copies of all five are deleted. A server running the mastery pack sees no change at all, since the pack already carried its own placement and conversation and they simply stop being overrides; a server running the jar alone has no trainer standing in the temple, and reaches the Mastery screen from the hub menu as before. Migration: install the mastery pack to keep the trainer. A pack that overrode
npcs.Mmo_Mastery_Trainer.name/.hintnow overrides the mastery pack's keys rather than the jar's. - Required companion jar:
ZiggfreedCommon-2.0.0.jar(see that mod's own changelog for its scope). The optional RPG Stations bridge compiles against its api artifact 0.3.0 (the artifact semver tracksapiVersion(): version N ships as 0.N.0); and every content pack rewritten this cycle raises its MMO floor to^1.6.0and bumps its own version (bounty-contracts 1.2.0, skill-mastery 2.0.0, taming 1.2.0, and zone-quests with its first release, 1.0.0). Skill Capes keeps its 1.0.0 version; its manifest raises the same floor. Two packs are rewritten but not part of this release: skill-classes 2.0.0 rides the Class System's own Future status, and skill-stations 0.1.0 waits on the RPG Stations companion mod.
Features
- "Ready to hand in" waits until the errand is actually done, and a hand-in takes what you brought. A quest on a character's list reads as ready only when handing over what is in the bag would FINISH it, so a temple quest with 150 rocks still to break no longer greets the player as though the job were over; until then it sits under In progress, where its Hand in button is still there to take a part-load. The guide's "I have returned" line follows the same rule and waits for something to have returned WITH. One press of Hand in discharges every delivery that character or board is owed rather than the first of them, so a tribute asking for three separate offerings settles in one go. Behind all of it, the shared engine's hand-in offer asks for one of what a step still owes instead of the whole amount, so a large delivery can be carried down in loads. The
settlesTurnInAt/hasSettleableTurnInAtreads this needs are forwarded explicitly throughMmoDialogueQuests, never inherited, so a conversation asks the amount-aware runtime rather than the interface's looser default. - The guide's lines read with emphasis. Every dialogue line in the mod's own catalogue that asks the player to do something marks what to act on, in all nine locales: the items to craft, gather, place, cook or hand over and the creatures to hunt read in bold, and the place and proper names to seek out (the Forgotten Temple, the Trork) carry the one dialogue accent colour (
#ffd97a). A line that only congratulates stays plain, and so does every option label, because a button label is a String-only sink that prints tags literally. The vocabulary is those two elements and nothing else, no per-line colours and no whole-sentence bolds, and it is written down in thelocalizationskill so a pack follows the same rule. - A notice's tone paints the whole toast, and the toast lists its payout. Every shipped
Server/ZiggfreedCommon/FeedbackMoments/file authors a whole-toastTonefrom the closed Info / Success / Reward / Warning / Error set instead of per-lineColors, so the in-page toast's frame and headline and the corner feed's native style always agree: a payout reads gold end to end on the page, frame and words alike, the progress tick is the neutral blue with a green "Objective complete" finish, a full bag reads red, and a lost server-first race reads amber. A moment that carries rewards - a quest completing, parking or paying out, an achievement earned or collected - lists them as rows under the headline of the in-page toast, one per reward through the same chip reading every reward panel uses; aRowsgroup on any moment file switches that off or caps it with a "+N more" line, and a server owner retunes any tone by overriding the file. (TheToneword, theRowsgroup and the engines carrying their payouts are ziggfreed-common 2.0.0's; this mod ships the authored files.) - The Objective Book's claim toasts show what they paid, and a giver's quest is taken at the giver (ziggfreed-common 2.0.0 book fixes this mod picks up). Claiming a quest, an achievement or a points milestone toasts one gold row per reward beside the headline, through the shared chip reading. And a quest that names its giver (
Npc.ViewId) is not acceptable from the quest log: its row swaps Accept for a hint naming that character (resolved from the placement and identity assets, so every pack's giver-bound quest reads right with nothing supplied), the accept verb refuses a stale binding, and the quest stays listed, trackable and abandonable; it is accepted on the character's own quest list, where its conversation lives. - The Forgotten Temple opens on day one, and the guide says where to find them. Meet Me at the Forgotten Temple used to sit four quests deep, behind the first hunt; it now needs only the welcome finished, so it stands beside Craft Starter Tools rather than after it and a player who would rather explore than forge has somewhere to go straight away. The guide's crafting brief names the temple in the same breath, and a "Where do I find you?" line rides both shared footers, so the offer reaches every screen the guide shows except the very first greeting. It hides itself once the quest is taken, and again anywhere inside the temple. The starter chain's listing order follows the new shape: the temple meeting sorts third and everything after it shifts one place.
- A locked or unstarted contract on the bounty board explains itself. The board's detail panel reads every step through the same shared text walk the quest book and the NPC quest page use, so a contract's objectives render as real sentences (the library's neutral per-kind lines, or this mod's own composed wording where it names the content) instead of "Unnamed step"; a locked contract lists each unmet requirement - the prerequisite quest by title, a gated factor by the name its
Server/ZiggfreedCommon/Factors/naming asset gives it, with the bound it asks for - instead of one generic "You cannot do that yet." Both commerce screens word a gate refusal through the one shared lock-reason mapping, so a gate reads identically wherever it shuts. (The engine work is ziggfreed-common 2.0.0's; this mod's own factor names and its composer install land with its gate convergence.) - One objective-sentence seam for the whole server. This mod's objective renderer installs into ziggfreed-common's
progress.ObjectiveComposer(the per-modquest/QuestObjectiveText/achievement/AchievementCriterionTextseams are deleted), so every shared surface - the objective book, a board's contracts, a character's offer page, the tracked panel - reads the SAME composed wording this mod's own screens build, and a step the renderer cannot speak (another mod's kind, another mod's key) falls to the library's neutral per-kind family instead of an unnamed line. The composed line always reads the step's authored vocabulary: aREACH_LEVELstep still says "Reach level 30 in Mining", never the stat channel it dispatches on. - The feature switches live in the library's generic feature factor.
content/gate/MmoFeaturesdeclares this mod's feature ids, aliases and live on/off suppliers into ziggfreed-common'sFeatureFlags, whose first declaration contributesmmoskilltree:featureprocess-wide - existing content gating on that id answers exactly as before, the availability-provider seam (RpgStations, NPC placements) stays, and the old per-mod registry and fold-time lift (content/gate/FeatureRegistry,FeatureLift) are deleted in favour of the library'sprogress.gate.FeatureLift, called with this mod's factor id. - The guide introduces the mastery trainer. Standing in the temple with the meeting behind them, the player gets one extra line asking who the other figure across the landing is; it opens a screen about her, and taking her up on it starts Meet the Mastery Trainer. The whole beat is held behind the mastery feature, so a server without the mastery pack never sees the line and never hears about a character who is not there. An option a switched-off feature holds shut is also no longer audited as this server's content, which is what lets a shipped conversation carry a beat for a pack that may not be installed without reporting that pack's quest ids as missing on every boot.
- The Forgotten Temple gets an arc of its own. Three main quests and a daily, all offered and handed in at the temple standing of the Adventurer's Guide and nowhere else, because an alias points one way: the temple answers to the spawn guide's id, never the reverse. What the Stones Remember clears the rubble and asks for worked copper, The Watch Below thins the Trork off the stair, and Keeper of the Deep waits on total level 150 before handing over the keeper's vestments. The Temple's Due is a calendar daily, refreshing at the day boundary rather than on a rolling timer. Its offering is three humble reagents (essence, plant fibre and a little light leather), none of which needs a bench or a smelter, and it pays a single loot-table roll whose four rolls resolve independently, so a good day pays several at once and the daily funds its own tribute.
- A loot table can celebrate its rare pull, and a rolled reward pays the whole table. Any roll or ladder floor in a
Server/ZiggfreedCommon/Lootablestable may carry aCuebeside itsGrants: an opaque celebration id that fires only when those grants genuinely paid out, presented as theServer/ZiggfreedCommon/FeedbackMomentsfile of the same name. It reaches every table this mod rolls, whichever path rolled it: a table paid out as a reward, a mob's bonus-drop row and a block's lucky find alike. So the whole pipeline is authorable, and a pack invents a celebration of its own by shipping a new cue id plus the moment file that answers to it. The jar shipsRare_Find(a gold toast and the legendary-chest chime), authored on the temple daily's warden-blade roll and the mastery daily's adept-staff roll. A table granted as aLootablereward (a quest payout, a shop offer, a dialogue) pays its FULL grants vocabulary - items, native drop lists, console commands and registered reward kinds alike - exactly as one rolled at a work station does, so the temple daily's boost-token roll and the mastery daily's mastery-point roll pay through the reward path. (The sink wiring, the cue seam and the cue-to-moment mapping are all ziggfreed-common 2.0.0's; this mod contributes only the moment file that says whatRare_Findlooks like.)
- Five reward items, each a copy of a base-game one. A crude pickaxe, sword and longsword and a linen robe and hood, each inheriting everything from the vanilla item it names as its Parent and differing in four lines.
Recipeis not among the fields a Parent hands down, so none of them can be crafted at a bench. The weapons and the tool carry small flat Health and Stamina; the robe and hood give LESS health than the plain linen they are cut from and carry an MMO channel instead, +1% luck on the robe and +1% experience on the hood, so the set is a trade rather than a straight upgrade. Held-weapon and tool stats rideUtility.StatModifiers, which the equip bridge applies because the engine reads that block only off an accessory-slot item; armor stats are the engine's ownArmor.StatModifiers.
- The guide speaks to every side quest at both of its moments. Make Camp, Home Cooking, Prove Your Mettle and the temple meeting each get a line for the hand-in and a line for while the quest is still open, in all nine languages, where three of the four previously fell through to the standing menu whatever the player was carrying. Each hand-in line opens the quest list with that quest called out, so the player sees what they are collecting. The one-size line stays in the conversation as the default a server owner can point their own side quest at without writing a screen.
- The Asset Editor shows each field's real default and offers dropdowns on the closed vocabularies. Every jar asset schema declares the effective value of a leaf whose unauthored meaning is not the control's zero-state (an
Enabledmeaning true,IsParamKeymeaning true, a bonus-drop row's gold notify colour) and exports each code-closed string vocabulary as an enum with a one-line meaning per value: a passive trigger'sOnhooks (HitDealt/Kill/Finisher/DodgeArmed), a masteryScaling.CurveandSacrifice.Mode, a bonus-drop row'sWhen.Kind(BreakBlock/KillMob), the Power LevelDefaultMode, a custom skill'sTriggers, a pack control file's add/replace modes, a cast cost'sStatpool (MANA/HEALTH/STAMINA), and an ability body'sTarget/Overlapknobs. Decode is unchanged; a pack-extensible vocabulary (a paramKey, a damage school, an engine stat name) stays a plain string on purpose.
- Adds Poison as a seventh damage school beside Fire, Ice, Lightning, Water, Arcane and Void: the jar's
Server/Entity/Damage/Poison.jsonlayers the vanilla cause underElementalwith its own combat-text colour (#7CFC00), and the school ships itsMMO_Bonus_Poison/MMO_BonusFlat_Poison/MMO_SchoolResist_Poisonchannels, tooltip labels and names in all nine languages. Poison, Thorn Patch, Paralyzing Dart and Forest Warden are Poison-school abilities. - Adds an elemental-school pass over the default abilities: Molten Slam, Flame Totem, Pyromaniac and Explosive Shot deal Fire; Dragon Arcana deals Ice; Tempest, Tempest Vault, Wind Dancer and Concussive Reverb deal Lightning (the school's first abilities); the dagger shadow kit (Assassinate, Mark of Death, Shadow Dance, Shadow Pounce, Smoke Bomb) deals Void; Archmage's empowered casts read as Arcane. Plain weapon strikes stay physical.
ArmNextHitnodes gain aCauseleaf, and a beam-only ability now shows its school on its card. - Completes the damage-school roster at nine. Adds Life, the drain / lifesteal / holy school (its own authored cause, spring-green combat text,
MMO_Bonus_Life/MMO_BonusFlat_Life/MMO_SchoolResist_Lifechannels and tooltip labels); Soul Drain, Vampiric Strike and Blood Pact deal Life damage. Makes Physical a genuine school with its own channels (MMO_Bonus_Physical/MMO_BonusFlat_Physical/MMO_SchoolResist_Physical): a hit whose cause reaches no school resolves to Physical, so every hit carries a damage type - mechanically and in the UI, where an untyped damage ability's card and tooltip read Physical. All the new channels ship empty like their siblings, so damage is unchanged until content grants them; Physical resistance is keyed on the vanilla weapon causes (Bludgeoning/Slashing/Projectile) too, since the game's resistance lookup matches a hit's exact cause. Gear enhancement can roll the new schools' bonus and resistance lines. - Adds an Arcane Essence item (
Ingredient_Arcane_Essence): every school now mirrors an essence ingredient - Fire/Ice/Lightning/Water/Void/Life map to their vanilla essences, and Arcane gets this jar-authored one (vanilla ships no arcane essence). - Ability tooltips show the damage type: a "Damage type" line in every damage-dealing ability's hover tooltip, named and coloured per school with the same colour the school's floating combat text uses, matching the school tag on the ability card.
- Styled ability tooltips: the hover tooltip carries the composition's full styling - school-tinted damage numbers, the accent tint on key numbers (cooldown seconds and cost included), bold keywords and section headers, italic flavor text, and an amber level-requirement line while the ability is still locked. The tooltip flatten converts the structured message styles the in-page text renders into the literal markup the item-description channel parses, so one styled composition feeds both surfaces.
- Per-school resistance has one authority and one applier. Everything that grants school resistance (a weapon or tool's
StatModifiers, an enhancement roll, a mastery node) adds up on the player'sMMO_SchoolResist_stat, andSchoolResistReconcilerkeeps one invisible infinite native effect per school on the player carrying that total, so the game's own armour mitigation applies it beside worn armour. The mod no longer folds that stat into its own defense reduction, so it is never applied twice. Armour pieces author nativeArmor.DamageResistancedirectly. - Adds a
schoolscope to mastery modifiers and aschoolResistmodifier key: a mastery node'sdamagevalue can belong to a damage school ("school": "Fire") instead of a weapon skill or an ability and is mirrored ontoMMO_Bonus_/MMO_BonusFlat_, andschoolResistmirrors ontoMMO_SchoolResist_, both under the mastery key segment so respec removes exactly what a purchase added. Node descriptions render the school by name in the player's language. - Content can be gated on another mod being installed. A
Requiresfactor condition{"Factor":"hytale:mod_installed","Param":"Ziggfreed:KweebecNightmare","Min":1}(a ziggfreed-common factor) works on quests, achievements, dialogues, loot, placements and shops, and the mod HIDES such content when that plugin is absent, the same waymmoskilltree:featuregates hide."Max": 0gates on the mod being absent. - Adds the
INSTANCE_ROUND_WONandINSTANCE_ROUND_ENDEDobjective kinds (a won round / any finished round of a minigame built on ziggfreed-common's instance layer;Targetissuch as: kweebec:chase,Qualifierthe preset id, credit is per participant), with objective text templates. Kweebec Nightmare fires the shared moment, so its rounds count for quests and achievements with no MMO-side kweebec code. - Adds a CozyTales Fishing integration (fishing adapter contributed by the mod's author, hexvane): a catch in CozyTales Fishing awards FISHING XP - the per-fish xp-map match plus a rarity payout plus a first-catch-of-a-species bonus, retunable under the reserved
_Cozy_Rarity0.._Cozy_Rarity4/_Cozy_NewSpecieskeys ofxp-maps.json's FISHING block - and counts toward CATCH_FISH quest and achievement objectives. Auto-detected at startup like every optional integration; inert without the mod. The shared fishing XP plumbing (xp-map lookup, bonus-key lookup, world-thread award) is extracted to oneFishingXphelper all three fishing adapters use. - Content packs (detail in each pack's own notes): the MMO Skill Quest Pack (first release, 1.0.0) ships the Emerald Wilds and Howling Sands campaigns, opened from the hub, plus the Kweebec Nightmares chain with three dailies and the Night Owl ladder awarding the Nightwarden set; the MMO Skill Stations Pack adds the Timber Rights quest that awards a Sawmill from the hub; the MMO Skill Mastery Pack adds seven damage-school mastery tracks.
- Every notice, banner, jingle and colour a quest or achievement produces is authored JSON, so a server owner can reword, recolour, resound or silence any of it without a jar. The mod ships one file per moment under
Server/ZiggfreedCommon/FeedbackMoments/(Quest_Objective_Progressed,Quest_Completed,Quest_Parked,Quest_Claimed,Achievement_Unlocked,Achievement_Claimed,Achievement_Server_First_Lost), reproducing the notices players already know: the progress tick and the green "Objective complete", the gold "Quest complete" and its jingle, the parked quest's three wordings (open the Quests page; make room, in red and silent; claim it at the Bounty Board), the legendary-chest chime on collecting a parked reward, the golden unlock toast with the achievement's own picture and the server-wide banner the announced achievements show, and the quiet amber note for a lost server-first race. Drop a same-named file in a pack that depends on this mod to replace any of them; the per-player quest-notification setting keeps deciding which of those toasts a player sees. SeeCONTENT_PACKS.md, "Feedback moments". - The quest log and the achievements menu are the two tabs of ziggfreed-common's objective book, filled by this mod's deps. The page, the filters, the rows and every lifecycle verb are the library's; this mod registers one deps record (
ObjectiveBookPages.deps) carrying only what the library cannot know: the themed frame, the MMO menu rail (branding, the skills header, and the cross-page navigation with its permission and feature gates, routed through the book's ext channel), the statistics column on the achievements tab with its drill-downs, bounty presentation (the Bounty pill, the board's own grade word, manage-at-the-board), the requirement vocabulary (total level, per-skill levels, achievement prerequisites), reward naming for this mod's own reward kinds, server-first claimants, the points-milestone ladder and its claim, the inventory-room pre-check in front of a quest claim, and the accept / abandon feedback moments. Achievement definitions thread their listing facts (category, subcategory, sort order, chains, feat-of-strength, legacy-since, icon) onto the shared runtime object so the book's browser reads this mod's catalogue exactly as its own pages did. Every achievements opener (/mmoachievements, theMmo_Achievementsdestination, the menu rails, the tracked-achievement row, the stat drill-down's Back) lands on the book's achievements tab; the three MMO achievement pages (AchievementPage,AchievementDetailPage,AchievementSummaryPage) and their.uitemplates are removed, and the achievement-menu and quest-menu wording moved to the library's ownziggfreedcommon.progression.langkeys (a translation pack overriding the oldui.achievements.*/ui.quests.*menu keys re-authors them underziggfreedcommon.progression.book.*). Achievement criteria read on the book with this mod's own generated sentences too: the plugin installsObjectiveTextRenderer.composeinto ziggfreed-common's ONEprogress.ObjectiveComposerseam, and both folds hand the shared text group a lazily-composed line per step beside its authored key, so a criterion with noTextKeyreads as its sentence rather than an untitled line and one whose authored key takes arguments gets them bound. - The quest and achievement admin commands are aliases of ziggfreed-common's
/zigprogressfamily./mmoquestadmin(give,reset,resetdialogues,complete,status),/quest(accept,claim,abandon,status) and/mmoachadmin resetkeep their names, their argument shapes, their permission gates (mmoskilltree.admin/mmoskilltree.command.quest) and their wording, and each verb now speaks to the shared runtime the library owns - the one quest and achievement engine pair, the subject the registered store understands, the registered call scope around every mutating call - instead of to a component of this mod's own; the same verbs are reachable mod-agnostically as/zigprogress quest,/zigprogress achievementand/zigprogress memory forgetunder engine-derived nodes (ziggfreed.ziggfreedcommon.command.zigprogress...), documented on the library's side. What stays this mod's own:/mmoquestadmin reload(this mod's owner files and pack layers; the shared assets are/zigprogress reload's),/mmoquestadmin list(this mod's category-tagged view), the OFFLINE half of/mmoquestadmin complete(the reward spool needs the directory of everyone who has ever connected, which only this mod keeps; the shared verb needs the player online),/quest's conversation-only accept refusal and its inventory-room check before a claim, and the release of the server-first claims a player won on/mmoachadmin reset(they live in this mod's own claim table; the shared verb wipes state and says the claim is still recorded). Two behaviour changes:/mmoquestadmin giveand/quest acceptnow settle a step the player already satisfies the moment the quest is taken on (a quest given to somebody past its finish line pays or parks at once), and/mmoquestadmin resetwipes an ORPHANED record (an id the catalogue no longer knows but the player still carries) instead of refusing it./mmoquestadmin resetdialoguesreads "Forgot everything every conversation remembers about X" now, which is what it does. The three page-openers keep their names and gates:/mmoquestsopens the shared objective book on its quests tab,/mmoachievementsopens it on its achievements tab, and/mmoquestuiopens the shared NPC quest page. - The admin configuration menu becomes a router hub over six focused section pages.
/mmoadminopens a compact hub that routes to General Settings, Command Integration, Leveling Parameters, Boost Stacking Limits, Rested XP and HUD & Abilities - each section a page of its own with its own status line and a Back that returns to the hub - beside the per-config editors, which all stay one click away. The hub keeps the Perfect Utils warning, the reload actions and the Danger Zone, gains an Edit Themes button (the theme picker was previously reachable only by command), and its own Back lands on the active top-level menu (the class hub on a class-pack server) instead of always the skills page. - The quest and achievement system toggles move onto a shared Progression Systems admin page. The hub's new Progression Systems button opens ziggfreed-common's progression admin page, which lists every server-wide progression switch a mod registers; this mod registers its quests and achievements switches over
skill-config.json's existing toggles (both apply immediately), each with a hint naming the staff bypass permission that keeps the system on for staff while the server-wide switch is off. - Adds a World Rules editor to the admin hub. A new World Rules button opens a page listing every effective per-world rule - its id, what it targets (the
Wheresummary), its key knobs and whether it is on - with a per-rule ON/OFF toggle, an edit card for the rule's XP multiplier, its three bonus switches and its two skill lists, and a per-rule Reset that drops the owner override. The page writesmods/mmoskilltree/world-rules.jsonrule bodies through the same codec the pack layer decodes and never writes the retired flatMatchkey; authoring a NEW rule and editingWheretargeting stay file-authored, which the page says on its face. Alongside it, a world rule gains an optionalEnabledleaf (absent = on): a disabled rule stays loaded and listed but matches no world, so a rule can be switched off and back on without re-authoring it. - Adds a Power Level editor to the admin hub. A new Power Level button opens a page over
mods/mmoskilltree/power-level.json: the Enabled switch and the DefaultMode preset up top, then every numeric leaf grouped the way the schema groups them (Clamp, the pillar factors, and the three mode weight maps over the six builtin pillars), each row showing its default beside its current value with a Default/Override tag, per-leaf editing and per-leaf reset-to-default. Every write is a partial overlay - an untouched leaf stays absent from the file and keeps inheriting defaults plus pack content - and the per-stat StatRewards factor map and custom pillar weights stay file-authored. Both new editor pages also surface their content validator's problem count. - Adds a horizontal-align control to the ability cooldown HUD's admin section (the stored position always carried both axes; only the vertical half was editable), and corrects the ability-HUD note: position changes apply live to every online player, no reconnect needed.
- Localizes the whole admin hub family: every action status line, section header, field label and dropdown option (the leveling formula and boost stack mode included), the milestone XP preview, the reset warning and the previously untranslatable in-layout labels now resolve through lang keys, and the reward-type dropdowns in the skill-tree editors read the localized reward names instead of raw enum text. all nine languages ship in this release.
- Retires the admin page's "Default Server Language" dropdown: display text has been client-resolved since 1.4.0, so the control no longer changed anything a player sees (the
defaultLanguageconfig leaf stays, read by the XP-boost command scope and a deprecated external-plugin shim). - Fixes three admin-hub defects: an event carrying no action could crash the page handler, the Boost Templates editor opened without re-checking the entitlement that hides its button, and a flipped toggle kept its previous state's hover and press colours. Removes the page's dead, commented-out combat-XP blacklist remnants (the feature lives in
/mmoconfig blacklist). - The two requirements editors get the same treatment. The Item Requirements page gains an All category tab listing every default and every saved override unfiltered - an override outside the named item-id prefixes, or a disabled override with no default underneath, was previously invisible in the UI and could never be reset there; every override row now shows Reset. The permanently-empty Armor tab is dropped (no armor requirement defaults ship; an armor override still shows under All), the Add/Update validator now refuses a requirement on a display-only skill id no player can level (previously it accepted one and saved a gate no matching item could ever pass), and the format hint's example names live built-in skills (it still said SMITHING, which this release moves out of the built-in roster, so the page's own validator rejected the example it printed). Both requirement pages are fully localized - requirement summaries resolve skill names in the viewer's locale, and the Tool & Weapon page's category labels separate from the stable internal ids its state machine keys on - repaint their tabs and toggles across all three button states so a selected tab or an OFF toggle no longer flips colours on hover, replace every local sound-less button style with the shared sounded set, and the Mob Weapons tab's empty state now says what is true: no defaults ship, any weapon works until a pattern is added.
- The Item Rewards screen answers every click. Several button paths (clicking the tab already selected, prev/next with nowhere to go, a malformed claim event) sent no response and left the client stuck on the black loading screen; every path now responds. The page also drops its private /give parser and name/description resolvers for the shared reward-preview seams - the private copies had drifted: a currency-grant reward rendered no card at all while still being counted, so the "X/Y Claimed" header could never complete, and a /give missing its target argument titled the card with the flag text - so currency rewards show their cards, the header counts exactly the cards on screen, and stack quantities render grouped. The class moves to
pages/skill/beside the screen that opens it: it is the player-facing milestone claim screen, not an admin config editor. - The boosts and XP admin quartet (Award Boost, Boost Templates, XP Overrides, XP Token Overrides) rebuilds on the shared admin styling: every button clicks audibly through the shared style set, the Award Boost page sits in the standard framed container like its siblings, tab / chip / toggle painting routes through the shared helpers, and the three overrides tables share one row look with the Reset button appearing only on rows that actually have something to reset.
- Fixes the Award Boost page's dead "Production" target: the button targets the real Crafting category now, so token awards, instant global activations and minted permission nodes for that column all work (nothing in the boost stack ever parsed "production", so every use of that button failed or granted a node that could never fire).
- Adds a Misc tab to the Boost Templates and XP Token Overrides admin pages, so MISC-category skills (Taming, Building) can be tuned and targeted like every other category - their default per-skill templates and boost-token items existed but were unreachable from the UI - and the Boost Templates page can mint category-wide Misc templates.
- Admin-awarded boost tokens for offline players queue by player UUID only, the same rule as the pending-rewards spool: an award for a name the server has never seen is refused loudly instead of waiting forever under a name, and a queue an older version filed by username still delivers when its owner connects. An online token award records the player-data dirty mark, so it survives under the database backend too.
- The Award Boost page says when permission boosts are entitlement-gated off: a warning under the permission preview and on the grant confirmation, instead of granting a node that silently never takes effect.
- The four pages' remaining hardcoded English is keyed and client-localized: statuses, row status labels, table headers, field labels, tab labels, empty-list notes, unit suffixes and the ON/OFF toggle all resolve in the viewer's own locale, and the category chips on the Boost Templates and XP Token pages render through the shared rich-button path so their labels and active tint actually show.
- The Mob Drop Commands editor tells the truth and stops crashing its admin. The status line painted a structured message onto a plain-text label, which disconnected the admin on every add, remove, disable or toggle; it now paints through the shared status seam. The pool table drops its DELAY and TIER columns (the per-pool stagger and elite-tier filter those columns described are retired schema) for a Form column naming whether each pool's chance is a flat number or a stat-driven formula, so every value sits under its own header again. The placeholder hint and the admin help pages describe
{tier}honestly (it resolves only when a companion mod attributes a kill rarity; MMO Mob Scaling does) and stop advertising the retired delay. Adding a fallback pool (empty pattern) no longer raises a bogus error or paints its success line red, and a hint under the buttons says a disabled pattern also stops that mob's bonus-drop kill rows. Saving a pool no longer writes the retiredrequiredTierkey back intomob-drop-commands.jsonor the generated reference file. - The Mob Kill XP editor joins the shared admin chrome. The page moves onto the shared overlay and container with the standard title bar, every button gets the shared sounded styles (the whole page was silent on click), the weapon-scaling row is hidden for non-admins instead of shown dead, and the help button no longer strands a non-admin on an infinite loading screen.
- Editing a command reward preserves every authored field. Re-saving a reward from the admin page rebuilt it from the four form fields, silently dropping its description, description key, auto-claim flag and currency dependency and forcing it to run as console; the untouched fields now carry across (the auto-claim toggle carries all of them too). The level-jump button answers an empty level field with an error status instead of an infinite loading screen and is labelled Go instead of the misleading "Add Tier", the display-name cell renders its parameterized fallback correctly (it printed a literal
{0}for key-less rewards), and the current-skill label resolves through the localized skill-name seam. - Localizes the three drops/rewards admin editors (Mob Drop Commands, Mob Kill XP, Command Rewards) end to end - titles, table headers, field labels, buttons, empty states, row statuses and every status message resolve through lang keys (all nine languages ship in this release) - and routes their row striping, status painting, toggles, number formatting and event bindings through the shared render seams instead of per-page copies.
- Hardens the Custom Skills admin page. Fixes two client crashes (the CATEGORY column of both tables pushed a client-resolved name into a String-only sink), lists pack-provided custom skills in their own toggle-only section so the disable-by-id switch can actually reach them, keeps a skill's authored
descriptionKeyandrequiresFeatureswhen saving from the admin pages (previously silently deleted fromcustom-skills.json), makes the per-skill cap grid honest about the three-layer max-level resolution (a typed cap equal to the shared default now still overrides a pack-authored cap, instead of silently snapping back to the pack value; the hint says so), and locks the Skill ID field while editing so a rename can no longer create a duplicate skill. - Hardens the Custom Skill Wizard's edit mode. Skill-tree tiers are keyed by their data tier (a sparse or standalone-tier tree previously round-tripped corrupted, mislabelling claimed rewards), opening the wizard on a sparse tree no longer crashes before the page responds, the Skill ID is locked while editing (a changed id previously saved a SECOND skill and orphaned the original's XP map, command rewards and tree), and Skip on the skill-tree step leaves existing tiers alone instead of wiping them. The step-4 reward form gains a Target field, so a wizard-authored reward can target a skill or combat type just like one authored on the overrides page, and the permission-denied view hides the whole form.
- Fixes a Skill Tree Overrides reset bug. Reset to Default on an added tier no longer makes the tiers above it unreachable (the config re-index now rebuilds each shifted node at its new tier, covered by a regression test); the page opens on the first selectable skill instead of always Mining, and its skill selector no longer offers owner-disabled skills.
- Localizes the three skill-editor pages' remaining English: every status line, empty state, tier and reward status word, the delete-confirm and Remove labels and the TYPE column now resolve through lang keys, and the row buttons on those pages click audibly through the shared admin button styles. all nine languages ship in this release.
- The White Label master switch now actually governs the menu accent recolor. The accent tint used to keep painting every themed menu while the admin page's Enable White Label toggle was OFF (the recolor checked only the Pro entitlement); it now applies only while white label is enabled, the same gate every other branding surface already honors.
/mmoconfig reloadalso re-reads a hand-editedwhite-label.json, which previously was only read at startup. - The White Label admin page is rebuilt on the shared menu family. It wears the shared decorated frame and the server's own active theme and accent, its buttons use the shared styles (every click audible), and every label and status line resolves through lang keys. The accent tint strength, the live
white-label.jsonknob deciding how far the accent is softened before it recolors the menu, becomes editable beside the accent color, and the color preview swatch shows the softened color the menu actually paints instead of the raw hex (and hides when the accent is empty, which means no tint at all). The Server Description and Branding Position help texts now describe what the branding really does (left mode brands the side panel and keeps page titles; right mode replaces the page title with the server name), and a flipped position choice repaints all three button states so hover never flashes the stale selection. - The theme picker reports the honest paint state. A premium theme selected without Pro used to show as Active while the menu actually painted the free default; that row now reads an amber "Selected (needs Pro)". The active row's indicator is a labeled Active tag instead of an unlabeled square, a theme without an authored display name shows its id instead of a raw lang key, and the footer upgrade hint points at
/mmofingerprint(the entitlement-registration command) instead of a command that does not exist. - The in-game Configuration Guide catches up with the whole 1.6.0 admin surface. Eleven new cards - Quests, Achievements, Abilities, Ability Mastery, World Rules, Power Level, HUD Layout, Menu Themes, NPC Placements, Config Presets and Content Packs - each with the full overview / how / examples / tips treatment and a settings button that opens the matching page (the two new editor pages, the HUD section, the theme picker, the shared Progression Systems page, and the presets page, which stays entitlement-gated like its hub button). Two topics that had drifted from the mod are rewritten truthfully: Mob Drop Commands no longer teaches the retired per-pool delay and elite-tier fields, and Boost Templates now documents the real structured permission node (
mmoskilltree.xpboosts.) and the player-activated flow instead of an invented. . . . mmoskilltree.boost.always-on model. The XP Maps tip now gives the--args=command form the parser actually binds, the dead Elite Mobs guide topic and the rest of the orphaned elite-mob admin keys are gone from all nine languages, and the guide's card buttons are audible and drawn from the one shared button style set. - Config presets cover the whole 1.6.0 config surface. The export list grows from 12 to 25 entries: mob-drop-commands, world-rules, power-level, mastery, ability-settings, ability-overrides, hud-layout, feedback, combat-text, reward-icons, integrations, white-label, and the achievements/ directory (achievement files plus point milestones) join the original set. Applying a preset reloads every covered config in boot order - boost templates re-arm the boost service, integrations re-resolve their adapters, and ability overrides re-fold onto the live catalog - and an achievements apply never touches the server-first claim ledger.
- The presets page is fully localized and hardened. Config names, counts, fallbacks and every status line resolve through lang keys in the player's own locale (the import result is structured data the page renders, never a pre-composed English sentence); a config key read from a preset FILE is rendered through a safe template instead of being interpolated into UI markup; an event carrying no action no longer leaves the client loading; and the presets and help page family draws its buttons, section cards and toggles from the shared style sets with click sounds throughout.
- Adds
/mmomigrate, an in-game admin/console command for the 1.6.0 content migration.scan(the default) walksmods/mmoskilltree/(including the.legacyrenames and timestampedbackup/copies earlier boots left behind) plus every installed content pack, and writes one Markdown report (mods/mmoskilltree/migration/report-) classifying every file the same four ways the docs-site Migration Converter does: converted, needs hands (with the target shape quoted), handled by the mod at boot, or dropped with the reason..md --action=owner --confirm=yesapplies the safe owner-file conversions in place: the world-rulesWhererewrite, the retired ability-body-key fold inability-overrides.json, and the four server settings still sitting unread in a retiredabilities.json, lifted intoability-settings.jsonandhud-layout.json. Every file is backed up first, written atomically, decoded back through its own codec, and rolled back if that decode fails.--action=packreports each pack's stale content and points at the web converter; packs are never rewritten in game. Everything boot already migrates is reported and left alone: one file, one writer. /mmomigrateand the docs-site Migration Converter learn the progression wave's retirements together, one vocabulary, corpus-proven from both sides. Both knowplaced-blocks.json(renamed to.legacyat boot, never read; the row names the shared ledgermods/ziggfreedcommon/placed-blocks.jsonand the fourskill-config.jsonknobs), the three commerce owner filescurrencies.json/bounty-board.json/token-shop.json(renamed to.legacyat boot; their residue is work for the web converter),database.json(unchanged, no per-domain knob; a file selecting a database backend is told once that rows filed under the retiredQUESTS/ACHIEVEMENTSdomains are never read again), the retired achievementannouncementCommand(REPORTED with the exact replacement in the boot warning's words: aCommandgroup or aWhen {achievement:} Variantsentry onAchievement_Unlocked.json; never converted), the written-outMeta.mmoskilltree.Announce.Title/Body(the converter renames them ontoTitleKey/BodyKey;/mmomigratenames the leaf), a packServer/MMOSkillTree/WorldRules/file still spelling the flatMatchstring, and aWherenaming a retired world selector. Both closed boot-rename tables list the same eleven names; a current shared-store file is no longer flagged merely for carrying aMeta.mmoskilltreeblock. Seven corpus cases joinsrc/test/resources/migration-corpus/, none edited.- Both migration tools cover the ability schema's in-cycle respellings through ONE declarative table (
ability/AbilityTopLevelRenames, mirrored in the converter'sabilities.ts):Identity.NameKey/DescKeytoTitleKey/DescriptionKey,XpRouting.HeldExcludetoExcludeSkills,PassiveTrigger.RequiresWeaponSkill(scalar) toWeaponSkills(array), the PascalCasePassiveTrigger.Onvocabulary, and the deletedCost.Costsstripped with a named report;RetiredAbilityKeySweepwarns on all of them at boot and--action=ownerconverts them inability-overrides.json. The retired pack-control key list gains the six commerce keys (MobDropsis dropped from it - git history shows it was never a control-key leaf), an unrecognisedbackup/leftover is now reported as "read it before deleting" instead of "safe to delete", and the XP-reset spool's live file and its backup (which holds resets queued under names no player resolves - re-run the command once they connect) are both classified truthfully. A coverage test now asserts everyLegacyOwnerFilecall site has a BootHandled row, and a real corpus case pins the respelling wave fixture-to-fixture on both sides. /mmopacks --action=exportnow writes the shared layout. Exporting a server's custom quests and achievements emits one file per entry in the shared schema the engines read (Server/ZiggfreedCommon/Quests/and/ .json Achievements/, manifest at the zip root), converting the owner files' older authoring shape on the way out: the flat requirement fields fold into one/ .json Requiresblock,repeatable/cooldownSecondsbecome the structuredRepeatgroup, historical reward kind spellings become the registered ids, and an objective array becomes the id-keyed map. An exported pack loads on any server running this version and scans clean by/mmomigrate; a leaf the shared schema has no home for (a rawdescription,variables, the markdown fields) is named in the server log rather than dropped silently. Each emitted file is proven in the build by decoding it back through the same shared codecs the server folds at boot.- An existing save's quests and achievements move across by themselves. The first time each player connects after the update, everything they had - active quests and their objective progress, completed quests and their cooldowns, tracked pins, how many times they have finished a repeatable, earned and claimed achievements, criterion progress, points milestones, pinned achievements and unlock dates - is copied onto the record the shared engines read, once, and never again. Nothing is asked of a server owner, and nothing is lost.
- Adds a Quest Book item: a non-consuming companion to the Skills Menu Scroll that opens the quest log directly. Granted the same way, as a second reward on the
getting_startedandforgot_somethingtutorial quests. - Adds native-asset ability authoring: every active ability and triggered passive is now a
Server/MMOSkillTree/Abilities/asset (Pattern A, PascalCase keys, the codec IS the schema), and 97 defaults ship at that path in the jar (63 active abilities plus 34 triggered passives). Content packs can author and override abilities for the first time (defaults < pack < owner precedence, same as every other custom asset type); a sibling variant reuses a base ability via native.json Parentinheritance instead of duplicating the whole body. - Adds
ability-overrides.json, a lightweight owner tuning layer (mods/mmoskilltree/ability-overrides.json) for retuning one ability's cooldown, cost, damage, or any other field without authoring a full content pack. A partial body overlays per leaf onto the already-folded ability (defaults + pack), so overriding just a cooldown never nulls out that ability's cost, effects, or presentation. - Adds a hard break of the legacy
abilities.jsonoverride format: on first start after upgrading, any file found atmods/mmoskilltree/abilities.jsonis moved (never read, never silently deleted) to a timestamped file undermods/mmoskilltree/backup/, with a startup warning explaining that it is not read and pointing at the new format. Re-author any customizations asability-overrides.jsonentries or a pack asset; seeCONTENT_PACKS.md. - Adds
AbilityValidator, a content audit over every authored ability (identity and cooldown/cost sanity,ABILITY_UNLOCKplacement in the skill tree, every authored mastery/reward modifier resolving to a real ability field, and sound/particle/effect asset references), surfaced by the startup content audit and/mmoconfig validate. - Adds
/mmoability reload(admin), which re-folds the ability catalog from the current pack state plus a fresh disk read ofability-overrides.jsonand re-validates it, so a pack author or server owner sees an ability edit take effect without a server restart (a brand-new zipped pack drop still needs one). - Adds
/mmoability unlockall [--arg1=all|actives|passives]and/mmoability lockall(admin), a self-only test pair for trying the ability roster without levelling into it.unlockallgrants every catalog ability (or just the bindable actives, or just the triggered passives) to the sender;lockallruns the existing ability-state reconciliation, so unlocks backed by a currently-claimedABILITY_UNLOCKskill-tree reward survive and only the test grants are stripped, along with their cooldowns and slot bindings. - Adds a two-level ability presentation: the ability-level cast moment (launch sound, cast animation, camera shake, one feedback moment) plays once per cast, while every step in an ability's effect chain carries its own presentation moment (and a step's own intrinsic second moment, like a projectile's impact), so a multi-beat ability tells its whole story instead of one flat cue.
- Adds per-step ability description fragments: an ability's tooltip composes from its actual effect-step chain (damage, cooldown, cost, on-hit effects, and so on) instead of one hand-written description string. An ability you've unlocked folds in your own current gear and mastery bonuses, so the tooltip's numbers match what you would actually cast right now; a preview of an ability you don't have yet (a reward or shop card) still shows its authored base value.
- Adds a VFX and SFX pass across the whole ability roster (65 abilities at the time of the pass; later additions in this cycle ship their own): a cast-moment sound and particle burst (plus, on the heaviest hits, a camera shake) plays once per cast, every effect step in a multi-beat ability carries its own particle treatment (impacts, damage-over-time ticks, buff auras), and a new Teleport "Arrive" moment plays a sound and particles at the destination of a blink instead of only the departure. Projectiles that only played a sound on impact now also burst with particles, several previously-silent capstone zones (Cataclysm's aftermath crater, Concussive Reverb, Skewer, and others) gain a tick or telegraph treatment, previously-invisible party buffs (like Phalanx Master's formation aura) render a visible aura, and damage-over-time stacks (Burn, Bleed, the icy Slow stack) show a consistent visual wherever they apply. Ships as 18 new
MMO_*particle, entity-effect, and (the mod's first) model-VFX assets, each composed from verified vanilla particle spawners rather than new art. - Adds a void-purple dissolve feel upgrade to Vanish and Shadowstep: both now carry a shared
MMO_Dissolve_Voideffect (the model-hide shroud, replacing an off-theme cyan placeholder) instead of two different treatments. Shadowstep adds server-enforced invulnerability for the length of the blink, an origin dissolve burst, and a destination arrival flash and sound; Vanish gains a matching void shroud sound. - Adds registry-backed particle, camera, and feedback validation to
AbilityValidator: aPresentation.Particles/Cameraid is now checked against the real particle-system and camera-effect registries (previously particles were shape-only and camera was unchecked), and aPresentation.Feedbackvalue is checked against theFeedbackEventvocabulary. Two new warnings enforce the two-level presentation model: an ability-levelPresentationauthored on a passive ability (dead - a passive never plays a cast moment) and aCamera/Feedbackleaf authored on a step-levelPresentation(reserved - nothing below the ability level plays it). - Adds 8 default abilities back onto the new step-engine asset schema as
Server/MMOSkillTree/Abilities/*.jsonfiles:Blood_Pact,Challenging_Shout,Forest_Warden,Iron_Bulwark,Prospectors_Focus,Smoke_Bomb, andSoul_Drain(active abilities), plusWind_Dancer(an ACROBATICS triggered passive). The jar-bundled default roster grows from 65 to 73. Each ships with a matching cast VFX/SFX pass, including roughly 8 newMMO_*particle and entity-effect assets, composed from verified vanilla particle spawners the same way the rest of the ability roster was; a couple of the eight reuse an already-shipped, previously orphaned effect verbatim instead of authoring new art. - Adds 10 new active abilities built on native Hytale weapon-chain and movement mechanics: Acrobatics gains Skyward Leap and Backflip Disengage (pure mobility, no damage), Blunt gains Bull Rush and Ground Slam, Axes gains Tempest, Swords gains Vortex Lunge, Polearms gains Javelin Toss, Archery gains Hookshot Arrow and Explosive Shot, and Magic gains Arcane Lash.
- Adds two new deployable abilities and a new summon: Archery's Sentry Turret and Magic's Flame Totem each plant a structure that fights nearby hostiles on your behalf for a limited time, and Archery's Summoned Wolf calls a temporary wolf ally that hunts hostile mobs and never targets a player. Kills and per-hit XP from all three credit the summoning player. Together with the 10 abilities above, the default roster grows from 73 to 87.
- Adds the ability for a non-player mob to cast from the same ability catalog players use, complete with that ability's real cast presentation and effect chain, through a new
MMOSkillTreeAPI.castNpcAbilityentry point. Inert on its own; a fourteenth new ability, the NPC-only Dragon Arcana, ships as a demonstration caster ability, and the standalone MMO Mob Scaling companion mod (1.1.0) is the first real consumer, giving certain mobs a per-role caster roster and wind-up animations before they cast. - Adds the ability-improvement platform: a mastery node, quest, dialogue, or shop entry can now grant a standalone, player-facing improvement to an EXISTING ability instead of only a full replacement ability. A new
AbilityModcontent type (pack-authored underServer/MMOSkillTree/AbilityMods/*.json) names a target ability and a list of modifiers, including a new splice-a-whole-effect-step modifier kind that inserts an authored effect (a bonus on-hit proc, an extra beat at the start or end of the cast) into the target ability's own effect chain rather than only tuning an existing number. A granted improvement is delivered through the same unified reward pipeline every other reward uses (quests, dialogues, shop entries), persists per player, and can be granted or revoked by admins via/mmoability grantmod/revokemodfor testing or manual awards. SeeCONTENT_PACKS.md's "Ability improvements (AbilityMods)" section for the authoring shape. - Adds a load-time check that catches an ability improvement or mastery bonus landing twice on the same hit (once as an ability-cast bonus, once again as a weapon-swing bonus); content authoring now gets a loud error instead of a silent double-counted number. Caught and fixed one such case already present in the shipped mastery content (Piercing Shot's pierce/damage mastery line).
- Adds the first seven of the damage schools a hit's damage type can carry: Fire, Ice, Lightning, Water, Arcane, Void, and Poison (Life and Physical complete the roster at nine in the completion pass above, and the Arcane Essence entry beside it settles every school's essence mapping). Fire, Ice, and Poison ride vanilla causes (Poison as an overlay filed under Elemental); Lightning, Water, Void, and Arcane are MMOSkillTree-authored sub-types of the vanilla Elemental cause. A full roster pass retags frost-identity abilities onto Ice, arcane-identity abilities onto Arcane, Tide Caller onto Water, and moves Riftblade onto Void (several of these previously fell back to untyped Physical damage silently, a pre-existing bug this pass also fixes; the drain kit around Soul Drain lands on the Life school added by the completion pass above); ability tooltips now name the school on every damage/dot/zone line.
- Adds a
SCHOOL_RESISTskill-tree reward type: a tree node can grant permanent resistance to one of the damage schools, stacking across every claimed node, surviving relog and world hop, and removed on respec or a full skill reset. - Adds a full stat substrate so gear, potions, content packs, and sibling mods can grant any of the mod's combat/utility scalars (defense, crit chance, crit multiplier, lifesteal, combo damage, fall damage reduction, effect duration, cooldown reduction, luck, bonus XP), a per-skill damage bonus for any active skill, or a per-school damage bonus, through an ordinary item or effect
StatModifiersblock, with zero plugin code. - Adds six ability-specific stat channels (ability range, radius, duration, cost reduction, i-frame duration, and pierce count) that gear and mastery can grant bonuses to directly, each named on an item's tooltip and each capped by a server-tunable clamp so a stack of ability-boosting gear can't push a single ability's number past a sane bound.
- Adds a second way to grant an ability improvement: a piece of gear can now grant an
AbilityModjust by being worn, held, or carried in the offhand (a newItemAbilityGrantpack asset, matched by exact item or by the item's own tag group), on top of the existing mastery/quest/shop grant path. A rolled or hand-authored item stack can also carry a pre-stamped ability mod directly in its own metadata, the same per-stack blob item enhancement already uses, so a piece of loot can arrive already boosting a specific ability with no separate reward step. Ships with six example gear ability mods and three example grant items (a battlemage armor set, a crystal flame staff, and a Stormbound Charm offhand trinket), plus a live drop-table example: mining a Zephyr gem block has a chance to drop a charm pre-stamped with a Vortex Pull range bonus. - Adds two Defense ability stubs, Guard Stance and Parry Riposte, over Hytale's native weapon block/parry hold mechanic (primes brief stamina-break immunity while holding a block or parry window); no default weapon wires the hold yet, so both are an unexercised code path pending a weapon or content-pack hookup.
- Adds native crowd-control diminishing returns: a stun or stagger from an ability (Shield Bash, Shield Slam, Paralyzing Dart, Berserker Charge) also builds the target's native Immunity toward a temporary CC-immune window, so repeated stuns in quick succession taper off instead of chaining indefinitely.
- Adds a specific reason message when a cast is refused (on cooldown, not enough of the required resource, the ability isn't unlocked, your class can't use it, or its own conditions aren't met), shown for the exact check that blocked it instead of one generic failure.
- Adds Frost Stream, a Staves ability unlocked in the staff skill tree: a staff-gated channeled ice beam that damages and slows everything it holds, refreshing the slow's duration on every tick instead of stacking it.
- Adds Void Slam, a Staves ability unlocked in the staff skill tree: a staff leap-slam that deals void damage, knocks nearby enemies upward, and leaves behind a void damage-over-time field where it lands.
- Adds an authorable
Channelpresentation group to theMmoBeamstep (a per-ability channel-loop sound and particle effect for a held beam; an ability that authors none keeps the previous flamethrower-loop default, so every already-shipped beam ability is byte-identical) and anOverlapknob toMmoApplyEffectcontrolling how a reapply of the same effect while an instance is still active is resolved:Extend(still the default when unauthored, so every ability shipped before this knob existed attaches byte-identically to before) adds the new attach's duration onto the active instance, the right call for a one-shot proc meant to stack;OverwriteandIgnoreare newly authorable, letting a repeat-tick channel refresh its effect to a flat duration on every tick instead of climbing without bound. Frost Stream is the first ability to author both: its own ice-wind channel loop, andOverlap: Overwriteon its per-tick slow so the slow refreshes to a flat 1.5 seconds instead of piling up additively across its 50-tick channel, which would otherwise have let the slow climb to roughly 70 seconds of stacked duration. - Fixes Wind Dancer's chained Shadow Pounce riposte bonus never applying after a perfect dodge; the damage multiplier now actually lands on the follow-up strike.
- Reworks Flame Stream onto the native Crystal Flame staff's stream presentation: the ability composes the vanilla staff's own flame-stream chain for its visual and audio beat while keeping every MMO-side concern intact (gear folding, per-cast modifiers, school-tagged damage, and XP attribution), so the sprayed cone the player sees is the cone that actually burns.
- Adds a shared slam presentation treatment derived from Ground Slam (weapon-trail arc on the wind-up plus a matching impact particle and sound beat) to Concussive Dive, Molten Slam, Rending Slam, and Crippling Slam, so the whole slam family telegraphs and lands consistently. Crippling Slam's slow radius is matched to the radius its own ground impact draws, closing a see-one-thing/get-another mismatch.
- Fixes ability-sourced damage-over-time and ground-zone ticks (Molten Slam's fire pool, Rending Slam's bleed, and every other ability-created DOT or zone) awarding XP to Magic regardless of which skill cast the ability; a tick now credits the casting ability's own declared XP skill(s), matching how that ability's direct damage already attributed.
- Fixes Explosive Shot only ever detonating on a direct entity hit: it now looses a real projectile that resolves and detonates on terrain impact and on a clean miss as well, so the shot always produces its blast where it lands.
- Fixes the held-weapon cast gate refusing with a genericized message: the refusal names the required weapon category (or categories) again, localized, restoring the specific feedback the singular-field gate used to give before the
HeldWeaponCategoriessweep. - Fixes ability descriptions falling back to a generic "does something" line: every effect-chain node kind now contributes a real, number-bearing description fragment, so an ability's tooltip and its skill-tree card describe the actual numbers everywhere instead of a placeholder for the uncovered kinds.
- Fixes Vortex Lunge's opening spin dealing no damage: the phase-one spin lands its authored damage before the lunge follows through.
- Fixes Hookshot Arrow's pull continuing to read as a light shove even with its distance scaling already correct: the pull previously routed through the engine's shared knockback pipeline, which multiplies a pending knockback's horizontal components by an internal 25x scale factor tuned for an ordinary hit's small nudge, crushing the hookshot's single larger pull down to almost nothing. The pull now writes the target's native
Velocitycomponent directly in one boundedSetinstruction instead, the same sink the vanillaLaunchPadInteractionuses, mirroring that same 25x horizontal pre-scale by hand so the delivered impulse still matches vanilla feel; the vertical lift stays unscaled, matching how vanilla knockback treats it too. - Fixes Shield Bash's impact flash rendering on the caster instead of the target it struck: a landed bash now chains a victim-anchored, standalone 0.4-second impact-flash effect targeted at the struck entity, on top of its existing impact sound; a whiff never reaches that branch and stays silent.
- Fixes Flame Stream's fire jet spawning from a disconnected point when cast bare-handed: the bare-hand jet now anchors to the player skeleton's own attachment socket using the same emitter pose the held-staff beat already used; casting with a staff in hand is unchanged.
- Fixes drinking a Shadowstep potion sometimes consuming the item and applying its cooldown without the teleport ever firing: the consume chain's step order is restored to payload-then-debounce with a fail-on-blocked guard, and the sickness marker moves onto the cast node's success path, so the potion's cast completes reliably every time.
- Adds rich markup to ability tooltips: bold stat keywords, damage numbers tinted by that hit's damage school via the
DamageSchoolscolor palette, and italic flavor text and conditions, all authored as markup directly in the lang values themselves (the only place the tooltip's flattened text sink can parse it). Every tooltip number now renders through a typedMessageparameter ({N, number}for an integer slot, a typed double elsewhere) instead of a bare interpolated string, and every server-formatted number pinsLocale.ROOT, closing off a class of locale-dependent number-formatting bug. - Extends the "With your bonuses" tooltip breakdown: a gear cooldown-reduction bonus now folds in as its own gear term using the same formula the cast pipeline applies at cast time, and any on-hit school or weapon damage bonus your gear grants renders as its own labeled section instead of disappearing into the base number.
- Adds a trigger-condition lead line to every triggered passive's tooltip and claim card (previously invisible on 34 abilities), and reorders the description walk to match the ability's real execution order. Zones, deployables, marks, heals, and buff auras each describe their own real knobs instead of a generic line (a crippling effect reads its slow field, a buff aura names its next-strike damage bonus), repeat-hit counts render, and a cast's own gating conditions (a required weapon class, a resource threshold) now render as an informational tooltip line rather than surfacing only as a refusal message when you fail to meet them. The native
ApplyForcedescription fragment is direction- and magnitude-aware (a vertical recoil kick, Explosive Shot, reads as a kickback rather than a leap; a genuine leap, Skyward Leap, reads its own drift direction), and a duplicate area line is suppressed when a scope qualifier resolves to the same number as its own child radius (Frost Nova, Blood Pact) or when a selector's scope note would otherwise repeat across every surviving hit line (Arcane Lash); two genuinely different numbers still both render. - Fixes an ability's authored
SummaryHiddenflag, meant to suppress a purely cosmetic effect-chain line from its tooltip, silently doing nothing: the description walk now resolves it through a getter-first reflective lookup implemented across all 21 step Types, and the content validator delegates to that same lookup instead of carrying its own copy. Shadowstep and Vanish, whose own teleport-effect line had no way to hide until now, suppress that line as authored; the ability-body fidelity digest treatsSummaryHiddenas presentation-only, the same as$Comment, so an edit to it alone no longer fails the build. - Adds a cooldown/cost envelope line to an ability's claim card (a skill-tree, quest, or shop reward preview) and a content-aware line cap in place of a flat three lines. Extends reward-chip formatting to cover every reward type: a negative value now renders with the correct sign instead of
+-8%, and a summon-count reward pluralizes correctly through a client-side ICU token instead of always reading likeSummons 1 allies. Fixes a claimed reward's HP-range gate silently disappearing across aRewardDataoverride round-trip, a mechanic loss with no visible symptom until the gate simply stopped applying. - Adds named status-effect icons to armed and triggered abilities: while a next-hit buff or a triggered passive is active, a named icon now joins your other active effects instead of the ability going unrepresented, backed by 40 aura assets (19 newly authored for this pass). Several shared cosmetics that previously mislabeled multiple unrelated abilities now split into their own per-ability badge (Vengeance, Challenging Shout, Blood Pact, Clean Kill, Iron Bulwark), and the ability-less dodge counterattack, a damage-system reaction rather than a cast, now badges itself too through a new
CommandBuffer.tryAttachoverload since it has no cast path of its own to hang a badge on. Every ability'sIdentity.Iconis unique across the roster (13 duplicate-icon groups resolved, including a five-way shield-icon collision and an unarmed-pickaxe misfit now reading as claw and gauntlet icons), so two abilities are never mistaken for each other at a glance. - Adds a cosmetic-only ability feel pass: an empowered strike (Vampiric Strike, Wind Dancer's riposte, Paralyzing Dart, Smoke Bomb's opener, Assassinate) now plays its own consume cue instead of landing silently, Smoke Bomb visibly throws its bomb with a camera kick, and Shadowstep's teleport plays a matching sound and particle burst at its destination as well as its departure. A buff now plays a distinct removal cue when it expires instead of dropping off silently, bleeding and marked targets carry a visible victim tint, seven buff auras gain a body glow, Prospector's Focus gains a first-person-only sparkle so its bearer can see their own buff, a cleanse effect gains tick cues and a screen overlay, and the frostbite capstone's proc now bursts a victim-anchored frost mist and ice crack instead of firing invisibly.
- Fixes ability names, status-effect badges, and tooltip text shipping English-only on non-English servers for this cycle's new content: all 8 non-English locales are now key-complete against en-US for every ability, status-icon, and tooltip key this cycle added.
- Fixes the mastery cape names (
reward.cape.*, all 20) and the short HUD stat codes (reward.short.*) shipping as untranslated English copies in most non-English locales: every one of the 8 non-English locales now carries real translations, with each locale keeping an English code only where it is the natural short form there. - Adds interactive work stations: pressing use on a station block pulls the camera to a third-person work view (or seats the worker on the block, camera free, body locked), plays a work animation timed to each swing, and converts materials one cycle at a time for small, steady XP. The first station, the Sawmill, converts a wood family's logs into that family's planks for split Woodcutting and Crafting XP per cycle. The station engine itself ships in a new standalone companion mod, RPG Stations (
additional-mods/rpg-stations; own docs at itsCLAUDE.md), which runs a full rich station loop (conditional loot rolls, command rewards) entirely on its own; the MMO jar adds a soft bridge that turns a work session into skill XP, aggregated luck, and mastery bonuses. Station content (the Sawmill block, loot tables, and the MMO bridge JSON) ships in the standalone content packskill-stations-pack, which depends on BOTH mods - stations stay dormant without RPG Stations installed, and grant no skill XP without the MMO jar also installed. SeeCONTENT_PACKS.md's Station authoring section for authoring a new one. - Adds station luck rewards: each work cycle can roll a bonus copy of its output on a per-skill luck chance (the same luck math gathering and mob kills already use), plus an optional ladder of native loot-table rolls at rising luck floors (50%, 100%, 150%, and beyond) for a rarer bonus drop the higher a player's combined luck climbs.
- Adds tool-power XP scaling to stations: working with a better tool for the station's gather type (a sharper hatchet, for example) earns more XP per cycle, on a curve the station's own content defines; a station that authors no scaling keeps the flat rate.
- Adds an optional face-the-station camera lock and a configurable per-swing cadence that re-fires the work animation as a one-shot alongside its own sound/particle beat (an axe whoosh and wood chips, on the Sawmill), so the swing motion lines up 1:1 with the cue, on top of its per-cycle presentation.
- Adds a general-purpose flair-unlock system for stations: any reward path that already runs a console command (quest rewards, achievements, mastery, mob-drop commands, command rewards) can unlock a named cosmetic flair, a per-station sound/particle overlay, for a player, persisted per player; admins can also grant, revoke, or list a player's unlocked flairs directly with
/mmostation flair grant|revoke|list. - Adds an optional no-material practice mode for a station: when a station opts in, working it with no materials on hand grants a small, extra-delayed trickle of XP (no items, no luck, no quest or achievement progress) instead of ending the session; off by default.
- Adds a center-screen session-summary panel for stations: a unified ledger lists every item consumed and produced (icon and name, luck bonuses flagged) alongside XP earned per skill, each row carrying the skill's own icon and a named breakdown of what produced it (base rate, tool bonus, and any class, world, gear, boost, or rested modifier that applied), when a work session ends; configurable to fall back to the classic corner notification instead.
- Adds a session-completion flourish to stations: finishing a work session with at least one completed cycle plays a distinct sound and particle beat on the player, grander than a single cycle's cue and smaller than a rare-find jackpot; a silent stop with no completed cycles stays quiet.
- Adds optional held-tool durability drain to stations: a station's content can wear down the tool in your hands per swing and/or per completed cycle (off by default); a tool that breaks mid-session ends the work loop.
- Adds a distinct sound and particle flourish per rare-find loot tier at a station, so a luckier roll feels luckier; the Sawmill's higher tiers play a grander flourish than its lower ones.
- Tunes the Sawmill's work cycle to land exactly on the fifth swing beat instead of mid-swing, for a cleaner-feeling work rhythm.
- Adds Smithing as a content-pack custom skill (not a built-in): the MMO Skill Stations Pack ships it as a
Server/MMOSkillTree/CustomSkills/*.jsonasset, feature-gated on the RPG Stations integration, and it earns XP from a station's work cycles and completed enhancement rituals - dormant unless RPG Stations plus the pack are installed. Any Smithing XP a player earned earlier in this cycle is preserved across the change. The same pack ships a second custom skill, Cooking, bridged onto RPG Stations' own jar-shipped fish-preparation station. Seeskill-stations-pack's own docs for the station content that trains each one. - Adds content-pack custom skills: a pack can add a whole new skill to the skill tree as a
Server/MMOSkillTree/CustomSkills/asset (a Pattern A structured type, PascalCase keys), with the same defaults < pack < owner precedence as every other pack asset and its own additive.json .langlayer for the skill's name and description. SeeCONTENT_PACKS.md's Custom skill authoring section. - Fixes an enhanced item's tooltip showing a raw
server.items.*.descriptionlocalization key as its first line when the base item had no description of its own (a descriptionless vanilla weapon run through the Anvil or/mmoitemenhance); the enhancement header and stat lines render with no phantom key line. An enhanced item otherwise keeps nesting its own native per-locale description like any other item, except when that description carries rich-text markup: the tooltip surface that renders an enhanced item's description cannot parse markup, so in that one case the tooltip shows only the Enhancement stat lines instead of literaltext. - Adds
/mmoitemenhance refresh(admin): re-derives the held stack's tooltip from its existing rolled stats with no re-roll and no stat change, the sanctioned fix for a stack whose displayed tooltip has gone stale. - Fixes a skill fed by a service bridge or by commands (one with no XP-trigger map, such as Smithing) being hidden from the skill pages, the leaderboard, and the admin boost target list, and its earned XP being left out of a player's total XP, summed total level, and "reach level N in any skill" progress.
- Adds coalescing XP notifications while working a station: during a work session, a skill's rapid XP toasts collapse into one updating entry per skill (carrying that skill's icon and the award amount in the icon's count badge) instead of a stack of separate banners, riding the same client path native item pickups stack on. Outside a station session, XP notifications behave exactly as before, and level-up banners are unchanged.
- Adds an enhancement summary to the anvil: a completed Enhance ritual's session summary now lists each stat the stamp rolled, with the same colored label and value the item's own tooltip shows, plus a durability line - so the summary and the tooltip can never disagree about what the enhancement did.
- Adds an authorable header balance strip to both storefronts and bounty boards: a
Currencieslist on aServer/ZiggfreedCommon/Shops//.json Server/ZiggfreedCommon/Boards/asset names which currency balances show in that page's header, rendered as a chip row (icon plus amount) through the shared cost-chip vocabulary rather than a fixed single icon-and-label pair. A storefront that omits the field shows.json bounty_token, and a bounty board that omits it shows that board's own reward currency, so every existing pack keeps its current header with no edit. The hub's bounty preview panel renders the same row, capped at two chips with a+Noverflow. Both lists are cross-checked against the currency registry by/mmoshop validateand/mmobounty validate. - Adds authorable shop category ordering: a storefront's catalog categories previously sorted alphabetically, which put
rareahead ofuncommonwith no way to correct it (an entry's ownorderonly ever sorted within one category). ACategoryOrderlist on aServer/ZiggfreedCommon/Shops/storefront fixes the sequence (a server owner writes the same leaf by id in.json mods/ziggfreedcommon/shops.json); a category left off the list follows the named ones alphabetically, so an unset order is exactly the previous behavior. - Adds one-shot station find XP: a station loot roll can grant a fixed XP bonus on a lucky find (RPG Stations'
Roll.Grants.Contributions), awarded at full value regardless of held-tool tier and never reduced on idle practice cycles, and counted into the session summary's XP totals. - Adds asset-driven NPC placement: where an NPC stands, which worlds it stands in, whether it appears at all, and what pressing F on it does are ONE pack-authorable asset (
Server/ZiggfreedCommon/NpcPlacements/, Pattern A, every leaf wired for native.json Parent), resolved by one engine that ships in ZiggfreedCommon. It replaces seven parallel mechanisms in the MMO jar (the hub config, the hub placement store, the quest-giver asset/definition/config, the giver spawn service and role generator, and the structure- and zone-discovery spawn systems) and the role-name special case that made the hub privileged. Five INDEPENDENT anchor groups (world spawn, fixed coords, a worldgen structure marker, a discovered zone, a registered custom provider) produce the union of their positions rather than a placement-mode enum;LimitsandLifecycle(keep-alive, respawn, fortify) are opt-in knobs beside them. The Adventurer's Guide and every quest giver are now ordinary placements. SeeCONTENT_PACKS.md's NPC placement authoring section. - Adds NPC placement into instance worlds, which the old exact-name world lists could not reach at all (an instance world is named
instance-). A placement targets its worlds with a- Whereselector: a named selector from the shared vocabulary, an inline name pattern, or the world's own stableGameplayConfigkey. The jar ships the Adventurer's Guide standing in the vanilla Forgotten Temple as a shortParentchild of the spawn placement (it overrides onlyWhereand its identity bindings, and inherits the role, anchor, and lifecycle), plus the quest that sends you there. - Adds NPC identity aliases: a placement may list further npc ids it ALSO answers to (
mmoskilltree:npc_aliases, aValuesarray with one id per entry) beside its primarymmoskilltree:npc_id. Every quest bound to the primary id of one placement is offered, credited, and turned in at any placement listing that id as an alias, with no quest-file edit, now or in future; an objective aimed at a placement's own PRIMARY id stays scoped to that placement. A tracked quest bound to an aliased id marks every answering placement on the map, which is the intent - either one serves it. - Adds the
Oncedialogue knob: keyless seen-ness on aStartentry or an option,"Once": truefor once per character or"Once": {"Where": {"Match": ["for once per world family ("]}} Whereis the shared world-selector group, so aGameplayConfigkey covers an instance family). AMatchpattern speaks the same grammar every world-targeting field uses (Foo,Foo*,*Foo,*Foo*), so the pattern itself is the exact-versus-fuzzy knob; state keys by the literal name or the pattern's literal CORE, which is the stable handle across an instance world being torn down and rebuilt under a fresh uuid name (*KweebecNightmare*keys underKweebecNightmare, and the narrower*KweebecNightmare_Barn*scopes per arena). An entry's Once is spent by COMPLETING the beat (choosing any option on the routed node, the implicit farewell row included), so leaving mid-beat shows it again; an option's is keyed by itsLabelKey(or an explicitOnceId), never its index, so reordering a node's options can neither resurrect nor spend one. - Adds declared dialogue
Memories: named state declared once at the dialogue top level with its own scope and lifetime (Wherefor per-world-family, taking the same world-selector group asOnce;ResetWithQuestto be forgotten when that quest resets;Sharedfor cross-dialogue visibility), then used by BARE NAME through theRemember/Forgetactions andRemembered/NotRememberedconditions. The map merges per key underParent. Validation catches what raw flags could not: an undeclared name, declarations of a shared memory that disagree, a memory never written or never read, a blank scope, and a scope pattern that would match every world. - Adds one shared condition-factor vocabulary across NPC placements, dialogue options, and RPG Stations: the same
{Factor, Param, Min, Max}leaf everywhere (a dialogue writes it as{"Type": "Factor", ...}), with a portablehytale:*standard library of straight native reads -hytale:stat(any registered stat channel, which is how MMO skill levels are reached),hytale:tool_power/tool_durability_percent/tool_quality/tool_item_level,hytale:held_tag, andhytale:held_item. The same ids resolve context-appropriately: a work station answers the tool ids from the session's tool, a dialogue or placement gate from what the player is holding. A pack can now gate a dialogue option on a stat or a held item with zero Java. - Adds authoring diagnostics for placements that used to fail silently: a validator finding for a binding key with no
yourmod:namespace (previously dropped with zero signal even at runtime), for a binding namespace nothing registered, for aui_targetprefix nothing can open, for an alias no content anywhere names, and for two placements claiming the same primary npc id. Every open registry (binding channels, condition factors, custom anchor providers) now records the mod that claimed each id plus a per-id failure count and last message, surfaced by the new/mmonpc channelsand fed to the in-game Asset Editor's pick lists. - Adds pack-authorable quest-giver and hub NPCs the native way: a placement names its character with
Identity.Role, and that role is an ordinary Hytale role file a pack ships atServer/NPC/Roles/**with full engine validation. The jar ships two parameterizedAbstracttemplates holding the whole stationary-greeter body,Template_Mmo_HubandTemplate_Mmo_QuestGiver, each declaring exactly the five parameters a variant can really override (Appearance,NameTranslationKey,Weapons,OffHand,DefaultOffHandSlot), so a pack's own giver is a three-lineVariantwith aModifyblock. Beside them the jar ships three ready-to-place roles:Mmo_HubandMmo_QuestGiveras thin variants, andMmo_Mastery_Traineras a full body, because the engine reads a role's press-FHintand wornArmorliterally and the trainer's prompt says train rather than talk.MmoRoleTemplateParameterTestfails the build on aComputebinding at either literal-only field and on aModifykey the referenced template does not declare, across the jar's roles and the content packs' alike, since both mistakes show up in game only as an NPC who is never anywhere. - Adds the shared quest and achievement content schema: both lift onto ziggfreed-common's engines and both author as native assets under
Server/ZiggfreedCommon/Quests/and/ / .json Server/ZiggfreedCommon/Achievements/. One shape covers both (// .json Text/Listing/Requires/RewardsplusFlow/Repeat/Npc/Objectiveson a quest andScoring/Criteria/MetaChildrenon an achievement; who may SEE content before taking it -Hidden,RequirePrerequisites- and itsIconlive on the sharedListinggroup for both), with nativeParentinheritance instead of a hand-rolled template DSL, a recursive folder scan, id = filename (a leading-underscore folder contributes its name as an id prefix, stacking in path order), and unique basenames store-wide.QuestGeneratorsstamp a family of near-identical quests from one file. The legacyServer/MMOSkillTree/Quests/andServer/MMOSkillTree/Achievements/stores stay registered as the compat surface for existing packs and owner files, and precedence is unchanged bottom to top: shared schema, then the legacy pack layer, thenmods/mmoskilltree/quests/andachievements/. - Adds the
Repeatgroup, the whole vocabulary for a quest that comes back around. Authoring the group AT ALL is what makes a quest repeatable, so"Repeat": {}is a quest with no clock of its own, governed entirely by whatever offers it - which is exactly what a rotating bounty-board contract is. Inside it, four INDEPENDENT constraints compose and all must pass:Cooldown, a rolling wait written through one structured time group whose parts are summed ({"Hours": 24},{"Days": 7});Reset, a calendar allowance ({"Period": "Daily"}or"Weekly", plusAt,WeekdayandTimes, anchored to the server clock in UTC);MaxCompletions, a lifetime cap; andResetsOnComplete, the quest ids wiped when this one finishes.CooldownFrompicks which instant the rolling wait counts from:Claim(the reward being taken) by default, so a quest parked completed-but-unclaimed at an NPC is not quietly burning its cooldown, orComplete(the last step ticking over) for a quest belonging to a rotating offer, so collecting late does not burn a slot in the next period. An admin quest reset wipes the completion record, because a reset means starting over; abandon and self-heal preserve it, so a lifetime cap stays enforceable in play. - Adds
CompletionDialogue, a top-level leaf on a shared-schema quest naming the conversation that follows it settling at a character. It plays only where there is somebody to play it, so a hand-in at an NPC runs the beat and a quest finished from a log or an objective book skips it - and that decision belongs to the shared engine rather than to whichever screen the player was looking at, so every consumer surface behaves the same way. Authoring an empty string drops a conversation inherited from aParent. - Adds
Meta.mmoskilltree, the structured consumer-knob block on shared-schema content: a quest readsNpcOnly(cannot be taken from the quest log) andFeature; an achievement readsServerFirst,Feat,Feature,Class,LegacySince,Announce/Announce.Body, and the two per-skill stencils underPerSkill. Every leaf is nullable and inherits throughParent. The engines' own schema carries no consumer vocabulary:Metais one generic namespaced extension map any mod can claim its own key in. - Adds
Listing.Chains, multi-membership content laddering: an item may sit in several chains at once ([{"Id": "sands_hunter", "Tier": 2}, ...], tier per membership, ids are free strings), and the ladder UI treats the first membership as primary. Quests carry the same leaf as questline grouping vocabulary. - Adds
Text.TextArgs, auto-calculated display arguments: a title or flavor key can name argument slots filled by engine sentinels rather than baked numbers, with@amount(the objective or criterion's own required amount) pre-seeded. An author writes the sentinel once and the number follows a retune. - Adds one gate schema across quests AND achievements: an achievement's
Requiresblock (feature factors, class, and thefactorsaxis) is evaluated exactly the way a quest's is, so a pack that gates its taming or mastery content on a feature gets that gate honored with no content edits. Gates also gained afactorsaxis onrequirements/prerequisitesblocks ({"factor", "param", "min", "max"},Factorson codec surfaces);skillsandminCombatLevelare unchanged sugar for it, and a bounds-less condition is a genuine presence check that fails closed on an unresolvable factor. - Adds honored
Scoring.CountsTowardTotal: an achievement may state outright whether it contributes to a player's point total, instead of that being derivable only from its feat-of-strength flag. A feat of strength still never counts toward the total, so atrueon a feat does not override it. - Adds
STAT_THRESHOLD, a generic objective and criterion kind:{"type": "STAT_THRESHOLD", "target": "completes when that channel reaches the amount, so any registered stat channel (including another mod's) becomes a step with no new objective type.", "amount": N} REACH_LEVELkeeps working unchanged and is the friendly form for skill levels - it desugars onto the same kind at runtime, so the two render identically and a player can never tell which one an author picked. - Adds two public level channels,
MMO_TotalLevelandMMO_HighestSkillLevel, beside the existingMMO_Level_andMMO_CombatLevel. All four are readable from any factor formula - a loot chance, a content gate, mob scaling, a work station - the same way, and all four are written on every path that can move a level. - Adds a process-wide factor CONTRIBUTION door in
ziggfreed-common, so one mod can publish what it knows as ordinary factor readings and another mod's authored content can cite them with no dependency in either direction. A contributor claims an id at setup (owner-tagged, identity-idempotent, one boot line per id's first claim) and every factor site falls through to the contributions on a local miss, so a consumer's own registration always wins.FactorContextgains a nullabletargetleaf besidesubject- the entity a moment is ABOUT, as opposed to the one acting - andMobKillEventSystemthreads the victim into it, which is what lets a mob-kill formula weigh the mob's own properties and the killer's luck in one expression. An id nobody answers degrades by the standing rule: a gate stays shut and a formula term adds zero, so one authored file is correct on every server. - Adds five citable readings from the standalone MMO Mob Scaling companion, usable in any
Chance,Conditionsor gate formula the moment a server has that mod installed:mmomobscaling:mob_rarity_tier(the mob's place on the rarity ladder, 0 for plain and one step per authored tier, derived from the tiers themselves),mmomobscaling:mob_rarity(Param= a rarity id),mmomobscaling:mob_affix(Param= an affix id),mmomobscaling:mob_difficulty, andmmomobscaling:region_power. So a bonus drop can be richer off an elite, a quest gate can want a boss, and neither mod knows about the other. That mod's own rarity and variant death loot also re-bases onto the sharedLootblock this release, so a tier's loot, a bonus drop, a station find and a quest reward are one vocabulary. - Adds the open reward vocabulary:
Kind(ortype, on the older surfaces) names a kind and every other field is that kind's parameter, so alongside the mod's ownMmo_Xp/Mmo_Boost_Token/Mmo_Currency/Mmo_Ability_Moda piece of content can pay out any framework kind -Item({"Kind": "Item", "Params": {"Item": "Ingredient_Bar_Iron", "Count": "5"}}),Lootable(roll a shared loot table),Stamped_Item(grant gear with rolled stat bonuses already stamped on the stack),Effect(apply a native entity effect),Droplist(roll a nativeItemDropListand spill it at the player's feet, or at an authored position), andCommand(run an authored command line). A kind id is native-asset style, PascalCase with underscores, unprefixed for the framework's own and mod-prefixed for a consumer's; ids match case-insensitively and every historical lower-case spelling (xp,item,boost_token) translates through one canonicalization, so the older surfaces keep working verbatim. It works on every reward surface at once - quests, achievements, point milestones, shop offers, bounty crates, dialogue reward actions, command rewards - and a kind a companion mod registers is authorable with zero changes here. An item reward renders with the real item icon, its name and count, and the item's own native tooltip. - Adds reward kinds AS CONTENT:
Server/ZiggfreedCommon/RewardKinds/declares a.json Paramsschema (Required/Defaultper parameter) plus oneCommandtemplate with{player}/{uuid}/{}substitution, run as the server console through the shared command primitive, and the file name is the kind id. All four of this mod's kinds ship that way in the jar (Mmo_Xp,Mmo_Currency,Mmo_Boost_Token,Mmo_Ability_Mod, each mapping to an admin command), so a server owner with an admin command they want paid out as a reward needs no plugin to say so. Kind assets layerdefaults < pack < ownerand inherit throughParent(Paramsmerges per parameter,Commandreplaces). Collision policy is JSON WINS, loudly: a kind asset may shadow even a Java-registered id, with one boot WARN naming the shadowing file and a matching validator finding, because a command-backed kind gives up that kind's engine services (the ask-first inventory fit check, the replayable offline retry) and the warning says so. - Adds one inventory-room probe paired with its own granter, so a payout that would not fit is refused before anything is charged instead of overflowing or vanishing. The quest, shop and achievement claim paths share it, and a full backpack with a free hotbar slot now correctly grants a single-stack reward that used to be refused.
- Adds
BonusDrops, ONE pack-authorable surface for what a moment hands over on top of whatever the game already gives for it - the lucky find in a broken block and the bonus a killed mob is worth, which were the same question in two vocabularies. One row per file atServer/MMOSkillTree/BonusDrops/: a nested.json Whengroup (Kind,BreakBlockorKillMob, plus aMatchname pattern), an optionalChance, aLootreference, an optionalNotify, andEnabled.Chanceis a percent factor formula, so{"Factor": "hytale:stat", "Param": "MMO_Luck", "Weight": 0.2}is literally how a lucky player is luckier, andLootis the same vocabulary a station's rare find and a quest reward speak - sharedLootablesby id,Rollswritten inline, or both, with a roll'sGrantspaying out items, native drop lists, commands or any registered reward kind. Native drop lists spill where the moment happened, at the block or at the corpse, beside whatever fell there anyway, with no vanilla table touched. The two moments keep their own per-trigger behaviour and their own documented default odds: aBreakBlockrow with noChanceis as likely as the player's own summed luck, aKillMobrow with noChancealways fires and leaves the odds to its rolls, and only a player's kill counts. - Adds one matching rule across both moments:
When.Matchspeaks the shared name grammar (Ore_Coalexactly,Ore_*starting with,*_Crackedending with,*Trork*anywhere in the name,*everything, case ignored) and the row pinning down the most characters wins - exact first, then the longest literal core, then the catch-all. Luck's old first-match-over-a-hash-map pick was nondeterministic for two overlapping patterns; it is deterministic now, and a family row plus a one-block row compose without either file knowing about the other. An ENABLED row with noLootis a first-class deliberate hole: the more specific pattern wins, so it stops a broader row covering that one name, which is different from"Enabled": falsetaking the row out entirely and letting the broader row take over again. - Adds
mods/mmoskilltree/bonus-drops.json, the one owner layer over both moments: it overrides a row BY ID through the same codec with the shipped row as its parent, so the body merges per leaf.{"Enabled": false}switches a row off without erasing its pattern, aLootblock alone re-tunes what it hands over, and"__DISABLED__"in place of the whole body is the one-word spelling. Precedence is the ordinarydefaults < pack < owner. - Adds a shared
Notifygroup on both moments ({Enabled, Color, Icon, TitleKey, SubtitleKey}, off unless a row asks, fired once after the grants and only when something actually landed), which retires luck's hardcoded English "Lucky!" literals: the shipped luck rows author the note explicitly and its wording comes from translation keys in all nine languages. - Adds the fourteen built-in luck rows as readable jar assets (ten ore families, plus four cracked-ore variants shipped as worked deliberate holes), so a pack can
Parent-retune a built-in row and a server owner can read the file that decides what a lucky break gives.config/defaults/LuckDefaults.javais deleted; no Java default luck table survives. - Adds
BonusDropsValidatorin place ofLuckValidator, auditing both moments under one domain: an owner body that cannot be read, a row naming no moment, a pattern with a wildcard in the middle, a row-level chance that can never rise off zero, an unknown factor id, a row that hands nothing over (reported as information, since it may well be a deliberate hole), and two rows pinning down the same literal core in one moment. - Adds native
Parent-inherited dialogues as fully structured assets: a dialogue is decoded once through its own codec, so an unknown condition or action type fails the read loudly at load instead of misbehaving at the first conversation. Authored dialogue JSON is unchanged. - Adds NPC identity as first-class content:
Identity.NpcIdandIdentity.Aliaseson a placement, plus overlay assets atServer/ZiggfreedCommon/NpcIdentities/for aliasing, the same character in two roles, and renames. Identity resolves down a documented ladder - the placement's own declaration, then a role overlay (walking the native variant chain), then a group overlay, then the role id - and a placement that declares no.json NpcIdIS its role, so identity costs an author nothing and two placements of one role are two standings of one character (a quest bound to it is offered, credited and handed in at either). Authoring anNpcIdopts out and makes a standing a character of its own, which is how a step is scoped to one of them: the jar's temple guide is the worked example, with its own id plus the spawn guide's as an alias. One authority answers "does this NPC answer to id X" for quest offers, turn-ins, dialogue conditions, the quest list, talk objectives, and map waypoints, so those can never disagree. - Adds an explicit talk-credit pipeline:
MarkTalkedin a conversation andZigTalkCrediton an NPC with no conversation both feed one credit sink with a shared de-duplication window, so one conversation credits once no matter how many beats mention it. A talk at an aliased placement credits both the primary id and each alias without double-counting the statistic, and a nativeNpcTalkedEventfires once per credited conversation for third parties. - Adds an honest split between "this NPC is your destination" and "this NPC can take your hand-in" for new-shape quests: a
TALK_TO_NPCstep still lights the character up as where to go, but their conversation no longer offers a hand-in for a quest with nothing to hand over. Old-format quests keep the previous conflated behavior exactly, so nothing already authored changes. - Adds achievement categories and point milestones as SHARED-schema assets (
Server/ZiggfreedCommon/AchievementCategories/for order, icon, title key and subcategories;/ .json Server/ZiggfreedCommon/AchievementMilestones/for the points ladder), so a pack can declare its own category and a server owner still outranks it. A category's filename IS the category word content writes; a milestone's/ .json Thresholdis its identity, so a file re-authoring 3000 replaces that rung outright whatever it is called. A milestone pays out through the sharedRewardsvocabulary (Autolands the instant the total is crossed,Claimwaits on the achievements page), so a milestone can hand over a real item or roll a loot table like anything else. The five built-in point milestones ship as assets rather than Java, andmods/mmoskilltree/achievements/point-milestones.jsonstays the sanctioned owner layer. - Adds ONE
Wherespelling for every world-targeting field: an NPC placement, a world rule, a dialogueWorldcondition and a mob-scaling world rule all author the same nested group (Match/GameplayConfig/ExcludeMatch), scored on one specificity ladder, so an author learns one grammar and a mod never invents a second. A bare word underMatchis an EXACT world name, andExcludeMatchtakes the same grammar as a FILTER over the positive axes rather than a complement of them./mmoconfig validatereports aWherematching none of the loaded worlds and one with no positive axis at all, from one shared check every surface calls. - Adds ONE shared loot asset type. A
Server/ZiggfreedCommon/Lootables/gained the competing-outcome half it was missing, a nullable.json Poolgroup of{Rolls, Entries[{Weight, Conditions, Grants}]}, so a score-tiered or luck-tiered reward pool is an ordinary shared table anything can point at.Pool.Rollsis the standard{Base, Factors, Clamp}factor formula, so "one more pick per 1200 points" is a term weighted1/1200over any registered reading rather than a bespoke field, and a premium entry's requirement is an ordinaryConditionsgate; picks draw with replacement, and an entry can hand over items (with an optionalCountMaxquantity range), native drop lists, commands, or any reward kind. - Adds
Lootable.ContributesTo, cross-mod loot enrichment: a file naming another table's id has its rolls and pool entries folded INTO that table on top of what is already there, so a pack enriches another mod's loot without touching its files and removing that pack removes exactly what it added. Ordinarydefaults < pack < ownerid layering resolves first and contributions after, so an owner override of a table keeps every contribution made to it, and how often the merged pool is drawn stays the target's decision. - Adds dialogue authoring in the shared store: a conversation is authored at
Server/ZiggfreedCommon/Dialogues/with an/ .json Ownerleaf, the same way quests and achievements are, and this mod folds its own owner plus every unowned file.Server/MMOSkillTree/Dialogues/stays registered as the compat surface for packs written earlier, precedence runs shared store then legacy pack layer thenmods/mmoskilltree/dialogues.json, and an id authored in both stores is named in one boot warning with both file locations rather than silently shadowed. - Adds a Mastery Trainer NPC at the Forgotten Temple, standing beside the guide: taller, darker, staff in hand, opening the Mastery screen on use, and gated on the mastery feature so it never appears on a server that has mastery switched off. With the Mastery pack installed, the trainer talks instead: a short conversation offering their quest list, the mastery menu, or a farewell (the pack overrides the trainer's placement by id and binds its four mastery quests to the trainer as their giver, accepted and handed in at that quest list).
- The quest log IS the shared Objective Book.
/mmoquests, the hub tile and every menu's quests tab open ziggfreed-common'sObjectiveBookPage, which gained the whole quest-log feature set for it: category tabs, a search field, a tag dropdown and status filters (all read from the sharedListingschema any mod's quests author), per-row track/untrack and abandon on carried quests, and a "Completed" filter view for finished ones. The page wears this mod's theme through the book's theme seam, and the mod ships no quest page of its own; the old page'sPages/QuestPage.uifamily and itsui.quests.*strings are gone. - Completing a quest now arms whatever it unlocked IMMEDIATELY: an auto-accept quest gated on the one that just finished (the tutorial chain) lands on the tracker the moment its prerequisite settles, not on the next login. An auto-accepted quest also records its giver as where it was taken, so a hand-in of the "return to whoever gave you this" form resolves at that character.
/mmoquestadmin resetnow puts the player back at day one for real: the same maintenance a fresh login runs re-arms every auto-accept quest the wipe re-opened, and the tracked-quest HUD repaints instead of keeping the wiped quests on screen.- A locked quest row now says WHAT is missing: a quest gated on another quest reads "Complete quest:
" on the giver's list and in the book, instead of a generic "Not available yet"; a full quest log gets its own line too. - Fixes the NPC quest list's header showing a raw id (such as
Mmo_Hub) instead of the character's name: the page now reads the name off the live character it was opened on, and the static role-name resolve seeds the engine's execution scope first, the way the engine's own off-entity reads do, so a Variant role built on an Abstract template resolves everywhere else too. - Fixes ability cards and tooltips showing literal
//markup (the onboarding ability step, skill-tree claim cards, bind-page tooltips): every composed fragment now carries its emphasis as structured message styling, which survives nesting, and the number/school/source tints moved from lang markup to the one Java vocabulary that builds those fragments. - Fixed: slows, roots and hastes reach player targets too. An
EntityEffectspeed change used to author only the NPC leaf (ApplicationEffects.HorizontalSpeedMultiplier), which a player's client never applies - so Crippling Slam's slow, Arcane Lash's root and Concussive Dive's stun held mobs but let a player target keep running, and Tempest Vault's self-haste never sped its caster up at all. Every shipped speed-changing effect now also authors the Update 6 player leaf (MovementEffects.SpeedMultiplier) at the same value, so mobs and players feel the same slow. A newabilityaudit WARN (EFFECT_SPEED_NPC_ONLY) catches the gap going forward: any MMO-familyEntityEffectauthoring the NPC leaf away from 1.0 with no player leaf beside it is reported as a slow that does nothing to players. - Adds a
masteryaudit domain under the startup audit and/mmoconfig validate: every ability id, skill id, modifier paramKey, prerequisite and cross-node gate reference a track makes is checked (a dangling one was silently inert before), and the content fold's own findings are replayed into the same report. - The mastery node effect line covers the whole canonical paramKey vocabulary in all nine languages (five new nouns); no canonical key falls to the raw-text fallback any more.
- The mastery paramKey check accepts the clamp-store extension seam, the same rule the ability audit applies: a mastery modifier's
ParamKeypasses when it is canonical OR aServer/MMOSkillTree/ModifierClamps/DECLARES it (a clamp file authoring.json IsParamKey: false, like the hit-sideHitDamage.jsonbucket, bounds its fold without declaring an authorable key), and the WARN on a genuinely unknown key now names the clamp file as the way to declare a new one. - Adds a kill-rarity attribution seam for companion mods (
MMOSkillTreeAPI.registerKillRarityProvider, additive on the FQN-frozenapi/surface besideregisterMobKillXpMultiplier): ONE registered provider names a killed mob's rarity, the shared kill producer consults it once per credited player kill (through the kill-qualifier bridge the mod registers into the shared progression runtime), and the answer rides the kill moment as its qualifier. That one answer feeds BOTH halves of a kill's rarity: quest and achievement criteria authoring the rarity word as theirQualifiermatch the kill - the tier achievements (First_Legend_Kill, Legendary_Slayer, Rare_Hunter, Elite_Slayer) progress on scaled kills - and the mob-drop command placeholders resolve a kill row's{tier}to it. An unqualified criterion still counts every kill exactly once (there is one dispatch per kill, never a second qualified re-fire). MMO Mob Scaling 1.1.0 registers its rolled rarity ladder (Rare / Epic / Legendary / Boss); without a provider, or for an unscaled mob, kills fire unqualified and{tier}stays empty.
Technical: the ability-modifier vocabulary and grammar (the pre-release break wave)
- The ability-modifier grammar is PascalCase (
{"Shape","ParamKey","Value","TargetAbility","School","CombatTarget","Condition"}); the arithmetic shapes areFLAT/PERCENT/SET(replace, last-write-wins), and any modifier can carry an optionalCondition(SelfHp/TargetHp{Min,Max}+HostileOnly/OnCritOnly/Filter). Field names match case-insensitively like every hand-parsed surface, and the two shape words the 1.5.x public mastery format shipped,OVERRIDEandADD_PARAM, still parse asSET- that is the whole legacy surface: theKeyfield alias, the flat camelCase HP-bound spellings and theCONDITIONALwrapper shape are not read. Every writer and every shipped entry emitsParamKey. - Adds
ability/ParamKeys, the one canonical paramKey vocabulary; the content validator checks every authored modifier key and every body-declaredMmoNumkey against it plus the pack extension seam (a pack declares a new key by shipping its ModifierClamps asset), WARNs an unclamped authored key, and ERRORs an unclassified body declaration. The mastery validator's canonical-key check is a view of the same authority. - Adds seven ModifierClamps assets (onHitHealPercent, lifesteal, lootMultiplier, comboFinisherBonus, schoolResist, armMultiplier, armFlatBonus), per-leaf clamp merging across layers, per-key clamping inside the fold, and a new owner file
mods/mmoskilltree/ability-clamps.json, loaded beside the other owner configs and re-read (with a re-fold) by/mmoconfig reload. - The four non-ability paramKeys are clamped at their own aggregation seams, since their values never pass through the per-cast fold the other clamps guard: the mastery swing aggregation clamps its summed
lifesteal/comboFinisherBonustotals, the per-skilllootMultipliersum clamps before it is cached, the mastery school-resist mirror clamps each channel's summed FRACTION (soSchoolResist.json'sMax0.9 is the same 90-point ceiling the damage fold enforces), and the tree+mastery stat aggregator clamps the mastery subtotal of any channel whose paramKey is non-ability. A stack of mastery nodes past a clamp now caps exactly the way stacked gear or cast modifiers do. - A
ModifierClamps/*.jsonfile says whether its key IS a modifier paramKey with a nullableIsParamKeyleaf (absent means true, so every existing clamp keeps declaring its key).HitDamage.jsonauthorsfalse: it clamps the hit-side percent fold bucket, which no modifier may target directly, so shipping its bounds no longer smuggleshitDamageinto the authored-key vocabulary - the ability and mastery validators consult the declared set and flag a modifier authoring it, while the bounds still apply. A pack declaring a genuinely new key still just ships a clamp file for it. - The defense-reduction cap and the school-resist ceiling are clamp assets an owner can retune. A reserved
ModifierClamps/DefenseReduction.json(Max0.9,IsParamKey: falselikeHitDamage.json) bounds the victim's total percent damage reduction from defense, resolved at the damage pipeline's one cap site through the new fail-closedParamClampTable.maxOr(armor penetration still applies before the cap, and with no asset loaded the 0.9 convention stands in - a safety cap exists even on a broken pack layer); the school-resist reconciler's whole-percent bucket ceiling and the damage-school audit's overcap WARN both derive fromSchoolResist.json'sMaxthrough the same read, so retuning either bound inability-clamps.jsonmoves every reader of it together. - The mastery damage mirror is bounded by the same clamp the cast fold uses: each per-channel total the mirror writes onto the
MMO_Damage*/MMO_Bonus*channels is clamped toDamage.json's bounds in its authored unit (a percent total as the summed fraction, a flat total as a value), so a repeatable-purchase damage stack caps on the channel route exactly as it does per cast. MmoArmNextHit's arm flavor is the explicitMomentumknob (a 0.0FlatBonusis no longer load-bearing) and its keys split toarmMultiplier/armFlatBonusso an arm scale and a damage value never share a fold bucket. The ability audit WARNs aMomentum: truenode that also authorsFlatBonus/OnHit/CastRay/CastDamage- the momentum branch never reads those leaves.- Adds the
MMO_AbilityDamageRadiusgear channel (damageRadius) besideMMO_AbilityRadius(radius). - Schema nestings:
Casting.SelfHp,PassiveTrigger.SelfHp/TargetHp/Bonus{FromAbility,Multiplier,ScalesParam},Presentation.Animation{Name,Item,Slot}andPresentation.Particles{Id,MaxMs}(both with a bare-string shorthand);MmoProjectile.Raycast,MmoDeployable.FaceCaster,MmoHookshotPull.GapCloseSeconds;MmoDot/MmoHeal's effect key unified asEffectAsset. - Passivity derives from the
PassiveTriggergroup's presence; the legacyIdentity.Passiveleaf still decodes and gets a validator WARN. - The three snake_case interaction-Type aliases are deleted; the scan-only cause tag is
MmoCauseScanRef. - The nine slam-family native damage leaves are
MmoDamagenodes, so gear and mastery damage bonuses reach them. - Adds two grantable ability-mod exemplars: Duelist's Instinct (TRIGGER, grants the Punishing Dodge passive) and Tremor Aftershock (ADD_STEP, a second shockwave on Ground Slam).
- The GateOnly-without-paired-cast WARN (
CAST_GATE_ONLY_UNPAIRED) is wired into the ability audit. - The body-fidelity oracle gains a post-wave baseline: a verbatim capture of every shipped ability body (
post-wave-fidelity.json) the equivalence check compares against going forward, with the frozen pre-wave digest kept byte-identical as the record of the pre-wave shape.
Technical: the progression move is write-first, claim-last
ProgressStateMigrationwrites every legacy leaf BEFORE spending its one-time claim, each leaf family under its own guard: a throw mid-move now costs a retry on the player's next ready instead of a permanently-burned claim over half-moved data (every write is a SET, so the clean leaves re-move harmlessly). The achievement-progress pass re-keys positional records onto criterion ids through the folded catalogue (a pluggable resolver, so the move stays pure logic a test can drive), andZigProgressComponent.setAchievementUnlockedAtkeeps an existing stamp (putIfAbsent, the legacy contract), so a replay can never rewrite the instant something was first earned.
Technical: `/zigprogress`'s two groups conjugate the same way
- The shared admin family's achievement group gains
claim(--achievement=, the peer ofquest claim), its force-it verb isgive(the same word, the same meaning, asquest give),resettakes--achievement=on both groups (the one-item wipe subsumes the oldrevoke, meta-cascade included), andachievement statustakes the same one-id filterquest statushas. Node depth follows nesting:reload, the one family-level verb, sits under the family and every group verb under its group. The MMO's own aliases keep their shipped names and gates. A contract posted on several boards now credits every one of them on completion - the primary board rides the fullCOMPLETE_BOUNTYfan-out and each further board fires as a targeted alias, so an achievement naming a second board counts while the untargeted "complete N bounties" tallies count one completion.
Technical: the reward reading converges on ziggfreed-common
- A reward reads the same everywhere, through the library's ONE chip ladder.
RewardPreviewBuilderno longer derives aRewardSpec's name and icon per kind: both come from ziggfreed-common'sRewardChips(a reward's own words and picture first - an authoredIconwins on every rung, the contributed one included - then theServer/ZiggfreedCommon/RewardKinds/Mmo_*.jsonkind files, then the item form), and the newpages/render/MmoChipReadingis contributed process-wide at setup - the same rung zc-commerce's wallet reading uses - carrying only the lines a file cannot say: the boost multiplier-and-duration sentence, an ability-mod title read out ofAbilityModCatalogfor a modifier that never named itself, a parsed/giveor/mmoboost givecommand line (an unparseable one still reads as the neutral generic label, never the raw command), and the XP line for a custom skill whose pack shipped noreward.xp.key. The roll-at-grant-time kinds (lootable / drop list / effect) need no arm at all: ziggfreed-common ships their presentation-only kind files itself, soreward.line.lootable/reward.line.droplist/reward.line.effectretire from all nine of this mod's locale files in favour of the library's ownziggfreedcommon.loot.*copies (same wording, same icons). The quest book takes the same shared ladder with no per-consumer reward reading (MmoObjectiveBookDepsregisters no chip source), so this mod's panels and toasts, the quest book, the NPC quest page and the board cannot disagree about what a reward is called - and a reward of a kind nothing on the server can read rows as the localized "Special Reward" label, never the raw kind id. - XP wording settles on the per-skill key family, with digit grouping. The
reward.xp.values gain{0, number}in all nine locales, so every surface shows the grouped "+2,000 Mining XP", andMmo_Xp.jsonauthors the newArgs: ["Amount"]so the amount binds as a typed number wherever the family renders. The Java-composedreward.line.xp/reward.line.xp_allsurvive only as the custom-skill fallback line. Mmo_Boost_Token.jsonauthors noNameKey: a boost token's one label is the computed sentence ("Swords XP x2 for 30m"), so a book row, a board row and a panel row all read that same sentence. Two reward-row wordings move deliberately and stay moved: a currency row reads through the shared wallet reading as "+50 Bounty Tokens" (the wallet's own icon and name, the plus kept, and the amount bound as a typed number so each player's own client decides the digit grouping), and an item row reads through the shared item form as "x3 Iron Bar" - both in place of this mod's private compositions.- Four unread lang keys retire from all nine locales:
reward.line.currency,reward.line.item,reward.line.amount(an authored reward title is used verbatim, never amount-wrapped) andreward.boost_token. - Reward-toast row capping routes through the library's
RewardToastLines(ToastablePage.showRewardToastand the quest completion toast), so the "+N more" overflow row is computed in one place instead of two private copies. - A display number binds as a typed param, never as server-formatted text - build-enforced. The cost chips, balance strip and cost summary route bare figures through the shared
{0, number}reading (Msg.num, andCostRenderer.currencyAmountMsgthrough zc-commerce's shared price composition), and the ability bind page's cooldown meta becomes the localizedability.bind_page.cooldown_metakey ("{0, number}s cd") in all nine locales, so digit grouping and decimals are each client's own decision.RepoHygieneTestgains the matching rule: aMsg.raw(...)/Message.raw(...)wrapping aNumberFormatter.*/String.formatresult fails the build, with// NUMBER-OK:(reason mandatory) as the one escape hatch for a number that is genuinely not localized display text.
Technical: the quest and achievement notices speak one lang-key vocabulary
- The keys the seven shipped FeedbackMoments files author unify under
notify.*:achievement.unlocked/achievement.points_awarded/achievement.server_first_lostbecomenotify.achievement_unlocked/notify.achievement_points_awarded/notify.achievement_server_first_lost, and the bounty-wordednotify.bounty_ready_to_claimin the quest-domainQuest_Parkedfile becomesnotify.contract_ready_to_claim, renamed across all 9 locales. Filenames are untouched.
Technical: six retired SkillComponent leaves stop writing
Currencies,LifetimeCurrencySpent,ShopPurchaseTotals,ShopPurchasesToday,ShopPurchaseEpochDayandSlotRerollskeep their decoders (the one-timeCommerceStateMigrationreads them at ready, before the first save can drop the keys) and their encoders return null - the same one-way self-retiring idiom the removed built-in skills use - so a stale ghost copy of the wallet no longer rides every save beside the live economy's own record. A rollback to an older jar still recovers the values from an un-migrated save.
Technical: English is authored in the `.lang`, not generated from Java
- The Java-baked English-defaults paradigm is deleted.
i18n/lang/EnglishDefaults.java(3,546 lines), theLanguageDefaultsinterface,i18n/lang/EnglishLangWriter.java, thegenerateEnglishLangGradle task andEnglishLangExportTestare all gone, and thei18n/lang/subpackage with them.src/main/resources/Server/Languages/en-US/mmoskilltree.langis the authored English now: it is edited directly, nothing regenerates it, and it is byte-identical to what the retired task last produced, so no key moved and no value changed. Adding a key is one line in that file plus the eight sibling locales. i18n/LangResourcesreplaces the in-code map by reading the shipped file back. It parsesServer/Languages/off the classpath, so/*.lang LocalizationConfig's layer-3 fallback andhasKeynow answer from the exact fileI18nModuleloads as a pack. The fallback can no longer disagree with what a client resolves - the whole class of "server-resolvable but not client-resolvable" drift thathasClientKeyexists to guard. It also absorbsStatTooltipLangGenerator's private parser andEnglishLangWriter'sescapeValue, so the mod has ONE.langparser and ONE escaping authority instead of three.- A net-new lang file is namespaced
mmoskilltree., matching what.lang ziggfreed-common(ziggfreedcommon.fmt.lang,ziggfreedcommon.commerce.lang) and RPG Stations already ship.I18nModulederives a key's namespace from the file's basename, so entries in a domain file drop the segment the filename carries and every call site keeps its bare key:hud.layout.titleinmmoskilltree.hud.langis writtenlayout.title.LangResources.MMO_LANG_FILESdeclares the set and the newLangResourcesTestfails the build if it drifts from what is on disk or if a locale is missing a declared file. The existingmmoskilltree.langis deliberately not split; the convention applies going forward. - Two dynamic key families become authored data.
skill.andreward.xp.were synthesized in Java from the liveSkillRegistry, which only ever reached the built-ins at build time anyway; both families are plain entries in the.langnow. Adding a built-in skill means authoring both keys in all nine locales (therecipesskill says so at the step). - Fixed: passive ability cards emitted a lang key that shipped in no locale.
ability.bind_page.passive_tagwas authored as an empty string, and the retired writer SKIPPED empty values because Hytale'sLangFileParseraborts a whole file on one - so the key never reached any.langwhileAbilityBindPagestill emittedMsg.trfor it on every passive card, leaving the client an id nothing could resolve. The tag is authored as real text ("PASSIVE") in all nine locales, matching its three sibling branches. Removing the generator is what surfaced it:MsgTrKeyCoverageTestnow checks literalMsg.trkeys against the shipped file rather than the Java map, so this shape cannot recur. - Tests, hooks and docs follow the source. Twelve test files read the authored file instead of the Java map;
SkillNameMsgClientResolvabilityTestnow pins both halves of the custom-skill bug (the fallback carries no runtime-only id AND the client probe refuses it) where it could previously only pin the gate.check-localization.ps1drops its generator-parity branch,no-balance-tests.ps1drops a dead exemption and carvesServer/Languagesout of its resource heuristic, and thelocalization/recipesskills, thelocalization-translatoragent and bothCLAUDE.mdrouters describe the authored-file workflow.
Technical: the content-key namespace is no longer registered
- The MMO registers nothing for authored-key resolution any more.
i18n/MmoContentI18nand itsContentKeys.installcall insetup()are deleted along with ziggfreed-common's whole registration surface: the sharedContentKeysnow attributes a bare authored key against the server's live loaded lang catalogue (the exact id first, then the key under any loaded.langnamespace with the alphabetically first id winning a tie, else the key exactly as written). Shippingmmoskilltree.langis the whole declaration, for this jar and for every content pack that ships into the same filename. No authored content key changed anywhere: aTitleKey,NameKey,FlavorKeyor FeedbackMomentKeyresolves to the same registered id as before.
Technical: the tracked-quest HUD becomes the library's, event-driven
- The in-world tracked-quest HUD is no longer this jar's. ziggfreed-common ships it (
objectives/hud/TrackedQuestHud, its.uiand the three native objective textures beside it, attached to every player at ready), and it repaints on the quest engine's SIX native events - a pin or unpin (the library's newQuestTrackedEvent), an accept, an objective moving, a completion, a claim, an abandon - with a burst folded into one paint per tick and NO tick anywhere.ui/hud/QuestTrackerHud,event/QuestHudTickingSystem,pages/render/TrackedQuestPanelRenderer,Common/UI/Custom/Hud/MmoSkillTreeQuestTracker.uiand its three textures, andCommon/UI/Custom/Pages/TrackedQuestRow.uiare DELETED. Titles and objective lines on the panel come from the shared text sources this mod already folds its content into, so the tracker cannot drift from the quest log; the panel looks exactly as it did. - What this mod keeps is one class,
pages/quest/MmoQuestHudDeps. It answers the library's three seams and nothing else: WHERE the owner put the panel and WHETHER the owner has it on, both read live offHudLayoutConfigunder the unchangedmmoskilltree:quest_trackerentry (so/mmoconfig questhudand the admin page keep working verbatim, now persisting and broadcasting through the library, and no saved layout moves); and whether THIS player wants it - the/mmohudquest preference plus the world's own abilities-and-HUD rule. The theme seam is filled with NOTHING: the native look is preserved bit for bit, and a theme, when there is one, goes throughUiRetinton the tracker's own selectors, never the page-frame painter. - Two moments no quest event announces are pushed by this mod.
/mmohud show|hide quest|allasks the library for one repaint after it writes the preference, so the panel hides or shows at once instead of on a tick; the world-change hook does the same after a player lands, so a world with HUDs off hides it and the return trip shows it again.MMOSkillTreeAPI.isHudVisibleis unchanged. - The quest-giver map waypoints rebuild off the same six events plus PlayerReady (
QuestWaypointService.install/refresh(UUID)), which is what let the HUD tick delete clean: what a player is being pointed at only changes when their quests do. - The tracked-quests SIDE PANEL on the quest log and the Skill Overview is painted by the library's
TrackedQuestPanelRenderer(rowsPages/ZigTrackedQuestRow.ui), handed a read-only subject through the newQuestService.readerFor; the two host pages keep only their#TrackedQuestsListcontainer,#TrackedQuestsEmptylabel and header.ui.viewxp.tracked_quests_emptyretires from all nine languages (the empty note is the library'stracked.emptyline now).
Technical: quest and achievement toasts become authored JSON
- Every quest and achievement lifecycle moment is authored JSON; no toast, jingle or banner for either is Java any more. ziggfreed-common gained an asset-driven feedback engine (
Server/ZiggfreedCommon/FeedbackMoments/) that the two shared progression engines announce every lifecycle moment to, and this mod SHIPS a file for each of the seven moments, reproducing today's wording, icon and sound, each file's colour spoken as its whole-toast.json Tone:Quest_Completed.json(the gold "Quest complete" line plus its jingle),Quest_Parked.json(the gold "open the Quests page" notice plus its jingle, with twoVariants:reason: no_spacereads red "free up inventory space" and stays silent,turnIn: accept_sitereads "claim your reward at the Bounty Board"),Quest_Objective_Progressed.json(the{step} ({current}/{required})tick, the green "Objective complete" finish,EveryPercent: 25on the toast for the quarter marks),Quest_Claimed.jsonandAchievement_Claimed.json(the legendary-chest chime,When: {collected: true}only, so a quest or achievement that settled on the spot keeps its one completion or unlock jingle),Achievement_Unlocked.json(the golden title, the points line, the achievement's own icon, the unlock jingle, AND the server-wide announcement as aBroadcastgroup whose two lines read their keys throughKeyArgfrom the announcing achievement's ownAnnounce {TitleKey, BodyKey}, carried into the moment asannounceTitleKey/announceBodyKey/announceSkill/announceAmountbyAchievementDefinition.toRuntimethrough the runtimeAchievement.momentArgs) andAchievement_Server_First_Lost.json(the quiet amber note). A server owner retunes any of it by overriding the file, with no jar involved, and can add aBroadcastbanner or a consoleCommandto any of them the same way. MmoQuestListenersandMmoAchievementListenerskeep ONLY the gameplay side effects: the completion log line,resetsOnCompleteand the completion fan-out (achievements, the completed-N credit, a contract's board tail) on the quest side; the log line and the PowerLevel cache drop on the achievement side. The objective-progress notification, its 25-percent-tick math, the parked wording, the announcement banner and every claim jingle are deleted from Java:RewardSoundUtillosesSOUND_QUEST_READY,SOUND_QUEST_CLAIMandSOUND_ACHIEVEMENT_CLAIM(the points-milestone claim keeps its chime asSOUND_MILESTONE_CLAIM, since a milestone is derived state the engine fires no moment for), andQuestService.claimQuest/AchievementService.claimAchievementRewardsno longer play a sound of their own. The parked wording branches on arguments the shared quest engine now carries (reason,turnIn) rather than on this mod's own bounty and inventory reads, so it needs no Java; the per-player verbosity gate staysMmoQuestSubject'sFeedbackAudienceanswer, now graded per moment (MmoQuestSubject.wantsToast:allsees every tick,milestonesthe authored marks and the finish,objectivesthe finish,questsno step toast,nonenothing) off the values the engine hands it. One deliberate change: a player onnoneno longer sees the red inventory-full notice either, which used to bypass the setting; the reward still waits, and the Quests page still shows it. Thenotify.bounty_inventory_fullkey, reachable by no branch, is removed from all nine locales.Meta.mmoskilltree.Announcekeeps onlyTitleKeyandBodyKey; the written-outTitle/Bodyleaves and the legacy owner-fileannouncementCommandare retired. Both lines are keys (a key no lang file ships is shown as written, so a one-server broadcast can put the sentence itself inTitleKey), and how the banner is drawn is authored once for every achievement inAchievement_Unlocked.json. An owner-file achievement still carryingannouncementCommandgets one boot WARN naming the replacement (aCommandgroup on that file, or aVariantsentry scoped byWhen: {achievement:), and the leaf is otherwise ignored.} - The runtime
Achievementgains theiconthis mod's own resolver produces, stamped on when the catalogue folds (and folded again on every republish, so a chain or category layer that lands later still reaches it), so the shared unlock toast paints exactly the picture the achievements screen does without re-walking the authored-then-chain-then-skill ladder per unlock. - A shared moment fires for shared content, so the toast reaches quests and achievements this mod does not own. The old Java handlers drew nothing for a quest or an achievement missing from this mod's own catalogue (a board contract published straight to the shared runtime, or another mod's content on the same server); the moment is announced by the engine for everything in that runtime, so such a completion or unlock now wears these files' wording and jingle. A line whose named value the moment did not carry is skipped rather than drawn with a gap, so content with no title stays silent. A server running two mods that both want their own words gives one of them its own moment ids rather than sharing these.
- A player's own
questNotificationLevelstill silences the completion toast. An authored file cannot read a per-player setting, so the moment engine asks the SUBJECT instead:MmoQuestSubjectanswers ziggfreed-common'sFeedbackAudienceoff the level the player chose, and a player onnonekeeps the jingle and loses the toast exactly as before. Only the personal toast is gated that way; a banner, a sound and a console command belong to the moment rather than to one screen.
Technical: the five progression seam fills delete
- The MMO no longer answers a single shared progression seam.
MmoQuestGates,MmoAchievementGates,MmoQuestTextSource,MmoAchievementTextSourceandMmoNpcOffersare DELETED outright, and none of them is replaced by a smaller version of itself: what each one knew now rides the runtimeQuest/Achievementthe fold already builds - the authoredRequiresblock, the words, the giver id and listing order, the one-winner flag, and availability as a live PREDICATE - so ziggfreed-common's ONE gate, ONE text source and ONE offer provider answer for this mod's older authoring format exactly as they do for the shared schema.ConsumerOwnsNothingTestfails the build if a class here implements one of those seams again. - A quest's whole requirement set is now one block. The older format spelled a permission node, a total-level floor and per-skill floors as leaves of their own beside
Requires; they are folded into it as ordinary factor bounds at the one point a definition becomes a runtime quest, so a permission, a level and a prerequisite are all answered by the same evaluator. An achievement'sclassRequiredfolds in the same way, as anmmoskilltree:classbound atAchievementDefinition's constructor, which is why no Java compares a class anywhere any more. - One
GateEvaluatorper server, and it is the library's. This mod built a second one over its own factor registry and its own completion probe, so a shop row rendered from saved records and the same requirement on a quest were two decisions that agreed only by inspection. NowMmoGateskeeps the LANGUAGE half (the lock reasons a player reads) and reads the shared evaluator, and this mod registers ONE factor vocabulary (GateFactors, saved-record-first) plus one factor context onto the shared runtime.MmoQuestGates.preSatisfiedAmountdeletes with it: the engine's own standing-value reading now reaches the same saved skills. - The quest-log cap and content availability became data. The engine reads the live
maxActiveQuestsnumber and a live availability predicate, so the "log full" and "unavailable" refusals are the engine's own - one refusal instead of two that could disagree. The engine counts the same set the quest screen's own "X/Y" header counts: a board contract is carried beside the quest log rather than inside it, so it spends no slot and is never refused for a full one, exactly as before. A bounty's park-at-its-board rule deletes here too: the engine already parks a contract at the board it was taken from. - A requirement with nothing to read now refuses, where three older leaves used to wave it through.
Permission,MinLevelandSkillRequirementswere each checked by a helper that skipped the check when it had no player or no saved skills in hand; folded into the requirement block they are ordinary factor bounds, and a bound that reads nothing shuts. It bites only where a requirement is evaluated with no records at all behind the player, and no shipped quest authors aPermission. - An achievement's claim rewards are now checked for somewhere to land before the claim spends, the same check quests already had. This mod's own screen already asked first and still answers "your inventory is full"; a claim reaching the engine from anywhere else now parks instead of paying out into nowhere.
- Server-first races keep working exactly as before (one winner, a loser keeps their criteria met and is told once), with the arbitration in the library, only the persisted claim table left here, and the notice itself authored as the
Achievement_Server_First_Lostmoment above. - What a server owner may notice. A quest kept out of open listings until its requirements are met is still offered at the character who hands it out, and a giver's list is sorted by the
SortOrderits files author, with quests sharing oneSortOrdernow falling into id order rather than catalogue order. A quest that setsRequirePrerequisitesWITHOUTHiddenis a second change on that screen: the giver now lists it only once the requirements pass, where it used to be listed locked from the start. No shipped quest is affected (every one that setsRequirePrerequisitesalso setsHidden), and/mmoconfig validatenames the combination asVISIBILITY_GIVER_GATEDso a pack author is told rather than left to find out. Because the whole requirement block is one question now, a quest gated behind a level floor, a skill floor or a permission is held back at its giver in the same way, rather than being listed there locked until the player is up to it; again no shipped quest pairs one of those withRequirePrerequisites. Going the other way, a quest a player is already CARRYING stays listed at its giver even if the feature behind it is switched off mid-run, which is what lets them finish and hand in something they were given before the switch. On the shared screens (the objective book and the NPC quest list), a quest refused by a requirement reads as the generic locked line rather than a prerequisite-specific one, because the refusal now carries the evaluator's own token naming the factor, the permission or the quest that shut it; this mod's own screens read that token and are strictly more specific than before. A step still reads with this mod's own auto-generated sentence unless its file authors aTextKeythat a shipped translation actually answers.
Technical: six more consumer fills move down into the library (the zc-lift wave, items 1-6)
- The dialogue factor vocabulary is no longer installed by this mod.
MmoDialogueEnginestops building aFactorRegistryand claiming the shared dialogue engine's one factor slot; ziggfreed-common's own wiring root seeds thehytale:standard library there at library setup (which also fixes conversations gated onhytale:factors failing closed on servers running the library without this mod). This mod's threemmoskilltree:factor ids already ride the process-wide contributions table (util/MmoFactorContributions), so nothing this mod's dialogues gate on changes. util/MmoFactorsis deleted. Itsabout(store, subject[, target])builder was the zcFactorContextverbatim with validity guards and no MMO type anywhere; the factory now lives onFactorContextitself and the two callers (event/MmoMomentReactions,util/LuckUtil) call it there.player/CommerceSubjectis deleted. The live-playerSubjectfactory is the library's ownSubject.of(player)/Subject.of(playerRef, player); the thirteen files that built commerce subjects through the local class now call the library, byte-identically.player/PlayerRefSubjectHandlemoves to ziggfreed-common (subject/PlayerRefSubjectHandle, unchanged contract: resolve-on-demand,Player/PlayerReffacets, the all-zero anonymous id). Its three callers re-import; the MMO'sSubjectHandleinterface stays, implemented by the two engine handles that carry real MMO state.util/PermissionUtil.resolvePlayerfolds onto the library's newPlayerAccess.player(PlayerRef)primitive at the same time (one behavioral hardening: a stale-but-non-null reference now resolves to null instead of throwing from the engine's ref validation).- The station flair-unlock provider is deleted (
integration/stations/StationFlairUnlockProviderand its bridge registration). Reading ziggfreed-common'sZigFlairComponentis RpgStations' own built-in provider now, so flair unlocks resolve on servers running RpgStations without this mod; the write side (/mmostation flair) stays here. pages/quest/MmoNpcQuestDepsstops filling the NPC quest page's completion hand-off. The library'sNpcQuestPageDepsdefault already routes a settled quest through the dialogue engine's installed quest runtime - the sameMmoDialogueQuestsinstance this mod installs - so the fill was a second spelling of the same answer. The theme painter and the completion toast stay, being real presentation policy.
Technical: the lock-reason vocabulary is the library's, and this mod ships only names
- This mod has no gate language of its own any more. The per-mod lock-reason model and its renderer (
content/gate/GateReason,pages/render/GateReasonRenderer, theui.gate.locked_*sentence family) are deleted; every locked surface renders ziggfreed-common'sLockReasonslines over the evaluator's structuredGateRefusalrecords, through the thinMmoGatesfacade (refusals/lockLines). One requirement is one sentence on every screen on the server, whichever mod's content authored it, and a refusal now quotes where the player stands - "Requires Mining level 40 (currently 12)" - off the very reading the evaluation was decided on, on every gated surface at once (the bounty board's contracts included, which never had the readout). - What this mod ships instead is NAMES: four
Server/ZiggfreedCommon/Factors/overlays in the jar. Thehytale:statoverlay bridges every per-skill level channel onto theskill.name keys the jar already ships (one line: stripMMO_Level_, lower-case, pattern-substitute) and folds each resolved name into the onefactor.skill_level"{0} level" phrase viaWrapKey, so a skill gate reads "Requires Mining level 40" for every built-in skill and for any custom skill whose pack ships its own name key - no per-skill phrase keys. The three aggregate channels (MMO_CombatLevel,MMO_TotalLevel,MMO_HighestSkillLevel) carry bespoke complete-name keys the wrapper never touches.mmoskilltree:mastery_nodereaches each node's own title off the compoundParam (: StripThrough ":"cuts to the node id, the pattern lands on themastery.keys the mastery pack already ships, and a node without one falls back to the generic name);.title mmoskilltree:classnames a class through its ownclass.key, and.name mmoskilltree:featurecarries a generic name a pack can override per id with an overlay of its own. Seven newfactor.*name keys, nine locales; the fourteen retiredui.gate.locked_*keys leave all nine (onlyui.gate.lockedsurvives, still painted as the bare LOCKED status word). - The mastery page paints client-resolved lock lines.
MasteryService.PurchaseCheckhands backMessagelines instead of server-resolved English strings (the bespoke mastery refusals - prerequisite node, choice-group sibling, cost shortfalls - nest their names as client-resolved arguments too), andMasteryPagepaints each as its own line, so the lock panel reads in the viewer's language instead of the server's. String-typed result toasts still flatten their one line server-side, as before. - The cheap path is still the right tool for a boolean check.
MmoGates.passes/firstFailureshort-circuit, which is what a per-row loop wants (the mastery track rail,ClassService.recheckAdvancements,QuestService.evaluatePrerequisites);MasteryService.canPurchasekeeps itslistEveryReasonknob, with both arms reading one collect-all walk so even the one-line failure toast can carry the current-value readout. - An
AnyOfrefusal stays ONE entry - its routes are alternatives, not separate chores - and carries what each route asks for, so the shared mapping reads it back as "Unlocked by: A or B" (the routes joined by a translatable or; one route reads as its own line, and a multi-ask route stays one comma-joined bundle). A passingNotreads as the negated sentence naming the met requirement, and a permission refusal reads its own sentence instead of squeezing into the numeric-bound frame - all of it the library's rendering, so every mod's gates read the same way.
Technical: the `content/reward` package shrinks to this mod's own vocabulary
- Four classes delete.
MmoRewardKinds(a seam over the ONE shared kind table that registered nothing: a payout site now namesRewardKinds.shared()directly, andisPayable(kind)moved ontoRewardSpecswhere the kind vocabulary already lives),RewardFit(a wrapper over the framework's ownLootRewardKinds.canAddAll, now asked directly by the quest and achievement surfaces that want to warn before an irreversible claim),RewardGrantService(the grant PASS is the library'sRewardGrants.grantAll, called directly by the three payout paths outside the two progression engines) andRewardParser(lifted to ziggfreed-common as a generic reader this mod builds with its own dialect).RewardExecutorkeeps only the connect-time spool drain; itsretryCommandOf/processQuestPlaceholderspair had no production caller left, because the framework'sCommandhandler is what resolves a retry line. The package drops from 1,571 lines to 1,122. - The two rules every payout site was applying in front of a grant became the library's. A reward authored
queueIfOffline: falseis dropped when nobody is there to receive it, and a reward that names nosourceis stamped with what actually paid it out, both once in front of every kind. The behaviour is unchanged; what changed is that a companion mod's payout gets the same two rules for free instead of re-deriving them. - Reward refusals still happen at LOAD. A
commandwith no command, a currency reward naming no wallet or a non-positive amount, and anability_modwith no mod id are still skipped with a boot WARN naming the file, and/mmoconfig validatestill reports them. The rules are this mod's (RewardSpecs.refusalReason) and are handed to the shared reader, so a pack author finds out at boot rather than a player finishing a quest and receiving nothing. - One factor registry in this mod, not two. A luck formula, a bonus-drop chance and the bonus-drop validator resolved factors through a second, live-only registry while every
Requiresblock read the saved-record-first one, so an authoredhytale:stathad two possible readings depending on where it was written. All three now readGateFactors.registry(), which is a strict superset; nothing reads differently today because every formula site runs with a live entity in hand. Which entity a formula is asked ABOUT is the other half of the question, and it is the sharedFactorContext.about(...). - A payout outside the progression engines is built through the shared
subject/PlayerRefSubjectHandle, since who a payout is for is an identity question rather than a reward one.
Technical: the producer claim mechanism deletes; the placed-block guard becomes the library's
- Block break, mob kill, craft and pickup progress is produced by ziggfreed-common alone. The claim-and-stand-down mechanism is gone: the library's four generic producers always run, this mod claims nothing, and
MMO_PRODUCED_KINDSdeletes. Those producers feed the same sharedQuestEngineandAchievementEnginethis mod's own services read, so one break advances one objective once. What this mod DOES with those four moments (XP, statistics, bonus drops) is a reaction to the library's dispatch,event/MmoMomentReactions, described in its own section below; the harvest ALIAS (a pickup also credited as break-blocks progress under the picked-up ITEM id) stays this mod's own to send, and a kill landed by a turret or summon reaches the library's own kill producer through itsKillAttributionseam. - The place-then-break and place-then-harvest guard moved into ziggfreed-common, as the shared
world/placed/PlacedBlockLedger.skill/PlacedBlockTrackerdeletes. The library's own place-block system is the single writer, and the library's producers plus this mod's XP path all read the SAME ledger, so quest progress and XP can never disagree about whether the breaker put that block there themselves - which the old arrangement could not have survived, since the library's producers knew nothing about this mod's tracker. The four knobs stay exactly where server owners already have them, inskill-config.json(trackPlacedBlocks,strictPlacedBlockCheck,placedBlockExpireMinutes,placedItemExpireMinutes); this mod installs them as a live policy on the ledger, so a reload still moves them. A creative-mode placement is still not recorded. - Placements do not survive the upgrade. The ledger keeps its own file at
mods/ziggfreedcommon/placed-blocks.jsonand the oldmods/mmoskilltree/placed-blocks.jsonis not read: it is renamed to.legacyat boot with one notice naming the replacement, never deleted. Blocks placed before the upgrade are forgotten once, so for one expiry window a player could break something they had placed earlier and be credited for it; placing anything after the upgrade is remembered as usual. The pre-1.0.2 hash-keyed placed-block key migration retires with the file it migrated. - The boot log's
[progression] producersline changes shape: the per-kind "N of M library defaults standing down" count is gone, replaced by a single always-on line naming the four built-in kinds and pointing atProgressDispatch.fireas the open extension point for a net-new moment. - Everything a produced moment used to carry still travels with it, which is what makes the swap invisible in play. The library's shared dispatch resolves the ZONE for itself now (through ziggfreed-common's own
ZoneLocator, whichutil/ZoneUtilwas a line-for-line copy of; that copy is DELETED andProgressEventsreads the library's), so a zone-scoped kill or block break still completes; the achievement POINTS-MILESTONE toast rides the registered call scope rather than only this mod's own facade, so a milestone crossed by a produced break or kill still announces itself (a milestone is derived state the engine fires no event for, and a missed one would have stayed silent forever, since the next call's before-snapshot already holds the threshold); it wraps each engine call in this mod's registered call scope, so a finished quest still fires its message, its jingle and its objective line; and it asks this mod first whether the player's quest and achievement systems are switched on, soenableQuests/enableAchievementsand their bypass permissions still hold for those four kinds - contributed as ONEProgressionSystemGate(quest/MmoSystemToggles) rather than answered by refusing to build a subject, since the same subject carries the player's wallet and board state and a null would close the shop along with the quest log. The harvest alias above stays this mod's own to send, because no library producer covers it; a kill landed by a turret or summon is the library's producer's to fire, once this mod'sKillAttributioncontribution names the owner (the attacker itself has noPlayerRef). BreakBlockEventSystemdeclares itself BEFORE the library's block-break producer in the engine's own system-dependency graph. A break this mod cancels for an unmet tool or item-level requirement is one the producer must see cancelled, and without the declared order the engine was free to sort them either way.
Technical: the MMO reacts to a produced moment; it does not re-detect it
- The five block / kill / craft / pickup / place event systems collapse into ONE reaction listener.
event/MobKillEventSystem,event/CraftRecipeEventSystem,event/PickupItemEventSystemandevent/PlaceBlockEventSystemare DELETED, andevent/BreakBlockEventSystemkeeps only what CANCELS the engine event (the tool-type and item-level requirement gates and their notifications, still declared BEFORE the library's break producer, with the unordered fallback and its warn untouched). Everything they awarded - gathering / building / crafting / kill XP, theblocks_broken/items_pickedup/items_crafted/mobs_killedstatistics, theLuckUtilbonus drops, the mob-drop commands,PassiveAbilityTriggers.onKill, the fish branch, the crafting eat-veto mark and the harvest alias - now lives inevent/MmoMomentReactions, this mod's oneMomentListener, registered into the shared progression runtime fromMmoQuestEngine.register()and fanned by ziggfreed-common'sProgressDispatch.fireFIRST, before the subject test and both system gates: a player who has never opened a quest log still mines ore for XP, andenableQuests=falsenever switches off mining XP. The method bodies moved rather than being rewritten; every gate inside them (the entity blacklist,PermissionUtil.canGainSkillXpByName, theMMOSkillTreeAPIkill-XP multiplier,LethalHitAttribution, the deployable skill list, XP per craft ACTION never per unit) is intact. - XP, statistics and bonus drops hang off the library's produced moments, through its typed payloads. Each reaction reads the record ziggfreed-common's producer ships with the moment (
BlockBreakPayloadfor the block position a lucky find drops at,PickupPayloadfor the picked-up stack,CraftPayloadfor the RECIPE id the craft XP table is keyed by beside the moment's output-item target,MobKillPayloadfor the victim and the death,PlaceBlockPayload) and does nothing without it, so a library kind fired by hand advances objectives and earns nothing here. The placed-block anti-exploit guard is now read by NOBODY in this mod: a placed-then-broken block or placed-then-picked-up item produces no moment, so no XP, no statistic, no drop and no progress, and the two ledger reads this mod used to make are gone with the systems.ProgressEvents.fireStatisticsOnlyis deleted (the reactions feedStatisticsService.handleEventdirectly);fireObjectivesOnlystays for the movement accumulators. - Turret and summon kills credit the owner through the library's attribution seam.
DeployableAttribution.KILL_ATTRIBUTION(the existingresolveOwner, in ziggfreed-common'sKillAttributionshape) is registered beside the listener, so the library's own kill producer fires a deployable's or summon's kill for its owner and the kill reaction routes the XP to the spawning ability's skills off the raw attacker the payload's death names. The MMO-side "fire the objective half myself for the redirected kill" branch is gone withMobKillEventSystem. - The harvest alias never re-enters the fan-out. A pickup also credits
BREAK_BLOCKunder the picked-up ITEM id (no library producer fires that); the pickup reaction sends it through the library's new engines-onlyProgressDispatch.fire(..., DispatchOptions.OBJECTIVES_ONLY)overload, which reaches no listener, so one harvest cannot pay block XP twice or count ablocks_brokenit never earned. - Two behaviour notes an owner may notice: a CANCELLED placement earns no building XP any more (the deleted
PlaceBlockEventSystemnever checked the cancel; the library'sPLACE_BLOCKproducer does), and a CREATIVE-mode placement produces noPLACE_BLOCKmoment at all (the producer mirrors the placement recorder's three filters), so it earns no building XP and advances no place-block objective. - The deprecated-engine-API convergence reaches
event/: every plain-deprecatedDamage#getCause()read inCombatDamageEventSystemandCombatXpEventSystembecomesDamageUtil.causeOf(damage),CombatDamageEventSystem's private duplicate ofAbilityDamageUtil.resolveCauseis deleted in favour of the one implementation, every rawstore.getComponent(ref, PlayerRef.getComponentType())in the package becomesPlayerAccess.playerRef(store, ref), andEntityCombatTextSanitizeSystemno longer returns the deprecatedAllLegacyLivingEntityTypesQuery: it queries the engine's ownUIComponentListcomponent - the one it sanitises - and declares itselfOrder.AFTERthe engine'sUIComponentSystems.Setupthat puts it on every living entity's holder (with the same unordered-fallback-plus-warn shape as the break gate). - Tests:
MmoMomentReactionsTest(a hand-built moment, no ECS: an unknown kind and a library kind without its payload are left alone before any world handle is touched; the attribution answers nobody for a stranger) andMmoProgressionRegistrationTest.thisModReactsToTheLibrarysMomentsAndAttributesItsSpawnedKills.CraftRecipeEventSystemTestretires WITH its system; its guarantee (a batch craft counts every unit, clamped to at least one) now lives in ziggfreed-common'sZigCraftProducerTest.
Technical: the deprecated-accessor shim converges on one shared primitive
- The mod reads the non-deprecated replacements for
Inventory#getStorage/getHotbar/getActiveHotbarItem/getCombinedBackpackStorageHotbarandPlayer#getPlayerRefthrough ziggfreed-common'scom.ziggfreed.common.inventory.PlayerAccess, the ONE SHARED primitive this mod and RPG Stations both call for that accessor shape, so the replacement for a deprecated engine accessor is never re-derived per mod.util/PermissionUtil#playerRefOfis a one-line delegation to it. Reading straight off aPlayeranswersnullwhen the ref cannot be resolved (including anullref, which is what the old mod-local copy dereferenced unguarded); the three sites that dereference the combined-inventory view were audited and either already fail closed or carry an explicit null check. Every inline fetch of the Hotbar section or thePlayerRefcomponent off a live store and ref in this mod goes through the two-argumentPlayerAccess.hotbar(store, ref)/PlayerAccess.playerRef(store, ref)forms as well (the commands, the pages, the player-data seam, the plugin's own ready hooks and the frozen API's XP notify path included); the onlyPlayerRef.getComponentType()mentions left are ECS system QUERY declarations, which is what that call is for. - This is a pure DRY consolidation plus a module-graph constraint (zc-core sits below zc-entity, so the lifted class reads native components directly rather than delegating through
HeldItemUtil), not a gameplay change. It does NOT fix unarmed-combat XP: every downstream consumer of a null held-item read already resolves to "Unarmed" and awards its configured XP, so the guarded-vs-unguarded distinction was never the cause of that symptom - see the1-6-0-progression-dry-waveplan's ruling 30/39 if that diagnosis resurfaces.
Technical: a seam holding a store and a ref derives the player's reference instead of taking one
- Every
(store, ref, playerRef)triple across the mod collapses to(store, ref), and the reference is read off the entity through ziggfreed-common's one accessor at the point it is wanted. 119 seam signatures in this mod and their call sites moved in one pass (194 across the mod, the shared library and both sibling mods together): the XP, reward, statistics, luck, quest, achievement, class, ability, dialogue and page surfaces alike. Nothing about what any of them DOES changed. - The point is that a carried reference can disagree with the ref beside it. Two handles for one player is two chances to pass the wrong one, and the mistake is invisible: a seam handed one player's entity and another's reference reads and writes two different people, and every type still checks out. Deriving removes the possibility rather than documenting it.
- The subject records keep a
playerRefFIELD, and no factory that could read one takes one instead.MmoQuestSubject.liveandMmoAchievementSubject.livederive it once off the entity handles they were already given, so the field is a snapshot of the ref beside it rather than a second input; the component-onlyofforms carry none at all, because a subject with no world behind it has no player to name.MmoQuestSubject.savedis the single factory that accepts a reference, for the one case with nothing to read it from - a requirement evaluation holding saved records and no store - andGateSubjectreaches it rather than building the record itself. A public record's canonical constructor cannot be narrower than the record, so that constructor stays reachable; nothing outside either record calls it, and both records say in their own javadoc that it is the factories' implementation rather than a way in. They stay records rather than becoming classes with a private constructor: the value equality a record generates is part of what they are, and hand-writing it back to hide one constructor would be the worse trade. - A seam whose reference names a DIFFERENT entity than its ref keeps taking one, because there is nothing there to derive it from:
CombatXpEventSystem.recordLethalHitAttributionand the kill reaction'sresolveLethalHitSkills(event/MmoMomentReactions) are both keyed by the VICTIM and have to name the attacker, so the attacker's reference is still passed in. So is the engine's ownAbstractPlayerCommand#execute, whose signature is Hytale's rather than ours. A PAGE is the same shape from the other side: a screen opened at a character is anchored on that character's entity, soNpcQuestPages.openand the twoCommercePagesopeners read the player's reference off the player they are handed rather than off the anchor. - A seam that already read the reference hands it on rather than dropping it - and a seam nobody downstream reads it from does not.
ActiveAbilityServicereads it once into the cast attempt and every later step in the same cast uses that;CommandRewardService.executeSingleRewardtakes the reference its caller already resolved, so a level-up granting five rewards reads the component once rather than five times; the per-hit combat chain carries the attacker's reference (and, as a plain boolean, whether the victim was a player) from the one read at the top of the dispatch down to the lethal-hit record.AbstractPlayerTickingSystemdeliberately does NOT: its read is a GUARD deciding whether the frame runs, two of its three subclasses never look at a player reference, and a base handing every implementation a parameter one of them uses would be the shape the sweep exists to remove - the one system that wants it reads it off the store and ref it is handed. The per-frame statistics accumulators take the caller'sUUIDand record their write throughmarkDirty(PlayerId, ...), so nothing on that path resolves the same component twice either. - A derived reference can be null, so every seam that reads one says what it does about that. An entity that is not a player (or one whose ref no longer resolves) leaves a notification unsent, a name blank, or the whole pass skipped, depending on what that seam was for. Nothing throws where it used to work, and nothing silently pays out to nobody.
- Two published contracts widen, and a consumer of either should know.
PlayerAbilityUsedEvent.playerRefis@Nullable: the event fires for an NPC or summon cast too, and an observer registered throughAbilityCastObserverRegistrymust null-check it rather than assume a player.player/PlayerHandle.playerRef()reads the component on demand instead of holding one captured at construction, which makes it WORLD-THREAD only: a handle built on the world thread and read from an async command body trips the store's own thread assertion, so resolve what you need before hopping off. - The public XP event did NOT widen, and that is the deliberate half.
MMOSkillTreeAPI'sPlayerGainXpEvent.getPlayerRef()stays@Nonnull, so no existing subscriber has to change; XP awarded on an entity that answers as nobody simply publishes no event, rather than handing a listener a null it was promised it would never see. Every other side effect of that award - the XP itself, the level-up, the milestone check, the progress fan-out - still runs. - Scope: this mod, the shared library, Kweebec Nightmare and RPG Stations. The MMO Mob Scaling companion is deliberately out of it - it calls none of the seams that moved and still compiles unchanged - so its own
(store, ref, playerRef)shapes are a separate pass rather than an oversight.
Technical: the content audit adopts the library's one finding vocabulary
validation/Finding,validation/Severity,validation/ValidationReportandvalidation/CommonFindingsare DELETED. Every validator in the mod now buildscom.ziggfreed.common.validation.Findingdirectly, so the record a library validator returns and the record an MMO validator returns are the same type and the four-method adapter between them has nothing left to convert. The factory signature is unchanged (Finding.error/warning/info(domain, code, message, sourceId)), so no call site moved an argument; the one field-name change issubjectIdreadingsourceId. The escalation that adapter also carried survives asValidationChecks.fold(domain, findings[, raiseToError]), which stamps a library finding under an MMO audit domain and still reports the codes inRAISE_TO_ERRORone tier higher than the library did.- The validation lines change shape. A per-finding boot-log line reads
instead ofvalidation ' ' [CODE]: [] :, and errors print at WARN with warnings and notes together at INFO, where before errors and warnings shared INFO and notes went to FINE. Because the library's line carries no severity word and both lower tiers share one sink, a warning and a note read alike in the log; the headline's separate counts are what tells you how many of each a domain reported. The summary headline reads1 error, 3 warnings, 0 notesinstead of1 error(s), 3 warning(s), 0 note(s)(andnothing to reportinstead ofno issues) - that headline is also what/mmoconfig validatechats to an admin, and the RPG Stations companion's/rpgstations validateline moved with it; the per-finding chat lines under it are unchanged, since both commands build those themselves. The finding a line describes and the headline COUNT are unchanged: the library counts errors plus warnings and excludes notes exactly as the deleted class did. ContentAudit.logDomain(domain, findings)is the one place that shape is decided, and every validator'srunAndLog()calls it.
Technical: the deprecated-engine-API sweep and the last enum-typed skill members
- The jar calls no plain-
@DeprecatedHytale API in the ability, asset, boost, config, interaction, item, util and xptoken packages:DamageCause.PHYSICALis gone (AbilityDamageUtil.resolveCauseresolves the Physical fallback through the asset map by id),Damage#getCause()reads move toutil/DamageUtil.causeOf(damage)(the asset map by cause index),RootInteraction.getRootInteractionOrUnknownleavesMmoSelectInteraction.walk(fork roots resolve through the asset map; a missing one is skipped as the execution path already did), and the fourCodec.BSON_DOCUMENTcaptures (AbilityModAsset.Modifiers,MmoGrantRewardInteraction.Reward, the twoInlinemodifier arrays) decode through the newasset/OpaqueDocumentCodec, an opaqueCodecimplemented directly against the non-deprecated codec contract with the engine's own mark / validate / cloneMark buffer capture (the engine'sBsonDocumentCodecand its BSON reader statics are all deprecated with no named replacement; pinned byOpaqueDocumentCodecTest). Theevent/package'sDamage#getCausereads and itsAllLegacyLivingEntityTypesQueryconverge the same way (see the moment-reaction section), andplayer/db/DbBlobCodecdrives the engine codecs with a freshExtraInfo(the public no-arg constructor the engine's own asset editor uses) rather than the deprecatedEmptyExtraInfo.EMPTY; the only plain-deprecated symbol the jar still touches is its ownSkillTypein the frozenMMOSkillTreeAPIsignatures.SkillConfig's five deprecated delegation shims (isCombatXpOnKillOnly/setCombatXpOnKillOnly/getXpMapByName/setXpByName/removeXpByName, zero callers) are deleted;XpMapsConfigandgetCombatXpModeare the reads. - Held-item and PlayerRef convergence:
CombatWeaponUtil,MmoDamageInteractionandGearAbilityModSourceread the active hotbar stack / hotbar section through ziggfreed-common'sPlayerAccess, and everyinteraction/fetch of thePlayerRefcomponent goes throughPlayerAccess.playerRef(store, ref);PermissionUtil.playerRefOfstays as the null-tolerant convenience over it. - The last internal
SkillType-typed members are deleted:BoostTarget.skill(SkillType)/applies(SkillType)/getSkill(),ActiveBoost.appliesTo(SkillType),XpTokenDefinition.flatXp(SkillType)/getSkill()(each had a*ByNametwin and no caller);SkillTypenow appears only indata/and the frozenMMOSkillTreeAPI. - Skills sweep:
.claude/skills/rewardsdescribes the landedRewardSpecs+RewardExecutor+PendingRewards/RewardGrants.grantAll/RewardJson/ShopEngineshape; config-system, recipes, db-readiness, localization, hytale-api and hytale-modding no longer nameMobDropsAsset/LuckLootAsset/luck-loot.json/mob-drops.json,QuestComponentStore/AchievementComponentStore,PrerequisiteGroup,DialogueBodyResolver,StepDescriptionRenderer/SummaryArgs, or advise@SuppressWarnings("deprecation")/player.getPlayerRef()/world.getPlayers()/getActiveHotbarItem().MmoRoleTemplateParameterTestskips its pack-variant check when everycontent-packs/mount is an empty uninitialised submodule (a linked worktree), not only when the directory is missing; the assertion is unchanged.
Technical: the two per-player progression components collapse onto the library's one record
QuestComponentandAchievementComponentstop being live state. Everything this mod reads or writes about a player's quests and achievements goes through ziggfreed-common'sZigProgressComponent- one record per player, holding both plus what conversations remember, attached at connect by the library's own hook and served by its ownZigQuestStore/ZigAchievementStore.quest/QuestComponentStoreandachievement/AchievementComponentStoreare DELETED: after the move the only thing either added over plain delegation was a database hook, and that is now two listeners on the library's own fan-out (player/ProgressPersistence). A consumer that brought its own store brought a second version of one player's state.- The two old components stay, registered and read-only. Their types are still registered so a world saved by an older build decodes, and
player/ProgressStateMigrationreads them ONCE per player - at their first connect after the update, claimed on the target record so it can never run twice - and copies every leaf across. The quest progress payload is copied verbatim rather than re-encoded, because it is the engine's own opaque packing. Nothing writes them afterwards, and a brand-new player never gets one at all. - The migration runs first, deliberately. It is wired into a
PlayerReadyEventlistener registered atEventPriority.FIRST, alongside the player-data hydrate and the commerce move, because the shared runtime runs its own maintenance (a quest self-heal, an achievement self-heal, the join-time auto-accept) on the same beat at normal priority - and that pass reads exactly the record the migration writes. Run it second and a returning player would be offered the tutorial they finished a year ago. - This mod's duplicate of that maintenance is gone. The library owns the stores now, so it runs the self-heal and the auto-accept for every consumer; the MMO's own
QuestService.selfHeal/autoAcceptOnReadycalls at player-ready are removed rather than run twice. What stays on this side is the migration and the legacy quest-feat backfill, which is this mod's alone. TheenableQuests/enableAchievementsswitches (and their bypass permissions) still hold on that beat: they reach the shared engines as the ONE composed system gateMmoSystemTogglescontributes, which the library'sQuestEngine.canAccept/autoAcceptAvailableandAchievementEngine.selfHealnow ask, so a server with quests off is not handed the tutorial at login and one with achievements off does not earn every level-shaped achievement at ready. - Both subject records (
MmoQuestSubject,MmoAchievementSubject) answer for the bare progress component, which is how the library's stores find a player's record through a consumer's own handle;content/gate/GateSubject's two component fields collapse into oneprogress, since a quest requirement and an achievement requirement are now answered off the same object. The MMO's own five-valueQuestStatevocabulary over the record lives as statics onQuestService, and theMAX_TRACKED/MAX_PINNEDcaps move ontoMmoQuestEngine/MmoAchievementEngine, where the runtime that enforces them is. - The six database blob-compatibility files pinning the two retired domains retire with them (no pre-1.6 database data exists anywhere); a new
ProgressBlobCompatTestplus a gatedProgressBlobFixtureGeneratorpin thePROGRESSdomain's wire format instead, which matters more because that format is the library's and reaches every consumer. The two pre-1.6 golden saves are KEPT, never regenerated, and are whatProgressStateMigrationTestdecodes to prove the move leaf for leaf.
Technical: quest and achievement engines lift to ziggfreed-common
quest/CommonQuestFoldandachievement/CommonAchievementFoldare the two fold seams that adapt a shared-schema asset into this mod's own definition model, registered fromMMOSkillTreePlugin.setup().config/defaults/QuestDefaultsandconfig/defaults/AchievementDefaultsare DELETED: every jar quest and achievement is an asset now, 281 achievements shipping as 167 files underServer/ZiggfreedCommon/Achievements/MMOSkillTree/(the per-skill levelling ladder collapsing from 126 definitions to 6 stencil files) and the jar'sServer/MMOSkillTree/Achievements/folder shipping EMPTY, its store still registered as the pack/owner compat surface exactly like quests. Ids are unchanged across the move, and both folds carryOWNER = "mmoskilltree", so an asset that omitsOwnerfolds into every reader.- Precedence is unchanged bottom to top: the shared-schema layer, then the
Server/MMOSkillTree/{Quests,Achievements}pack layer, thenmods/mmoskilltree/{quests,achievements}/.AchievementConfig.load()keeps the folded layer in a field andapplyPackLayer()recomposes it, so/mmoconfig reloadcannot drop the jar's own catalogue. - Achievement dispatch is synchronous on the world tick thread. The off-thread achievement worker, its progress/unlock/class caches, the 30-second flush, and the hop back to the world thread to pay rewards are all gone; the engine's inverted index narrows an event to just the criteria authored against that kind, so per-event work is a short indexed loop. A player's achievement state has ONE home, which is what let
AchievementComponentadopt the engine'sAchievementProgressStoreseam at all. - The persisted wire SHAPE is unchanged (string-keyed maps, the
UNLOCKED/CLAIMEDwords,MAX_PINNED = 5); the criterion progress KEY is the criterion's id ("), and the one-time migration re-keys a legacy save's positional# " "records (and the oldest saves' bare-id record, which lands on the first criterion) through the folded catalogue as it carries them - an id the catalogue no longer knows keeps its spelling verbatim.# " AchievementsBlobCompatTest,QuestsBlobCompatTest, andStatisticsBlobCompatTeststay green against stored fixtures. - Fidelity was proved empirically rather than asserted: the assets were generated from the live Java defaults, then the old Java catalogue and the new folded catalogue were dumped field for field and diffed - 0 missing, 0 field diffs over all 256 Java-defined achievements, covering every id, key, number, flag, reward list, and per-criterion
index:type:target:matchMode:qualifier:amount:textKeytuple. The emitter and both dump tests were deleted afterwards; the standing guard isCommonAchievementFoldTest's id and order pins. - The two per-skill stencils are the one genuinely new mechanism:
PerSkillstamps one achievement per ACTIVE skill (the levelling ladder, ids unchanged, spacedSKILL_SORT_STRIDE = 6sort positions apart so a skill's rungs read together) or stamps a single authored criterion out per active skill (the three all-skills server-first races). Both read the live roster, so a pack-added skill joins and a feature-gated skill stays out, instead of baking today's roster into a file. Scoring.CountsTowardTotalis read by the fold: an achievement states outright whether it scores, rather than that being derivable only from its feat-of-strength flag. A feat of strength is still forced to false, so atrueauthored on a feat does not override it.- A repeatable quest's whole
Repeatblock is evaluated by the shared engine for every consumer, so the fold maps it straight through rather than re-implementing any of it. The defaultCooldownFrom: Claimis what a quest parked completed-but-unclaimed at an NPC means for a player (the clock has not started yet);CooldownFrom: Completemeasures from the last objective ticking over. Bounties author"Repeat": {}and re-arm with their board's own rotation,BountyServicereading the engine's ownCompletionRecord.lastCompletionMsrather than keeping a clock beside it.Quest#repeat()is@Nullablethroughout: the group's presence is the flag, so there is no sentinel to confuse with an authored value. QuestValidatortreats an npc id that no placement DECLARES as UNVERIFIABLE rather than as an error, because convention ids (a placement with no authoredIdentity.NpcIdanswers to the role it names, and the roles a server has are not enumerable from content) are not enumerable; its giver findings stay lenient WARNINGs for that reason.REACH_LEVELdesugars ontoSTAT_THRESHOLDat the definition-to-ObjectiveDefseam, so both authored forms take the same runtime path andObjectiveTextRendererdeliberately resolves them to the same sentence.- One evaluator answers "what state is this quest in", and a completion prerequisite means a CLAIMED quest. The mod-side effective-state helper is deleted;
QuestService.effectiveState/cooldownRemainingMs/offerableInMsdelegate to the shared engine'sQuestLifecycle, which ANDs all three constraints a repeatable can be held back by - a rolling cooldown, a calendarResetallowance and a lifetimeMaxCompletionscap - so the quest log, the NPC offers, the accept gate, the turn-in readiness check, a conversation's Complete action and/mmoquestadmin statusread the same answer the engine acts on. A quest whose lifetime cap is spent reads Completed instead of Available, and a cooldown line held back by a calendar window reports the wait to the window boundary rather than to the elapsed clock. Alongside it, aQuestsprerequisite and theziggfreedcommon:quest_completedfactor both mean the storedCOMPLETED: a quest finished but not yet collected (itsClaimrewards still waiting) satisfies neither, both in the library and in this mod, where the two are one read off the saved progress record whenever the subject carries one.GateFactorsanswersziggfreedcommon:quest_completedoff that saved record, so a gate evaluated with no live entity (a rendered shop row, a validator) resolves instead of failing closed; andmmoskilltree:mastery_nodejoinsmmoskilltree:featureandmmoskilltree:classas a process-wide contribution, so a mastery-node requirement written in a storefront row, a shared-schema quest or a conversation condition is answered rather than silently locked.ContributedVocabularyTestdiscovers this mod's factor ids and fails the build on the next one that is registered locally without being contributed. ziggfreedcommon:quest_completionscounts COLLECTED runs, so a reward still parked is not a run already done under it or under the completion flag beside it. A quest authoredAutoClaim: falsewaits finished-but-uncollected until the player comes for the reward, and the count read that window as a run already done whilequest_completedread the opposite - so content written "come back once you have run this three times" opened on the third FINISH, ahead of the third payout. The sharedCompletionRecordgains a collected tally raised at claim time and the factor reads it. the progress record'sQuestCompletionsleaf carries it as a fourth comma-separated number (questId=). A save from a released build carries no completion record at all - the leaf itself is newer than any tagged version - so an upgrading player simply starts counting at their first finish after the upgrade: a quest they had already completed reads 0 on the count while still reading 1 on the completion flag, and collecting a reward they had parked before the upgrade pays out while the count stays at zero, since collected is clamped to a finish tally that has no memory of that run. No count, and no over-count. The three-number form is a development-tree courtesy for a save written earlier in this same development cycle: it reads back with collected equal to finished, since a finish WAS the payout under the rule it was written under, and the same clamp stops it counting its parked run twice when somebody finally collects it. A quest's lifetime, , , MaxCompletionscap and its calendar allowance keep counting FINISHES. No player can make the two readings disagree at a repeat decision: a parked quest is not offered, and the accept gate refuses it until it has been collected. A deliberate force can - a scripted or administrative accept that skips the eligibility check on purpose, or aresetsOnCompletere-arm that clears the parked status - and the finish is the safe half to cap on, since a run somebody walked away from without collecting still spent its slot.
Technical: interactive work stations (standalone-mod extraction, native-first design)
- The full diegetic station engine (
StationAsset/LootableAsset/RpgStationsSettingsAssetPattern A codecs, the per-player session state machine, camera/hold/seat-mount, tool gating, recipe derivation, the conditional-lootable + command-reward layer, validation, the summary panel) lives in the standalone RPG Stations mod, not the MMO jar; see its ownadditional-mods/rpg-stations/CLAUDE.mdfor engine internals (native-first throughout: tool gating and tool-power contribution scaling read the held item's realItemToolSpec, the rare-loot ladder rolls an ordinary nativeItemDropList, seat mode mounts the player via the nativeBlockMountAPI). - The MMO jar keeps only the progression slice, reached through a soft, two-way-optional extension surface (native events for observe-only moments, typed registries for request/response reads) in
integration/stations/: XP awards with the named-factor breakdown, aggregated luck (mmoskilltree:station_luck) and mastery bonuses as registeredFactorProviders,WORK_STATIONobjective/statistics progress, and the summary panel's per-skill XP rows via aSummaryEnricher. Neither mod hard-depends on the other. - The per-player unlocked-flair set is ziggfreed-common's
ZigFlairComponent, which the library attaches and persists, so a server running RPG Stations without the MMO still remembers what a player unlocked. The MMO peeks it through theplayers()seam asPlayerSession#station(), and theplayer/dbDB backend carries it as its ownSTATIONSdomain. - Station content splits across the two jars by ownership: RPG Stations' own jar ships the standalone, zero-progression Sawmill and fish-preparation (Cutting Board plus Cooking Fire) stations with their block items and RootInteractions, while the standalone content pack
skill-stations-packships the Anvil station asset, the Sawmill block item and RootInteraction, the sharpened-bar items, the loot tables and work emotes, and the MMO-bridge JSON (theExtensions/*Progression.jsonfiles that append the MMO's skill-XP contributions onto the jar-shipped stations). The pack depends on both RPG Stations and the MMO jar; stations grant no MMO progression without it, matching the Bounty Board pack precedent. - Phase 2: the anvil bridge.
integration/stations/StationEnhanceStamperregisters into RPG Stations'api.EnhanceStamperextension point, the item-format read/write half of the anvil's Enhance ritual (RPG Stations owns all the roll/cap math):inspect()reads the existing per-stackitem.ItemStatsMetaseam,apply()merges the rolled stat entries additively, refreshes the tooltip through the existingItemEnhanceDisplayseam (no name override, no tier/quality vocabulary, the same convention every other enhancement path already follows), and advances a newItemStatsMeta.StampCountleaf on the same metadata blob (a sibling of the existing stat entries, not a parallel store) so a future cap or display can read "how many times has this exact stack been stamped" without re-deriving it. Every other enhancement path (/mmoitemenhance, and so on) leaves the new leaf untouched. Smithing is a pack-authored custom skill (asset/type/CustomSkillAsset, shipped asServer/MMOSkillTree/CustomSkills/Smithing.jsonin the stations pack), removed fromSkillRegistry.BUILTIN_SKILL_NAMESand folded into the registry's pack layer withrequiresFeatures: ["stations"]keeping it dormant without the bridge (the Taming precedent). See the custom-skill technical section below for the asset type and the player-XP continuity path. - Round-7 D-5, XP-notification stacking. New
feedback/XpNotificationStackingis a stations-agnostic policy seam (onlyUUID+Predicate, mirroring the feature availability-provider precedent): an integration bridge registers a per-player "is a coalescing context live" predicate, and while any provider reports a player active,SkillService.notifyXpGainsends the stacking form (ziggfreed-common'sNotify.itemKeyed- the amount rides the item-slot quantity, the client sums it while the text freezes, keyed on a distinct per-skill item id derived from the skill'sIcons/ItemsGenerated/icon) instead of the non-stacking icon-path banner; a null item id (a custom skill with no generated icon) falls back to the non-stacking form..png integration/stations/StationSessionActivity(asessionId -> playerIdmap fed by the Started/Completed session events) registers the"stations"provider. Everything outside an active session is byte-identical. Invariant (verified empty in shipped content): no trained skill's icon item id may equal a station-producible item id, or the XP toast and item-grant toast would cross-stack. - Round-7 D-6, MMO-agnostic enhancement outcome.
StationEnhanceStamper.applyreturns RPG Stations' newStampResult(mutated stack plus oneEnhanceLineper rolled stat, each label built by the now-publicItemEnhanceDisplay.statLineMsg- the same colored tooltip stat line), so the anvil session summary renders the exact tooltip labels verbatim and the two can never drift; no MMO stat vocabulary crosses into the engine. - Schema-review wave (rulings 68-80). RPG Stations carries no progression vocabulary at all: a station declares generic
{Channel, Param, Amount}contributions and the engine forwards them without interpreting any of them. The bridge owns the MMO's half of that contract.MmoStationChannelsholds the ids (one channel,mmoskilltree:skill_xp, with the skill id in the opaqueParam- deliberately the same shape as the read-sideFactor/Paramconvention, never one channel per skill);RpgStationsBridgedeclares it into the engine's channel registry at install so it reaches the authoring dropdown and the engine's undeclared-channel warn.StationBridgeListeners.onCycleCompletedconsumes BOTH of the cycle event's contribution lists and filters each on that channel first, so another mod's contributions are ignored rather than read as skill ids:contributions()is per-cycle (tool-multiplied here, idle-prescaled by the engine),oneShotContributions()is one-shot find grants awarded verbatim with neither scaling, tallied into the summary's awarded totals with a zero base so the per-cycle factor math stays clean.StationBridgeValidatorwarns on ammoskilltree:channel this bridge does not declare, and on askill_xpParamthat is blank or names an unregistered skill. The luck double-count lint moves off the engine (where it hardcoded two MMO ids) onto a registered validation hook,StationLuckDoubleCountHook, which reports the same advice when a loot roll composes themmoskilltree:station_luckaggregate together with astat/MMO_Luck*factor. The manifest'sZiggfreed:RpgStationsoptional-dependency pin is">=0.1.0 <0.3.0", the range the compiled api contract (0.2.0) supports, per the api's additive-growth policy (apiVersion(), documented in RPG Stations'api/CLAUDE.md).
Technical: custom skills as a pack-authored asset type (Smithing relocation)
- Adds
asset/type/CustomSkillAsset, a Pattern A structured asset (Server/MMOSkillTree/CustomSkills/*.json, id = filename uppercased; nestedDisplay/Placementgroups plusTriggers/RequiresFeatures/MaxLevel) folded into a newSkillRegistryPACK layer byAssetMergeHandlers.onCustomSkillAssetsLoaded(precedence defaults < pack < owner; the ownercustom-skills.jsonstill wins per id).clear()clears only the owner layer soreloadFromConfig()preserves the pack, and the/mmoconfig reload+ config-preset-import sites moved offSkillRegistry.init(...)(which recreated the singleton with an empty pack map, dropping the pack skill until a restart) ontoreloadFromConfig(). The two custom-skill parsers plus the admin wizard share oneCustomSkill.of(...)factory (id-cased placeholder-name synthesis + defaults owned once).skill/CustomSkillContentValidatoraudits the roster (shadowing a built-in, unknown category/trigger, missing name lang) viaContentAudit//mmoconfig validate. - Removes SMITHING from
SkillRegistry.BUILTIN_SKILL_NAMES/BUILTIN_SKILL_DATA;LEGACY_REMOVED_BUILTIN_SKILL_NAMESdrivesSkillComponent's read-only legacy XP-continuity codec entries, so a pre-migration save keyedSMITHINGXp/SMITHINGFracXp/SMITHINGRewards/SMITHINGPending/SMITHINGRestedXpstill loads its XP and re-emits it under theCustom*aggregates on the next save (one-way, idempotent, no version gate; a rollback to the old jar still recovers the XP). Thedata/SkillTypeenum keeps its SMITHING constant, soMMOSkillTreeAPI.getSkillTypes()is unchanged (the COOKING/REPAIR/ALCHEMY name-only precedent). The jar drops the built-inskill.smithinglang key from all nine.langfiles; the pack suppliesskill.smithing/skill.smithing.descin its own additive layer. - Replaces the duplicated
XpMapsConfig.getXpMapByName(id).isEmpty()visibility guard (six sites) with oneSkillRegistry.isListableSkill(id)predicate + a hoistedlistableSkillIds(), so a service-fed or command-leveled skill with no trigger map is correctly listed and aggregated:pages/render/SkillFilterUtil,LeaderboardPage,AwardBoostPage, andSkillComponent.getTotalXp/getSummedTotalLevel/getHighestSkillLevel.
Technical: cast-engine common lift (Phase B mechanical move)
- Lifts the ability/effect substrate's reusable mechanics into
ziggfreed-common's newcastpackage ahead of the later step-dispatch kernel work (Phase B; a mechanical move, zero behavior change, 394 tests green at the time):OnHitEffectsandAbilityStateServicebecome thin policy facades over the newly-commonOnHitRegistry/ArmedStateStore(the 9 built-in on-hit builders, the slot vocabulary, and instance ownership all stay MMO-side);RaycastTargeting/BlockRaystepare deleted from the MMO and every consumer retargets onto their common equivalents;AbilityStatusEffectUtil's status-effect attach now delegates to common'sEntityEffectService.applyTimed(byte-for-bytePortal_Teleportattach semantics, the thread-local duration bonus kept MMO-side); the five world-ticking ability services (DOT, heal, ground-zone, projectile, beam) swap their bucket plumbing onto common'sWorldKeyedQueues, withAbilityDotTickingSystemnow extendingAbstractWorldFrameSystem; and five particle-spawn sites route through the new commonModelParticleService. This is the substrate the step-dispatch kernel lift (Phase G, below) later builds on.
Technical: ability system rework (step-engine asset flip, no-legacy dispatch)
- Flips the ability runtime from the legacy
ActiveAbilitiesConfigJSON blob toAbilityCatalog, a foldedServer/MMOSkillTree/Abilities/*.jsonasset store (defaults < pack <ability-overrides.jsonowner layer).ActiveAbilityServiceandPassiveAbilityTriggersdispatch exclusively through the step engine (AbilitySpec/StepRegistry/CastContext) against that catalog, with no legacy fallback path.ActiveAbilitiesConfig,AbilityDefinition, the 16ability/effects/*Ability.javaclasses, and the 1268-lineAbilityDescriptionRendererare deleted. - Mastery and skill-tree ability modifiers now fold onto typed
AbilitySpecleaves throughModifierTargetTable+SpecModifierFold, replacing the old untyped params-Map patch. The fold arithmetic (OVERRIDE -> ADD_PARAM -> FLAT -> PERCENT) is byte-identical; only the landing leaf is now a typed, codec-checked field. Every shipped modifierparamKey(jar built-ins and the skill-mastery-pack content) resolves through the table; an unresolved key logs loudly instead of silently dropping the modifier. - Replaces
abilities.jsonwith two files:ability-settings.json(the server-wide ability toggle and dodge / counterattack timing, moved verbatim off the old config) andability-overrides.json(per-ability tuning, decoded through the sameAbilityAssetcodec a pack uses, folded onto the current catalog entry via the nativeParent-style per-leaf inheritance machinery). LegacyAbilitiesFileMigrationruns once at plugin setup, before the asset pipeline exists: moves any legacymods/mmoskilltree/abilities.jsonto a timestamped file undermods/mmoskilltree/backup/, never reads it, and never overwrites a prior backup on a repeat restart.- Ability tooltips compose from
StepDescriptionRenderer, a per-step-kind fragment renderer, replacing the deleted monolithicAbilityDescriptionRenderer. - Adds the reader for the ability-level
Presentationgroup (launch sound / cast animation / camera / feedback):ActiveAbilityService's cast-moment player plays it once per successful cast. EachEffectStep's ownPresentation(and a step's named second moment, e.g.Projectile.Impact) is that step's own moment. - Ability description resolution moves from
ability.to.flavor ability.:.desc Identity.DescriptionKeyis the single resolver authority (every shipped ability JSON plus all 9 shipped languages migrated); the 10 already-deleted legacy abilities' orphaned flavor keys (challenging_shout,soul_drain, and others cut in earlier 1.6.0 phases) are removed rather than carried forward as.desc. EffectStep.Teleportgains anArriveleaf: a nestedPresentation(Sound/Particles) that fires at the blink's DESTINATION on a successful teleport, distinct from the step's own origin-timePresentation.Teleport.ofgains an overload threading it through the step-engine's Java-side spec builder, mirroringProjectile's ownimpactsub-Presentation.AbilityValidatorparticle checks move off a shape-only check onto the realParticleSystemasset map; camera checks are new againstCameraEffect's asset map;Presentation.Feedbackchecks againstFeedbackEvent.fromString(registry-independent, so it runs even in a unit JVM without the other three). NewPASSIVE_ABILITY_PRESENTATION(an ability-levelPresentationauthored on aPassiveTriggerability) andRESERVED_STEP_PRESENTATION(aCamera/Feedbackleaf authored on any step-level,Projectile.Impact, orTeleport.ArrivePresentation) warnings.Dot.EntityEffect/Dot.TickSound,Heal.EntityEffect,Dash.TrailEffect,Beam.VisualParticle/Beam.HitSound, andZone.ImpactParticle/TickParticlejoin the checked leaf set.- Ships 18 new
MMO_*assets (14 visual concepts; a few ship as a particlesystem+entityeffect pair), each composed from verified vanilla particle spawners:MMO_Burn,MMO_PyroReadyAura,MMO_Arcane_Pulse, theMMO_Dissolve_Voidtrio (particlesystem + the mod's first ModelVFX, a true-hide copy of vanilla'sPortal_Teleportwith noSwitchTo+ entityeffect),MMO_Void_Spark,MMO_AxeFury_Burst,MMO_LastBastionGlow, theMMO_ShieldCharge_Trailpair, theMMO_Phalanx_Aura/MMO_PhalanxAurapair,MMO_OneInchPunch_Flash,MMO_Ordnance_Blast,MMO_Dodge_Feathers,MMO_Timber_Kickoff, andMMO_ShotPrimed_Flash(a primed-shot cast flash forparalyzing_dart/arty_passive_sniper, which previously authored no arm-moment particle; it wraps the vanillaStar_Flash_GSspawner, a.particlespawnerthat is not directly referenceable as a particle system).MMO_Burnfills a live dangling reference too:flame_streamandmagic_capstone_pyrocasteralready named anEntityEffectcalledMMO_Burnin their shipped JSON with no such asset on disk, so both abilities applied nothing visible on their burn moments until this pass. dodge's castPresentation.Feedback = DODGEis the first live emitter of thatFeedbackEventvalue; per the ability-wide VFX capability map, dodge is the one ability permitted to authorFeedback(every other ability'sFeedbackleaf stays unset).- Adds a new
TAUNTEffectStepkind (Tauntgroup,Radius/DurationMs, dispatched byTauntStepHandler) that owns the aggro-pull cast notify Challenging Shout needs, the archetype's only home.AggroSuppressis reduced to a pure suppress group (timedAggroIntegration.suppresswhenDurationMsis present, else the one-shot Shadowstep-styledropAggro); its retiredMode="Taunt"discriminator had zero shipped JSON users. - Lifts the generic cast RUNTIME MECHANISMS into
ziggfreed-common'scast/cast.stepkernel, keeping the MMO's content in place. Common gains a step-dispatch engine (StepHandler/StepRegistry/CastKernel/StepSemantics, parameterized over the consumer's own step/context/result types), the per-hitHitResolverwith an evolvableHitContext/HitActioncarrier, and a guardedObserverRegistry; the MMO suppliesEffectStep/CastContext/AbilityResultas the type params and keeps the entire ability SCHEMA (AbilityAsset/EffectStep/Presentation), all 20 step handlers, and every gate/cost/notify/XP-attribution policy.ability.step.StepHandler/StepRegistry/AbilityStepEnginebecome thin shims (the byte-compatible staticexecuteentry + the missing-handlerseverelog stay MMO-side); a newMmoHitRequestsfactory supplies the resolver's MMO seams (damage sink, XP-origin decorator, per-hit sound), andAbilityCastObserverRegistrywrapsObserverRegistry. The liftedArmedStateStorenow carries XP-routing as an opaqueattributionpayload (the MMO'sArmAttributionrecord, folded back byAbilityStateService.foldPayload) so common holds zero MMO vocabulary. Dispatch, short-circuit, hit resolution, and observer fan-out are byte-identical (the schema and all handlers never move).
Technical: full-composition ability rework (Wave 2 - the Java step engine retires)
Supersedes the step-engine flip described in the previous section: the Java-side EffectStep
schema and AbilityStepEngine/StepHandler/StepRegistry dispatch retire in favor of composing
an ability's whole cast chain as one ordinary native Hytale RootInteraction chain, referenced by
a new AbilityAsset.Body field. Targeting and Effects are removed from the envelope; a 26-Type
Mmo interaction roster (MmoCastAbility, MmoDamage, MmoProjectile, MmoDot, and so on,
package interaction/) composes freely alongside native interaction nodes inside that chain, built
on a new generic chain-composition framework lifted into ziggfreed-common (ChainWalker, the
interaction/type/param/target toolkits). See CONTENT_PACKS.md's Ability authoring section
for the full Body/MmoNum/anchor authoring contract.
- Re-platforms
AbilityValidatoroff the deletedEffectStepwalk onto the sharedChainWalker:
every ability's Body is walked (cycle-guarded, capped at depth 64 / 512 nodes) and cross-checked
for resolvability, cycles, depth/node-count overrun, and a new decision-38 layering rule (a body
node may never wait on client-reported data - Charging/FirstClick/MovementCondition/a native
Selector belong only in the TRIGGER chain that fires an ability, never inside its Body).
Gains decision-34 MmoModSteps anchor checks (a body should declare Start/End anchors,
escalating a missing one to an ERROR the moment a shipped modifier ADD_STEP-targets that ability
at that anchor), decision-17 modifier-paramKey coverage (every authored paramKey must be
declared by some body's MmoNum.ParamKeys, or be one of the two ability-level or four
non-ability keys), decision-20 authoring guardrails (an MmoCastAbility node with every gate off,
or Progression.AwardXp on an unlock-placed ability), decision-28 (an MMO effect node whose
referenced EntityEffect has no status icon), and an INFO nudge toward MmoSelect over a native
Selector fork for any number-bearing MMO Type. Every per-node leaf read (Cause, SpawnerId,
Anchor, Gates, ...) goes through short, reflective, fail-soft helpers rather than importing the 26
Type classes directly, since they land across several parallel legs.
- Replaces
ability.step.StepDescriptionRendererwithability.ChainDescriptionRenderer
(byte-identical renderMsg/renderCardMsg entry points, so Messages/RewardPreviewBuilder/
AbilityBindPage change only their import): walks a Body via ChainWalker and composes one
fragment per visited node - an MMO Type keys off ability.summary. (with its single
MmoNum leaf folded, when the node carries exactly one) or .basic, a native node keys off
ability.summary.native., both falling back to a generic line
(ability.summary.generic / ability.summary.native.generic) rather than ever rendering a raw
key or nothing. SummaryHidden nodes are skipped. Deliberately does not reproduce the deleted
renderer's ~100 per-combination phrasing variants per Type in this pass (documented scope note on
the class); the two-key .basic/.value contract degrades gracefully and can grow richer keys
per Type incrementally without touching the renderer's mechanism.
SchoolCauseGenerator's setup()-time pre-scan switches from reading each ability's (now-deleted)
NativeChain.Cause leaf to resolving each ability's Body id and raw-walking the referenced jar
Server/Item/RootInteractions/*.json + Server/Item/Interactions/*.json files (no engine,
classpath/jar-safe, bounded depth/node-count cycle guard) for a native damage-shaped node
(DamageCalculator.BaseDamage) carrying an explicit Cause ancestor leaf - a new, additive raw
JSON authoring convention for a hand-authored native damage leaf that wants a generated per-ability
leaf cause (MMO_Cause_), documented on the class alongside the specific scan
assumption for maintainer/scout confirmation. An MMO damage Type no longer needs a generated leaf
at all - it carries a bare school Cause plus cast-frame attribution directly.
- Test suite re-platform: deletes 11
EffectStep/step-engine-shaped tests plus the superseded
RecursiveCodecSpikeTest and StepDescriptionRendererTest; adds AbilityAssetEnvelopeCodecTest
(Body decode under the unarmed clinit-safe forwarder + native Parent inheritance for the 7 kept
envelope groups), AbilityAssetNoEngineClinitTest (the guard keeping the whole unit suite alive -
touching AbilityAsset.CODEC must never force the engine's RootInteraction class-init),
TypeRegistrationTest (the 26-Type canonical roster, no alias ids, asserted over compile-time
TYPE_NAME constants only, never a Type's getCODEC()), ParamFoldArithmeticTest (the legacy
fold algorithm ported as a byte-identical oracle), ParamClampTableTest, BodyResolvabilityTest,
BodyLayeringRuleTest, ModStepsAnchorTest, CastRequestGateSkipTest, FailCastRefundTest,
ChainDescriptionRendererTest, BodyMigrationFidelityTest, and re-platforms
AbilityIdStabilityTest (91 ids unchanged, now also asserting a non-null Body) and the 81-tuple
mastery paramKey fixture as ModifierParamKeyCoverageTest. GateOrderTest is UNMODIFIED per the
cast-pipeline contract.
- Owner-facing break: an
Effectskey insideability-overrides.jsonis now a load-time error
naming the replacement field, Body.
Technical: gear-modifies-abilities, the guard/parry archetype, and four extra native seams (Wave 3)
- Adds the
interaction/Mmo*Interactionroster to 26 registered Types, all wired through a singleInteractionTypes.registerAllcall inMMOSkillTreePlugin.setup(); the canonical per-Type field/service/doctrine table is design authority (.claude/plans/1-6-0-ability-system-rework.md, the mmo-core-contract's Type Roster).MmoCastAbilityInteraction(MmoCastAbility) is the sole ability-cast dispatch node, resolving an ability id from exactly one ofAbility/Slot/Triggerand building aCastRequestfrom itsGates/Cooldown/Modifiers/Progression/Chainknob groups; it replaces the retiredMmoAbilityCastInteraction+MmoAbilityCastSlot1/2/3Interaction, so every existing consumable, block, and hotbar cast-item chain now casts through the one node. - Adds
Chain.GateOnlytoMmoCastAbility(decision 44a): a node authoredGateOnly:trueruns the full 11-gate ladder (unlock, cooldown, cost, class, conditions, and so on) and resolvesFailedon any refusal, but fires nothing and has zero side effects on a pass, letting a consumable or block chain gate its own consume/effect branch behind a real ability check before committing.CastRequest.gateOnly(...)forwards the caller's own gate flags through the SAME gate authoritytryActivatealways uses (no hand-mirrored subset), so a gear-granted cost discount or mastery cost reduction is honored by the gate exactly as it will be honored by the real cast. The shipped potion and scroll consumables use this to gate their own vanilla consume chain: a gate-only check runs before the item is spent, the real cast fires from the item's own effect slot after, so a cast interrupted mid-drink neither casts for free nor loses the item for nothing. - Adds the gear-modifies-abilities content surface (decision 19/23/25):
GearAbilityModSourcefolds a player's held/worn/offhand equip snapshot (a cached read offziggfreed-common'sEquipStatBridgetriggers) against a newItemAbilityGrantpack asset (Server/MMOSkillTree/ItemAbilityGrants/*.json, matching by exact item id or by the item's own native tag group, gated by an orthogonalApply.WhenHeld/WhenWornArmor/WhenOffhandknob set) plus a fresh per-stack read of the held item's ownitem.MmoAbilityModsMetametadata blob ({V, Mods:[{Mod, Scale}]}, the same per-stack shape item enhancement already stamps), feeding both intoCastRequest.extraModifiersalongside the existing mastery/rewardAbilityModifierService.ModifierProducers. Ships six jar-default gearAbilityMods and threeItemAbilityGrants as worked examples. - Adds six global
MMO_Ability*stat channels (AbilityChannels: Range/Radius/Duration/CostReduction/IFrames/Pierce), registered on the stat substrate at base 0 with aModifierClamps/*.jsonclamp per channel, read viagetStatMaxon the caster atresolve_modifierstime and folded into the per-castParamFoldTablethe same way a mastery orAbilityModbonus is.resolveCooldownReduction/resolveEffectDurationBonusunify onto the same channel table (single read site each, no surviving second copy of the fold arithmetic). The priorrangeparamKeyalias onMMO_AbilityRangeis dropped (distanceis the one canonical key); it was unclamped and could double-fold if a leaf declared both. - Adds native pre-stamped loot (decision 31): a rolled or hand-authored item stack can carry an
MmoAbilityModsMetablob directly in its ownMetadata, reachable through either of two native codec paths - aServer/Drops/*.jsonItemDropList'sItemDrop.Metadata(a mob/block drop table entry), or aModifyInventoryInteraction.ItemToAdd's fullItemStack.CODEC(any interaction chain - a quest action, a dialogue reward, anMmoModStepssplice). The lucky-find path ships live: mining aRock_Gem_Zephyrgem block can drop a Vortex Charm quiver pre-stamped with a gear ability mod, andServer/MMOSkillTree/BonusDrops/Mmo_Vortex_Charm.jsonis the worked example of the chance formula - aBasefloor plus weightedMMO_LuckandMMO_Luck_MININGterms inside aClamp, with a reader-facing$Commentexplaining each number and how to retune it. The reward-grant path ships as reference content only (MMO_Reward_VortexCharm_Grant), not yet wired to a live trigger. - Adds the Wielding guard/parry archetype (decision 29):
Guard_Stance/Parry_Riposte.jsonare envelope-onlyAbilityAssets (a body that applies a briefStamina_Broken_Immunewindow on cast) because decision 38 forbids aClient-waiting node inside a body, and nativeWielding/ChargingInteractionblock/parry is exactly that; the tight-window hold-to-block/parry/bash-fork mechanic itself lives in a standalone trigger-layer chain (Server/Item/RootInteractions/MMO_Guard_Wield.json/MMO_Parry_Wield.json) any weapon's ownSecondaryinteraction key can reference, mirroring vanilla's ownCommon_Guard_Wield.jsonshape; wiring a default weapon to it is a content-pack task, out of this leg's scope. - Adds native diminishing-returns CC (decision 29):
MmoStunInteraction(MmoStun) first checks the target's nativeImmuneEntityEffectand no-ops cleanly when present, then, by default, also writes a per-applicationImmunityRampAmount(default+25, foldable like anyMmoNum) onto the target's nativeImmunityEntityStat; that stat's ownMaxValueEffectsauto-appliesImmuneonce it saturates at 100, so four ramped stuns in quick succession earn the target a native CC-immune window with no custom bookkeeping. Live on Shield Bash, Shield Slam, Paralyzing Dart, and Berserker Charge. - Adds authoring metadata for a future native deployable ghost-preview (decision 29b, scope note):
MmoDeployable.Previewdocuments theMaxPlacementDistancea pack author would hand-wire onto a SIBLING nativeSpawnDeployableFromRaycastnode on an item's own trigger chain, because that native preview node isWaitForDataFrom.Clientand a body (Server/None only, decision 38) structurally cannot host it. No shipped ability authorsPreview; Sentry Turret and Flame Totem both spawn at the caster's facing with no ghost preview in this release. If a body ever reaches this Type withPreviewauthored (a mis-wired reference), it fails the cast cleanly with a guarded warning instead of silently spawning unpreviewed. - Adds per-gate blocked-cast feedback (decision 42): a cast refusal at one of the 8 gates that can itself produce one (
known,not_passive,unlocked,class_allowed,cooldown,primary_cost,secondary_costs,cast_conditions) resolves its ownability.cast.blocked.message viaActiveAbilityService.messageForBlockedGate, instead of one generic failure line; the two operator kill-switch gates (enabled/world_rules) stay a silent drop (no player-facing text, matching their un-authorable no-knob nature). - Folds the viewing player's own live gear/mastery modifiers into
ChainDescriptionRenderer.renderMsg(decision 41): builds the sameParamFoldTablesnapshot the cast pipeline'sresolve_modifiersgate builds and folds every renderedMmoNumthrough it, so an ability you've unlocked shows numbers matching what you would actually cast.renderCardMsg(a reward/shop preview of an ability you don't own) keeps folding againstParamFoldTable.empty(), the authored base value, since there is no caster context to fold against. Documented as a lower bound: it excludesGearAbilityModSource/AbilityChannelsreads that need a liveStore/Refthe render path doesn't have. - Deletes
HotbarAbilityPacketFilteroutright (decision 22, Phase M) in favor of dispatching every ability trigger through the sameMmoCastAbilitynode consumables and blocks use ({"Type":"MmoCastAbility","Trigger":"ABILITY_N[_CROUCH]"}); confirmed a pure dispatch swap with zero hotbar-packet involvement (the deleted class was aSyncInteractionChainssniffer, not a hotbar-packet handler).AbilityInteractionInjectoris unchanged. Invisible to players; no input/binding behavior changes.
Technical: full-composition consolidation and gate-ladder fix (Wave 3.5)
- Consolidates every default ability's
Bodyfrom a string ref to the deleted per-abilityMMO_Ability_*_N*.jsonfile onto a fully inline object (decision 43): all 97 jar-shipped ability assets now carry their wholeRootInteractionchain inline in the ability's own file, one file per ability, the load-bearing default going forward (a pack can still use a string ref for its own reuse). Body reuse across abilities rides sharedMMO_Frag_*fragment nodes (MMO_Frag_Anchor_Endat 95 users,MMO_Frag_Anchor_OnHitat 16),Replace/InteractionVarsslots, and nativeParent, not a keyed-step-merge mechanism (decision 30, considered and reversed by decision 33/37: the engine'sRootInteractioncodec cannot gain keyed merge, and a pre-parse rewrite of engine assets would be a forbidden second parser). - Fixes
MmoCastAbility'sGateOnlycheck running a hand-mirrored SUBSET of the gate ladder (9 of 11 gates, missingresolve_modifiersandsecondary_costs) instead of the one gate authority: the miss could both false-refuse a cast a gear cost-discount would have afforded, and false-pass a cast the real ladder would refuse after a consumable's item was already spent.ActiveAbilityService.tryActivatenow returns success immediately after the full gate loop whenCastRequest.gateOnly()is set, before any side effect runs, so GateOnly coverage is complete by construction rather than by a second, driftable implementation. - Fixes the ability-body consolidation fidelity test comparing
$Commentprose as if it were semantics (a comment edit alone could fail the build); the oracle now strips$Commentat any depth before comparing, on both sides.
Technical: native ability chains, deployable and summon steps, NPC casters
- Adds
StepType.NATIVE_CHAIN(NativeChain{Interaction, InteractionType}), anEffectStepthat fires an existing native Hytale RootInteraction chain by id instead of running an authored effect chain;NativeChainFireis extracted fromdodge's own dash-firing code anddodgeis refactored onto it byte-identically.AbilityValidatorwalks a chain's damage-bearing nodes: an unresolvable interaction id is an ERROR; a player-reachable damage node is a WARN, downgraded to INFO when the ability'sXpRoutingskill matches the wielded weapon's own combat skill (native damage now attributes XP correctly through the existing held-weapon fallback) or the ability is NPC-only. The chain-walk probe cannot see runtimeParallel/Replaceforks (documented; a live collector-walk is a follow-up). - Adds
DeployableStepHandler, backing Sentry Turret and Flame Totem: the structure spawns one world frame later through a newAbilityDeployableQueue(CastContextcarries noCommandBufferto spawn through directly), and aDeployableAttributionstore redirects the structure's kills and per-hit XP back to the summoning player.AbilityValidatorgains deployable checks: an unresolvedSpawnerIdis an ERROR, a still-defaultLiveDurationis a WARN. - Adds
StepType.SUMMON(the ability schema's 20th step handler), backing Summoned Wolf: spawns through the sameNPCPlugin.spawnEntitypath the hub NPC uses, stamps an engineDespawnComponent(30s) and a newMmoSummonComponentbefore the entity joins the world, and clamps the authoredCountto 1-5.DeployableAttribution.resolveOwnernow redirects a summoned entity's kills the same way; both consuming systems previously gated the redirect behind a deployable-only fast path that silently starved a summon's kill credit whenever no turret was also live, fixed by dropping that short-circuit. - Adds
NpcAbilityCastService.cast(Store, Ref, String): a cast entry point for a non-player mob that skips every player-only gate (unlock, cooldown, cost, HUD, observer) but still refuses a passive-only ability and still plays the ability's real cast presentation and effect chain. Surfaced as an additive method,MMOSkillTreeAPI.castNpcAbility(Store, Ref, String), on the FQN-frozenapi/surface. - Moves
CombatXpEventSystem's damage read from the damage-filter stage to the damage-inspect stage (the engine applies and roundsDamage.amountto a whole number in between), so damage-based combat XP now tracks the actual integer HP removed rather than the pre-round raw damage value; the difference is a sub-1-XP-per-hit rounding delta, not a balance change, and is documented on the class. - Native-chain damage a player deals directly (as opposed to a deployable's or a summon's) does not yet attribute combat XP: there is no reliable signal at the damage layer to tell a native chain-hit apart from an ordinary melee swing, and a naive one would misroute weapon XP after every
dodge. This half of the native-damage bridge is deliberately deferred; the wave ships real, correctly-attributed native damage only on its held-weapon-matched abilities (Bull Rush, Ground Slam, Tempest, Vortex Lunge), via the existing held-weapon XP fallback. - The new abilities' motion and native-chain shapes are cloned from vanilla Hytale weapon-family chains (Daggers' Pounce, Mace's Groundslam, Battleaxe's Whirlwind, Sword's Vortexstrike, Spear's charged throw, Bow's Pullshot and Boomshot, Wand's hitscan), each verified against the shared-source RootInteraction, with the native chain's
Charging/Signatureenergy-gate stripped since the ability schema owns its own cost and cooldown. - Per-ability masteries, ability improvements, and XP attribution now reach abilities whose damage runs through a native Hytale weapon-chain instead of an authored effect step. A native chain's damage hit previously always credited the held weapon's own skill (and never the casting ability's own mastery/reward bonuses); 11 of the abilities added this release (Bull Rush, Ground Slam, Tempest, Vortex Lunge, Explosive Shot, Hookshot Arrow, Arcane Lash, and others) now bridge correctly, crediting the declaring ability's own skills and letting its masteries and granted improvements actually apply.
Technical: stat substrate + damage schools (Phase I)
- Adds 24 authored
Server/Entity/Stats/MMO_*.jsonEntityStattypes (MMO_Defense,MMO_CritChance,MMO_CritMultiplier,MMO_Lifesteal+MMO_LifestealFlat,MMO_ComboDamage+MMO_ComboDamageFlat,MMO_FallDamageReduction,MMO_EffectDuration,MMO_CooldownReduction,MMO_Luck,MMO_BonusXp,MMO_Bonus_Fire/Ice/Lightning/Water/Arcane/Void, and the per-school FLAT channelsMMO_BonusFlat_Fire/Ice/Lightning/Water/Arcane/Voidjoining the flat-add step) plus runtime-registeredMMO_Damage_,MMO_Luck_, andMMO_BonusXp_channels for every active-roster skill (built-in andcustom-skills.json), all registered inStatSubstrateBootstrap.setup()beforeLoadAssetEventfires so an item pack can reference them by string at load time.StatModifierUtil.getStatMax(store, ref, statId[, default])is the single read chokepoint, folding a liveEntityStatType.getAssetMap().getIndexlookup instead of the old 3-stat switch. SkillTreeService.rebuildStatSubstrateandMasteryService.rebuildStatSubstratewrite one collapsed modifier key per writer per stat (mmoskilltree:tree/mmoskilltree:mastery), an idempotent full rebuild hooked at claim / reset / revoke / revalidate / PlayerReady / world-hop / disconnect-strip / and the full-reset interaction (the previously-blindMmoSkillAllResetInteractionbypass now rebuilds too). Each stat carries only the unconditional-global share of a claimed reward; per-skill, combat-target-scoped, ability-filtered, or HP-gated claims stay on the existing claimed-rewards cache, and every legacy getter now readsstatFold + conditional residualso old and new stay in parity (SkillTreeStatSubstrateParityTestasserts it across a 10-skill x 3-self-HP x 3-target-HP x 3-ability matrix).- Retires the 11 hand-authored
MMO_Cause_*DamageCauseJSONs in favor ofSchoolCauseGenerator: asetup()-time raw scan of jar ability JSONs plus theability-overrides.jsonowner overlay registers the sameMMO_Cause_leaf ids at runtime, before any asset decode (DamageCalculator.BaseDamagekeys bake to indices at decode time and are never re-resolved, so a cause must exist before then), leaving every existing chain JSON's authored id untouched.NativeChaingains a per-stepCauseleaf; the removed top-levelAbilityAsset.NativeDamageCausefield (1.6.0-internal, never publicly shipped) is gone. - Adds
Server/Entity/Damage/Arcane.json,Void.json,Lightning.json, andWater.json(Parent/Inheritsthe vanillaElementalcause,DamageTextColor#C77DFF/#7B2CBF/#F5D547/#2E6BE6respectively); vanillaFire/Icecauses are read, never overlaid. SCHOOL_RESISTeffects are runtime-registered instart()straight from the tree's reward declarations (ResistEffectRegistrar, aRawAssetin-memory buffer decoded throughloadBuffersWithKeys, no parallel on-disk JSON): one infinite nativeDamageResistanceEntityEffectper resist reward (MMO_Resist_), applied viaaddInfiniteEffectat claim and removed viaremoveEffect(COMPLETE)at unclaim/respec, reconciled against the desired set at PlayerReady, world-hop, and full reset, and re-registered on a config reload.- Adds a
damage_schoolContentAuditdomain (unknown school, an out-of-range resist value, a per-school aggregate resistance at or above 90% as a WARN, ancestor/child school overlap as a WARN, an unresolvable leaf cause as INFO) andAbilityValidatorchecks for per-step andNativeChainCauseresolvability plus the generated-leaf-cause bridge. - Adds
CanCritandArmorPenetrationtoDamagesteps (ArmorPenetrationreduces the MMO defense-reduction fold before the existing 90% cap; it cannot reach native armor) and aScalinggroup (PerStat,PerSkillLevel, snapshot-at-cast) toDamage/Dot/Zone/Beamsteps, plus a genericCHANGE_STATstep kind (StatModifierUtil.changeStatValueagainst caster or victim; a missing stat is a logged no-op). - Fixes the
wind_dancertoshadow_pouncechained-cast damage bonus never landing: thedamageMultipliermodifier fold now also reachesArmNextHit.Multiplier(previously onlyDamage.Amount/Zone.DamagePerTick/Dot.Amount), so the folded specArmedStateStorereads back at consume time actually carries the multiplier. - Mob ward affixes (native
DamageResistanceeffects keyed to the six schools, rolled on scaled mobs) ship in the standalone MMO Mob Scaling companion mod; see that mod's own changelog for detail.
Technical: native-first stat aggregation (equip-stat bridge, cooldown materialization, native tool-stat authoring)
- Installs
ziggfreed-common's newstats.EquipStatBridgeunder the"mmoskilltree"namespace as the stat-application seam for gear and enhancement: a held/armor stack's per-instance enhancement entries (now common'sStackStatsrecord, replacing the deletedItemStatsMeta) and a held tool's nativeItem.Utility.StatModifiersblock both fold into keyed nativeEntityStatMapmodifiers instead of an at-use MMO-side read, so the native stat map is the ONE aggregation authority;EquippedArmorStatsandHeldToolStatsare deleted. The bridge registers two ECS trigger systems (active-hotbar-slot switch, held/armor content change) plus a fullrecomputeAllhydrate on everyPlayerReadyEvent;CombatDamageEventSystem's DOT branch reads the bridge back and subtracts its own keyed contribution so gear and enhancement still contribute zero to DOT ticks, unchanged from before the migration.ItemEnhanceDisplay,ItemEnhanceRoll, andStationEnhanceStamperretarget onto the common record and the sharedToolStatDescriptionGenerator.ParsedEntryparsing authority. - Retargets
ToolStatDescriptionGeneratoroff the mod-ownedHeldItemStatsTagstring tag onto nativeItem.Utility.StatModifiers: a tool's stat line in its tooltip now reverse-resolves each modifier's stat index back to a stat id via the liveEntityStatTypeasset map and reuses the existing per-family color/label authority, so a pack authors tool stats as an ordinary nativeStatModifiersblock (Usable/Compatibleleft unset, the side-effect-free surface the engine never applies from a held item on its own) instead of a bespoke tag; runtime application still rides the equip-stat bridge. - Materializes cooldown reduction as a full 12th per-skill stat family (
MMO_CooldownReduction_, registered in the same dynamic skills-present pass as the other 11): the cooldown residual retires fromSkillTreeService/MasteryService, andresolveCooldownReductionnow reads the global plus per-skill native channels like every other stat family, restoring the per-skill cooldown gear/enhancement path the stat migration's first wave had briefly dropped. A per-skill cooldown reward now applies only in its own skill's context rather than the prior placement-less global fallback (no shipped default content authors a per-skill cooldown reward yet). - Retires every unconditional legacy
get*Residualstat family onto native modifiers, written at claim/reset/PlayerReady backfill under ammoskilltree:residualkey namespace; families with genuinely conditional at-use semantics a static native modifier can't express (HP-gated flat lifesteal, ability-gated residuals, class-passive families) keep a residual term reframed as the conditional-policy layer, the same way the existing 90% defense-reduction cap already lives in policy rather than the native map.
Technical: hardening pass (native-composition repairs, validator revival, authoring DX)
requiresClientwrapper regression fix. The decision-45d implicit-anchor wrapper broke all 8ApplyForceabilities (Skyward Leap, Backflip Disengage, Bull Rush, Dodge, and the rest):InteractionChain.requiresClientis computed once from a static scan that cannot see through aRunRootInteractionstring ref, so a client-executed body wrapped that way was misclassified server-only, never synced to the client, and timeout-cancelled. The wrapper is never applied to a client-waiting body, andAbilityValidator's decision-38 layering check is amended to allowlistApplyForceas client-executed-safe. Verified in game (Skyward Leap, Backflip Disengage, Whirlwind).- Projectile hit-path resurrection. Every physical-mode
MmoProjectileability (Fireball, Ice Bolt, the poison bolt, Arcane Missiles) had dead hit resolution for the whole full-composition arc: the hit queue only tracked spawns declaringdamage > 0whileMmoProjectileenqueues 0 by design, and inlineOnHitchildren were registered into the wrong interaction store. Fixed with an onHit-aware gate widening plus a synthesized bridging wrapper root; the affected abilities deal their hit damage again (verified in game). - Slam-family AOE ported to
MmoSelect. Ground Slam, Concussive Dive, Molten Slam, Rending Slam, and Crippling Slam ran their AOE damage through nativeSelectors buried insideGroundNext, invisible to the content validator's chain walk. All five are ported toSimple+MmoSelectwith byte-identical damage leaves (a supplemental blind-slot fix inziggfreed-common'sChainWalkerwas required for the walk to see them). Explosive Shot, Hookshot Arrow, and Dragon Arcana move off the nativeProjectilenode ontoMmoDamage, with a newKnockbackknob restoring the native push forces they lost. AbilityValidatorrevival. Three checks were silently dead and one was loudly wrong. The declared-paramKeyscan used duck-typed getters that never existed on the record-styleMmoNumtype, so its declared-key set was always empty and it false-ERRORed all 9 live modifier paramKeys on every boot; it is rewritten as a reflective field sweep. The armor-penetration range check, the free-cast guardrail (now default-aware), and decision-28's effect status-icon check are revived.ShopValidator'sUNLOCALIZED_DESCcheck (55 false WARNs on legitimately description-less shop entries) is replaced by a narrowerRAW_DESC_ONLY.ItemStatModifierValidator, a newitem_statsContentAuditdomain. Walks every item'sWeapon/Armor/UtilityStatModifiersand WARNs when aMultiplicativeentry targets a stat whose live base is 0, which always computes to a silent no-op under additive-first arithmetic. Covers both the vanilla base-0 channels (Mana, Ammo, SignatureEnergy, MagicCharges) and theMMO_*channels.BountyValidatorunresolvable-kill-target check. AKILL_ENTITYbounty objective whose target no live entity identifier can satisfy (a target id that is not a real NPC model id under the objective's own match mode) now WARNs asUNRESOLVABLE_KILL_TARGETat load, instead of shipping a contract that silently sticks at 0 progress forever (the class of authoring trap behind the retiredBounty_Hunt_Risencontract). A blank target still means "any mob", and the check degrades to permissive when the asset store is absent.- Ability store load ordering. The ability asset store declares
loadsAfterCameraEffect/SoundEvent/ItemPlayerAnimations, so camera-bearing generatedInteractionchildren validate against already-loaded stores instead of failing terminally at boot. - Authoring DX batch.
MmoNumdecodes a bare number directly as well as its object form (159 fold-less objects normalized across 73 files); single-userRunRootInteractionchains are inlined into their ability bodies (28 RootInteractions to 14, 113 Interactions to 63, 64 files deleted); a body with no authored anchors gets a boot-synthesized implicit wrapper; 24 internalMMO_Trigger_*files are inlined into their 6 externally-referenced roots and 4 orphaned pre-arc spike files deleted (expand-equality oracle verified, zero behavior change). Adds/mmoability explain(admin), which prints an ability's resolved body walk. - Native events on the engine bus. Adds
MmoAbilityCastEventandMmoAbilityResolvedEventas POJOIEvents dispatched throughHytaleServer.get().getEventBus()behindhasListener()guards, the native-events integration-bridge convention, so a third-party mod can react to a cast without a bespoke listener registry. MMO_*asset audit cleanup. Deletes 11 byte-identicalCameraEffectclones (14 references repointed at the vanilla ids) and 10 orphaned assets (elite-mob leftovers, an orphanMMO_ArcaneTrail/MMO_Stun, and the redundantMMO_Dissolve_Voidfamily members now thatPortal_Teleportis Vanish's canonical look), and converts several remaining full copies into nativeParentdeltas. Internal only; no player-facing behavior change intended.- Ability skill-tree claim-card flood fix. Suppresses exact-duplicate
Simpledescription lines, dedupes structurally identical adjacent lines, and caps a card at 6 lines plus an overflow indicator. (An earlier attempt this cycle changed javadoc only and did not affect rendering.) - First-boot smoke triage. Four rounds of live-boot asset-validation fixes: legacy-shape decode failures (an
AbilityOriginauthored as a boolean instead of a string,MmoNum-versus-int leaves, a missingMmoZone.AtVictimleaf, a stereo sound in a mono slot), retirement of duplicate asset-level tool tooltip stat lines in favour of the single native readout, and tooltip labels added for four native stat channels vanilla itself never labels. Also fixes a doubled%%in the crit-multiplier tooltip label across all 9 locales.
Technical: damage-pipeline simplification (unified hit fold)
- Damage joins the entity-stat substrate the same way the ten scalar families before it did: a hit's damage now folds from ONE deterministic read at resolution time (
(base + gearFlat) * (1 + pctSum) + progressionFlat + conditionalResiduals) over the nativeMMO_Damage/MMO_DamageFlat(new global channels),MMO_Damage_/MMO_DamageFlat_,MMO_Damage_Ability_/MMO_DamageFlat_Ability_, andMMO_Bonus_/MMO_BonusFlat_stat channels, instead of the prior four-addend walk acrossSkillTreeService/MasteryService/gear/effects. Runtime ordering is unchanged; a flat contribution folds pre-percent or post-percent based on its modifier key (gear/native/effect keys pre-percent, tree/residual/mastery/class progression keys post-percent), and attribution for tooltips and combat text derives from the same modifier keys (gear, skill, mastery, class terms stay distinct, never silently merged). - Adds a hit-side percent clamp asset (
Server/MMOSkillTree/ModifierClamps/HitDamage.json,MaxTotalPercent30.0) alongside the existing cast-sideDamage.jsonclamp, whose cap rises from 20.0 to 30.0 to match the wider modifier surface now folding through it. - Mastery damage nodes are channel-scoped end to end, and every targeting field an author can write now selects WHICH channel a node mirrors onto rather than adding a second application:
combatTarget/targetSkillpick the per-skill channel, and a node scoped to one ability (an ability mastery track, or atargetAbilityon the node) rides a per-ability damage channel registered at boot from the mastery catalog, so a per-ability bonus no longer widens to every hit of that ability's skill. A conditional mastery damage node (an HP-window bonus and similar) stays a hit-time conditional residual, since that kind of condition cannot be expressed as a static stat modifier. One ordering consequence: an ability-scoped mastery FLAT bonus now applies with the other progression flats after the percent multiply, rather than inside the cast's own base, so it no longer compounds with percent bonuses. - DOT ticks now receive a stack's per-instance gear enhancement by default, the same as any other hit; a DOT step gains a nullable
GearEnhancementknob (default true) so an individual DOT can still opt out of enhancement contribution when authored intent calls for it. - Zone ticks (ground-zone abilities) and beam ticks keep the full deterministic damage fold (channels, clamp, residuals) but no longer re-enter the stochastic combat layer per tick: no crit roll, no armed-slot or mark consume, no combo advance, no lifesteal, and no on-hit passive triggers fire from a field tick, matching how a DOT tick already behaves. XP attribution from zone/beam ticks is unchanged.
- Class passives now mirror onto the native damage channels uniformly across every reward family, the same mirror-then-fold pattern the tree and mastery writers already use.
Technical: NPC placement + world identity (the engine leaves the jar, the MMO keeps policy)
- The whole placement engine lives in
ziggfreed-common(com.ziggfreed.common.npc.placement, 23 classes) plus one world-identity authority (com.ziggfreed.common.world); the MMO keeps only what an MMO destination MEANS. Fourteen MMO classes are deleted:config/SpawnHubConfig,npc/MmoNpcPlacementStore,asset/type/QuestGiverAsset,npc/QuestGiverDefinition,config/QuestGiverConfig,npc/QuestGiverSpawnService,npc/QuestGiverRoleGenerator,npc/QuestGiverValidator,npc/StructureMarkerSightings,npc/MmoNpcSpawnService,event/StructureGiverSpawnSystem,event/ZoneDiscoverySpawnSystem, andnpc/ActionOpenMmoUiwith its builder. New MMO-side: thenpc_placementsfeature declaration (a row incontent/gate/MmoFeatures.declare()) plus the jar's two base roles,npc/MmoPlacementValidator,config/LegacyNpcPlacementMigration,asset/type/LegacyQuestGiverAsset. Rewritten:command/MmoNpcCommand,world/WorldRulesMatcher,asset/type/WorldRulesAsset,quest/QuestWaypointService,quest/QuestValidator, the feature-availability surface (todaycontent/gate/MmoFeatures). - A world is named by what it is CALLED or by the gameplay config it runs, and by nothing in between.
WorldSelector.match(World)returns aMatchRankrather than a boolean, so two rules pointing at one world are always ordered and there is no priority knob to set. The ladder is today'sWorldRulesMatcherladder verbatim plus one new top tier:GameplayConfigexact > exact world name > longest literal core across prefix / suffix / contains (tie-break prefix > suffix > contains) > bare*. AWherecarries the patterns themselves rather than a reference into a second file, so what a rule targets is readable on its own page. WorldIdentity.invalidateAll()is mandatory, not hygiene.AddWorldEventfor the main world fires during universe boot, BEFORELoadedAssetsEventfolds the selector config, so without invalidation on the merge path that world caches an empty name set for the life of the process andNames: ["default"]matches nothing at all. It is wired on the config's merge methods and in the registrar.- Two authorities, and this is the correctness core:
PlacedNpcComponentdecides DESPAWN,NpcPlacementLedgerdecides PLACE. Placement requiresledgerMiss && anchorChunkLoadedand NEVER places from absence alone: chunk unload REMOVES the entity from the store, so an entity sweep cannot tell "never placed" apart from "placed, chunk asleep", and placing on absence would spawn a second NPC every time a player walked back into range.NpcPlacementReconciler.sweepfollowsPerformerReconciler's pureKEEP/DESPAWN/REBINDpolicy shape, heals an older-build NPC missing the component by re-stamping rather than duplicating, guards the same-tick double-entry case with an in-flight claim set, and debounces per world.KeepAlivechunk pins are refcount-balanced (pin on first insert into a chunk's set, unpin on last removal, whole-world entry dropped by aWorldEvictorsevictor), becauseaddKeepLoadedis reference-counted with no automatic unpin. Interact.Bindingsis a channel-keyed MAP throughInheritMapCodec, not an array: an array leaf underappendInheritedis a single leaf, so aParentchild authoringBindingsat all would drop every entry it inherited - which would have broken this change's own flagshipParentexample, the temple guide that overrides one binding and inherits the rest.- Three extension points keep the engine open with no ziggfreed-common edit:
NpcPlacementBindings(claim a namespace AND supply its press-F handler in one call),PlacementFactorRegistry(Requiresfactors), andAnchorResolverRegistry(Anchor.Customproviders). All fail closed and all are validator findings. The fourth axis, who stands there, needs no registry at all:Identity.Rolenames an ordinary NPC role file, which any pack may ship. Common registers its OWNAddWorldEvent/RemoveWorldEventlisteners, andWorldEvictors.onWorldRemovedgains an already-evicted self-guard, because with two consumer mods installed the fan-out previously fired twice per world. - "Does this placement answer to id X" has exactly ONE authority, ziggfreed-common's
NpcIdentities.answerSetOfand the helpers over it; every read site routes through them rather than comparing a single id, which is what keeps alias behaviour identical acrossQuestService's offer/turn-in/deliver checks, the dialogue conditions,NpcQuestPage's quest LIST (a boolean/list mismatch at an aliased placement would be a visible in-game bug, so a parity test asserts they agree), thegiverturn-in sentinel,NpcTalkObjectiveUtil, andQuestWaypointService. ATALK_TO_NPCat an aliased placement fires the PRIMARY id through the normalProgressEvents.fireand each alias throughfireObjectivesOnly, so one talk credits both primary- and alias-scoped objectives with no statistic double-count. - The feature vocabulary gains the canonical id
npc_placements(the olderspawn_hub/questgiversfamilies are aliases of it), on when the shared placement store holds any content - a declared row incontent/gate/MmoFeaturesreading ziggfreed-common'sNpcPlacementConfigdirectly, the same way the economy's stores are read. MmoUiRouter.STATIC_TARGETS/PREFIXES/isOpenableare exposed so a content validator checks an authoredui_targetagainst the REAL routing vocabulary instead of keeping its own copy that drifts. A colon-less token is deliberately never flagged (it is a bounty board id resolved at runtime).- Dialogue state is engine-owned:
DialogueOnceandDialogueMemoryare nullable-leaf group codecs (a future axis is a new leaf, never a mode;"Once": trueis normalized by the sugar pre-pass so the codec only ever sees an object), andDialogueStateKeys+DialogueFlagScopeown key composition internally. AResetWithQuestPREFIXESq:onto the whole key while a world scope wraps only the FINAL segment (: q:) - that ordering is load-bearing, because the MMO clears a quest's state by a leading:mem:d: :w: : q:prefix match and a prepended scope would escape the clear and soft-lock the dialogue permanently. A world-scope pattern the current world does not match makes a write a no-op and a read unset; a pattern matching no loaded world warns once per pattern, since a typo would otherwise re-show a first-visit beat forever.: - The factor core is one shared implementation, not three:
com.ziggfreed.common.registry.RegistryLedger(owner attribution, identity-compared warn-once overwrite, failure count and last message,info()snapshot) generalizes the placement ledger, andcom.ziggfreed.common.factorholdsFactorContext/FactorProvider/FactorRegistryplus the ONEFactorConditioncodec and its array evaluator. The codec is a FACTORY, not a single static, so each consumer names its own Asset Editor dropdown dataset (an unserved dropdown renders as an EMPTY pick list, which is worse for an author than a plain text field). The placementPlacementConditiontype and RPG Stations' byte-identicalasset.Conditionare both deleted in favor of it; authored JSON on both surfaces is unchanged.
Technical: consolidation pass (one vocabulary per concept)
- World configuration is ONE vocabulary.
mmo-mob-scaling's flatWorldSettings.Matchstring, its copied pattern parser, and its second selection engine are gone: a world rule authors the sharedWheregroup overWorldSelector.CODECand selects byMatchRank, so a validator reasoning about a pattern and the runtime matching it can never disagree.WorldNameMatcheris now purely the pattern grammar (it parses and scores, it never SELECTS); its resolve/entry half and the parity test guarding two ladders retire with it. An unauthoredWhereon a placement means the world nameddefault, which is what a stock Hytale server calls the ordinary world players log into. ziggfreed-common'sLootTabletype is deleted and its content lives asLootables: a table's guaranteed payouts areRolls, its weighted pool is thePoolgroup, its additiveTableIdunion isContributesTo, and its native drop list is aGrants.DropListsentry. An instance preset'sRewardTableIdnames aLootableid, with the leaf name kept so no authored preset changes. The MMO's three Kweebec chase XP contributions move toServer/ZiggfreedCommon/Lootables/asContributesTocontributors, paying out unchanged.- One resolution engine answers both bonus-drop moments.
BonusDropsConfigfolds the asset layer, overlays the owner layer, and keys the effective table by (moment, pattern), so two rows claiming one pattern in one moment are one answer and the later layer wins it;LuckUtilandMobKillEventSystemare the two firing reads over it. The per-moment defaults live on theKindenum constants themselves (Kind.unauthoredChance), so the two firing sites cannot drift, and the name grammar plus its specificity ladder areziggfreed-common's sharedmatch/NamePatternandNameMatchRank- the same primitive aWherescores on - rather than a second matcher.mob-drop-commands.jsonstays a SEPARATE opinion consulted first for kills, so an owner who only wanted to add a command still gets the shipped table's drops beside it, and disabling a pattern there still silences both. - The 97 jar ability assets are filed by skill (
Server/MMOSkillTree/Abilities/, 18 folders). The store scans recursively and an id is the plain filename, so every ability id is unchanged and the folders are organizational only; a pack overrides by id from any folder, or none./ .json - There is ONE dialogue engine per server, and this mod registers into it.
MmoDialogueEngine.engine()no longer builds a private engine: it contributesReward/CompleteQuest/RunCommand/Gate/HasActiveBounties/ the adoptedHasOfferableQueststhroughDialogueEngine.registerShared, installs the two genuinely singular seams (the quest runtime and the factor vocabulary, first-install-wins with a named clash warning), and returnsDialogueEngine.shared(). Previously the SCHEMA was process-wide while the BEHAVIOUR was per mod, so on a server running two dialogue mods whichever one opened a conversation decided which half of the vocabulary could actually run: the other mod's actions silently no-opped and its gated lines silently hid. Alongside it, this mod registers howMmoDialoguePayloadis built for any player, so aGate, a quest beat or a force-complete answers correctly even in a conversation another mod opened - where before a missing payload read as gates locked, quests never started and a force-complete doing nothing - andNpcDialogueDepsRegistry.setDefaultnames its owner, so two mods taking the un-keyed default print one boot line instead of swapping silently. That payload is built at most once per conversation screen, so the on-render quest self-heal it carries runs exactly as often as it did before. AchievementCategoryAssetandAchievementMilestoneAssetmove toziggfreed-commonas common Pattern A types with common recursive stores, because both are generic achievement vocabulary: milestones feed an engine mechanism common already owns, and categories define the display taxonomy for the sharedListing.Categoryleaf. The MMO-side asset types, their registrar rows and merge handlers are deleted;achievement/CommonAchievementTaxonomyFoldis the one seam applying this mod's own policy (its queue-if-offline reward default) over a shared file. Common's own defaults publish the folded ladder, so a bare server with no MMO jar gets categories and milestone payouts in the Objective Book.
- Two single-call wrapper classes fold into the seams they wrapped (internal cleanup, no behavior change):
npc/MmoPlacementBindingsis deleted - thenpc_placementsavailability it reported through the provider seam is a directly-declaredfeature(...)row incontent/gate/MmoFeatures(ziggfreed-common is a hard dependency, so its placement store is read like the economy's stores; the provider seam stays for the genuinely soft RpgStationsstationsfeature), and themmoskilltreenamespace constant merges ontoMmoFeatures.NAMESPACE- andui/hud/MmoHud(a member-less alias) is deleted, withAbilityCooldownHudextending ziggfreed-common'sKeyedCustomHuddirectly.
Technical: logging facade converges on ziggfreed-common's shared guard
util.SafeLogis now a thin static wrapper over onecom.ziggfreed.common.util.GuardedLoggerinstance (this mod's ownMMOSkillTreePlugin.LOGGER, no prefix), instead of hand-rolling its owntry/catch (Throwable)guard per level. Every call site is unchanged; the printf-styleObject...overloads stay this mod's own on top of the shared guard, andfinersits in the shared guard beside the other levels.
Bug Fixes
- Fixed: a skill-scoped mastery damage bonus applied twice per ability cast (once through the cast-time modifier fold, once again through the swing-time mastery bonus walk); it now applies exactly once. This is a correctness fix, not a rebalance: builds that relied on the doubled bonus will read a lower effective damage number.
- Fixed: Meteor's mastery damage nodes never actually increased its damage (the damage modifier only reached direct-damage steps, and Meteor deals zone damage per tick); the modifier now also reaches zone and damage-over-time amounts, which also lets skill-wide damage masteries reach Meteor and Soul Drain.
- Fixed: Piercing Shot's pierce mastery node granted no extra targets (it only injected a pierce value when the ability had none, and Piercing Shot always had one); it now adds to the base pierce.
- Fixed: Rending Slam's bleed effect was cosmetic only (the visual played but dealt no damage); it now deals real damage over 4 ticks, attributed to and gaining XP for the casting player like any other hit.
- Fixed: Hookshot Arrow's pull yanked a target a fixed distance regardless of how far away they actually were, sometimes overshooting past the caster or barely moving a close target. The pull now scales to the live distance between shooter and target at the moment it lands, closing to a consistent range instead of a fixed offset.
- Fixed: hovering an ability-unlock node on the skill tree repeated the same one-line summary already shown on the card instead of showing the ability's real details; the tooltip now renders the ability's full body. A granted ability improvement's skill-tree node also now names the ability it targets (plus its effect, for a plain damage/cooldown-style bonus) instead of a generic label.
- An authored ability-modifier
ParamKeyis canonicalised to the vocabulary spelling when the modifier is built, so a mis-cased key folds instead of silently doing nothing; aFLAT/PERCENT/SETmodifier naming noParamKeyis an audit ERROR and is dropped rather than crashing the mastery page. The mastery node card localises thearmMultiplier/armFlatBonusnouns (all nine languages), and theMMO_AbilityDamageRadiusgear channel carries an item-tooltip label in every language (the six other channels join the eight non-Englishclient.langfiles with it). - Fixed: an explicit
/mmotheme defaultdid not survive a server restart when a theme pack (e.g. the Molten pack) was installed; the active-theme selection got clobbered by an unrelated registry-parsing step the next time the pack loaded, and re-reading the saved choice trusted that already-clobbered state instead of re-asserting it. Restoring the saved theme (or the default) now always re-resolves and re-applies it explicitly. - Fixed: disabling the quest tracker HUD did not survive a server restart; the HUD was re-installed unconditionally when a player joined, ignoring the stored toggle, so it re-appeared until an admin re-toggled it. The join-time install now honors the stored setting, the same way the ability cooldown HUD already did.
- Fixed: skill cards could show the orange "Claim Items!" button when the rewards page had nothing claimable to render. The card indicator counted every unclaimed reward entry while the rewards page only renders displayable ones; one shared displayability predicate now backs the card indicator, the rewards page, and the claim notification, so they can never disagree again.
- Fixed: a claimed reward tier for a lower- or mixed-case custom skill could keep its collect label (and stale claim count) after the items were already granted: the claim wrote under one id casing and the page read under another. Reward claim ids are now canonicalized in one place regardless of caller casing.
- Fixed: a shop entry whose category has no localization key rendered the literal uppercase key (e.g.
MMOSKILLTREE.UI.SHOP.CATEGORY.COMMON) in the shop UI; it now falls back to the raw category name. - Fixed: a force-completed quest paid its rewards but ran none of its completion hooks. The three paths that force-complete (a dialogue "skip" option, finishing the onboarding page, and
/mmoquestadmin complete) granted the rewards and stopped there, so an achievement fan-out, aCOMPLETE_QUESTobjective on another quest, and the bounty tail all silently missed a payout that ordinary completion would have credited. All three now run the same completion path as any other payout: rewards paid means hooks run, exactly once. Auto-claim and manual claim are unchanged, and a quest parked unclaimed at an NPC still fires only its parked signal. - Fixed: a quest's authored
displayNamewas ignored, with every quest surface showing the first objective's sentence as the title instead; the authored name now slots into the documented precedence between the convention lang key and the objective inference. - Fixed: a dialogue "hand it in" option gated on
ReadyToTurnIncould be offered while the player did not actually hold the required turn-in items, then silently do nothing; the condition now also checks possession of the full remaining stack. The NPC quest page's listing keeps its item-blind read on purpose, since partial delivery is supported there. - Fixed: a daily bounty completed yesterday could still read "Completed" after the midnight rollover re-selected the same contract. The completion instant is now stamped when the bounty's objectives complete (not when the reward is claimed), so the rollover re-arm correctly recognizes it as a prior-period completion, including one never claimed.
- Fixed:
/mmobountyui,/mmoshopui, and/mmoquestuirun from console (or an NPC command) reported "Player not found" for an online player standing in any world other than the one the resolver happened to pick; all three now resolve the target across every world, the same way the other admin commands do. - Fixed: five commands checked a permission node named after a player-facing alias (
mmoskilltree.command.xp,.setxp,.quest,.leaderboard,.boost) while the engine grants the node named after the registered command (.mmoxp,.setmmoxp,.mmoquests,.mmoleaderboard,.mmoboosts), so a granular grant on the advertised node did nothing. The in-body checks now match the registered names, and CURSEFORGE.md gains a Permission Nodes reference table listing every node. - Fixed: crafting a batch (e.g. 10x in one craft action) recorded the quest, achievement, and statistic progress of a single craft, because the engine fires one
CraftRecipeEvent.Postper action rather than per unit; objective progress now scales by the real crafted quantity. Crafting XP deliberately does NOT scale with the batch and stays per craft action, so bulk crafting is a UI convenience rather than an XP multiplier. - Fixes the legacy achievement dialect counting a feat of strength's points toward totals (a legacy
featOfStrengthforcescountsTowardTotalfalse again, as it always did; the shared schema'sScoring.CountsTowardTotalstays the one switch there), carries the shared schema'sListing.RequirePrerequisitesthrough the MMO fold, keeps a pre-1.3 save's composite progress record ahead of its bare-id twin in the one-time migration, and makes/mmoconfig achievement progressreport the key it wrote and refuse an index no criterion sits at. - Fixed: returning from an instance world (e.g. the Forgotten Temple Gateway) could teleport the server's placed Adventurer's Guide hub NPC or spawn a duplicate: the once-per-world auto-spawn marker was not armed by a hand-placed guide and not reconciled against the placement registry on re-entry. Hand-placing a guide now arms the marker, re-entry reconciles against the registry instead of blind-spawning, and a tracked guide that has genuinely gone missing is re-placed once with a log line. MMO-placed service NPCs (the guide, trader, board attendants) also now carry a very large health pool so stray damage sources cannot quietly kill them.
- Fixed: the tutorial gather quest's stone objective never counted mined stone (its target id matched no real block; mining stone yields the
Rock_*family), and its boar-hunt sibling silently rejected baby boars. The stone target now matches the rock family, and boar objectives count juveniles while still excluding tamed animals. - Fixed: switching one shipped quest off by authoring
{"id": "...", "enabled": false}in an owner quest file silently did nothing. The stub carries no objectives, so it was rejected by the empty-objectives guard as malformed, skipped with a warn line, andQuestConfig.applyPackLayerthen filled the same id straight back in from the pack layer, leaving the quest enabled (and, for the jar tutorial chain, still auto-accepting). Disabling a quest now needs only its id plus the flag: an explicitly disabled quest is exempt from that guard, since a disabled quest has nothing to do by definition. An objective-less quest that is still enabled is rejected exactly as before, so a typo cannot slip through as a live no-op.CONTENT_PACKS.mddocuments the off-switch shape. - The jar manifest now declares a real minimum
ZiggfreedCommonversion instead of a wildcard, so booting against a stale companion jar fails with the engine's own message naming the dependency and required range instead of a later bareNoClassDefFoundError. - Removed: the default action requirements shipped a set of
Log_*hatchet-requirement entries that matched no real block id (wood blocks are theWood_family), so chopping wood never actually required a hatchet. The dead entries are deleted, codifying the live behavior; a server that wants an axe gate can author_ Wood_*entries inaction-requirements.json. - Fixed: "New
rewards available!" repeated on every single XP gain, burying the screen in notifications. A tier where you had taken some but not all of your picks (a "Pick 3 of 7" tier with one chosen) could never be recorded as pending, so the check that suppresses repeat reminders never saw it and re-announced the tier on every XP award. Partially-claimed tiers now record correctly and announce once, and the reminder is additionally capped at one per minute per player as a burst guard. Suppressed reminders cost nothing: the tiers stay pending and the skill tree page still lists them. - Fixed: skill tree tiers described the same thing two different ways, labelling some "Pick 3 of 7" and others "Choice - Pick 1". Every tier offering a choice now reads in the one "Pick N of M" form, showing how many options it actually has. The tier badge is also rendered from one shared helper, so the header can no longer disagree with itself after a claim (the post-claim refresh previously counted hidden rewards the page never drew).
- Fixes
SkillComponent.clone()silently dropping a player's currencies, mastery purchase and contribution ledgers, permanent stat sacrifices, and class state, so any engine path that cloned a player component reset exactly those fields while leaving every other one intact. The nested ledgers are copied level by level so a clone shares no inner map with the original, and a reflective coverage test fails the build if a persisted field ever missesclone()again. - Fixed: the Boosts, Quests, Achievements and Abilities screens drew their content hard against the menu's outer border while the Overview screen sat comfortably inset, so those pages read as cramped and their side panels looked welded to the frame edge. Every one of them now uses the same inset as Overview.
- Fixed: the Mastery entry was missing from the left-hand menu on the achievements overview and on an achievement's detail view, so getting to Mastery from either meant backing out to another screen first. It now sits between Abilities and Settings on both, and hides itself when the mastery feature is turned off, exactly as on every other menu screen.
- Fixed: a custom skill created with the in-game wizard rendered its name as the literal
mmoskilltree.skill.on every surface after a server restart (community report; broken since 1.4.0). The server gated the client-resolved name emission on its in-code English fallback map, which synthesizes askill.entry for every registered skill at boot, so it emitted a message id the client's catalog never had; a fresh skill only looked right until restart because the fallback snapshot predates it. A newLocalizationConfig.hasClientKeyprobe reads the real.langcatalog only and now gates that emission (Messages.skillNameMsg, plus the custom-skill validator's name-lang check): a skill with no client-resolvable key renders its authored display name directly, so affected servers self-heal on update with no migration. Built-in and pack-authored skills, whose keys genuinely ship in a.lang, are unchanged. - Fixed: editing an existing custom skill through the admin wizard disconnected the player at the reward steps (community report; broken since 1.4.0). Both wizard reward rows sent a rich
Messageto a Label's String-only.Textsink, which the client rejects with a kick; both render through.TextSpansnow. Creating a skill never tripped it because a new skill reaches those steps with no tiers to render. - Fixed: the custom skill wizard's page titles, step headers, and Next/Finish buttons rendered raw
ui.skillwizard.*keys: the nine keys were never present in any language file. They now ship in all 9 languages, and a new reflective guard test (MsgTrKeyCoverageTest) checks every string-literalMsg.trkey in the source against the shippeden-US/mmoskilltree.lang, so a dangling display key fails the build instead of shipping. - Fixed: quests read as "Ready to hand in" at every character in the game. A quest with no hand-in step of its own asked whether its next objective pointed at the character in front of you, and it asked using the rules for matching a fired event, where a target left blank means "anything counts". So a step like "break any block" claimed to be waiting on every guide, trader and board attendant standing anywhere. Only a step whose kind genuinely names a place (talking to somebody, reaching a location) can mark a character now, its target has to name one, and the two are compared in full rather than by fragment. The same reading backed a conversation's "I have returned" option and a board's ready marker, so all three stop pointing at quests that are still in progress. A quest that really is waiting on you to hand something over is unaffected.
- Fixed: a conversation could offer to take a quest in for a step that has nothing to hand over. A "go and speak to them" step resolves at a character without anything changing hands, so the offer appeared and then did nothing visible; the hand-in offer is now made only where there is genuinely something to deliver, and reaching the character is credited by the conversation itself.
- Fixed: the quest screen at a character showed the character's internal id (for example
Mmo_Hub) as its heading instead of their name. The screen reads the name straight off the character's own placement now, which is the same one shown above their head and at the top of a conversation, so the three cannot disagree. - Fixed: a quest you had already accepted vanished from a character's "Quests here" tab and could only be found under "My quests", which made a giver look like they had nothing to do with a quest they handed you. A character's tab now lists every quest authored to them whatever state it is in: available, locked, in progress, waiting to be handed in, and finished ones in a Completed section at the bottom.
- Fixed: item icons were missing beside quest rewards, shop offers, and board listings, leaving an empty gap where the picture should be. The icon slots were being filled but had no size or backing frame to draw into. All of them now share one icon style, so an icon added anywhere in future is styled the same way by default.
- Fixes repeatable bounties never re-offering after the board rotated, and the quest log listing board bounties as a permanent "Available" you could not accept. A completed bounty's stored quest state stuck at COMPLETED indefinitely (bounties are excluded from the generic cooldown self-heal), so the Bounty Board showed "Completed" with no Accept button even after the rotation redrew that bounty, while the quest log's cooldown-aware read surfaced the same bounty as a misleading "Available" row (a bounty can only be taken at the board). Bounties are now pool/period-based: a completed one reads "Completed" only while it is still part of the current rotation period, then re-offers as re-acceptable once the board rotates (prior-period completions are re-armed when the board opens). The quest log now lists a bounty only while it is in progress or ready to claim, never a not-started or completed one.
- Fixes the Bounty Board never showing a bounty's skill / level / prerequisite / permission requirements: a bounty gated by one of those simply lost its Accept button with no explanation, and could even read "Available". The board now renders a "Required" section listing every unmet requirement through the same shared gate-reason renderer the NPC quest page already uses, and shows "Locked" instead of "Available" when a gate blocks acceptance. A quest's flat top-level permission gate is now explained on both the Bounty Board and the NPC quest page (previously only prerequisite-group permissions were).
- Hardened: the one-time progression move no longer spends its once-per-player claim against a catalogue that cannot answer for every record. A legacy achievement record whose id nothing loaded owns (its pack not yet reinstalled when the player first connects after the update) is carried under its legacy key untouched, ONE warning names the ids and the fix (install the pack that ships them), and the claim is withheld so the move retries at the player's next ready until it can finish; everything the catalogue does own moves normally on every attempt, so no achievement progress can be stranded by upgrading the jar before its packs.
- Fixed: with
schemaAutoCreateoff, the database backend skipped both boot guards (the schema-version check and the orphaned pre-1.6.0 row refusal lived inside the DDL path it was told not to run), so a DBA-managed database could boot against a newer schema or against orphaned legacy rows unguarded. A verify-only pass now runs both checks without issuing any DDL, tolerating a missing metadata table (a first boot before the DBA has created the schema is theirs to finish) while still refusing what would strand player data. - Fixed: the World Rules admin page kept a legacy rule's retired flat
Matchstring on save, writing back a rule that matches no world; a save now writes the pattern asWhere.Match, the shape the loader reads. A rule authored BOTH ways keeps its flat key verbatim beside the authoredWhere, so the boot report still names that ambiguity for its author to settle instead of the page silently picking a side.
Technical: Update 6 correctness set
- Every hold-then-consume/cast chain authors
OnItemChangeBehaviorexplicitly. The potion drink hold, the scroll windup (the chain's default beat and the fireball scroll's own override beat), the three ability-cast-slot charge holds and the six charged-trigger holds all author"OnItemChangeBehavior": "Cancel": the engine re-checks the live hotbar per node tick against the chain-start capture, andCancelaborts the whole chain on a mid-hold swap, so the-1consume or the released charge can never resolve against whichever stack is held after the swap (a dupe/free-cast vector if a node ever authoredIgnore, or if the engine default moved offCancel). A newitem_change_guardcontent validator (startup audit +/mmoconfig validate) raw-scans the jar's ownServer/ItemJSON and ERRORs on a hold node with aModifyInventory/MmoCastAbilitydownstream and no authored knob;ItemChangeGuardValidatorTest's shipped-content sweep makes the same rule a build gate. No refund wiring is needed: every authored hold runs before its cast commits, and a committed cast's body is queued as its own chain, untouched by a consumable-chain abort. - Locale-hardened ID handling, family-wide. Every locale-less
toUpperCase()/toLowerCase()on an id/enum/key path (381 sites across this jar's main and test sources, plus the sibling mods') now folds withLocale.ROOT, closing the Turkish-locale corruption class: on atr-TRdefault JVM,"MINING".toLowerCase()yields a dotless-i id, and one such site (AbilityModGrants) persisted corrupted ids intoSkillComponent. EveryString.formatin the family passesLocale.ROOTtoo - player-facing number text (crit multipliers and heal/block floats in combat text, HUD cooldowns, page counters and token labels), boost labels, admin command output, the fingerprint hex and the logging facade's format seam - so a crit readsx1.5on every server locale instead ofx1,5; the timestamp formatters behind config backups, owner-file retirement and migration reports pinLocale.ROOTas well.RepoHygieneTestgains rules 3 and 4: a locale-less case fold or a locale-lessString.formatanywhere in the family (theapimodule included) fails the build unless its line (or the line above) carries// LOCALE-OK:. - The generated-lang zip unregisters on shutdown.
MMOSkillTreePlugin.shutdown()unregisters the runtime tooltip-label pack through ziggfreed-common's newGeneratedLangPack.unregisterZipPack, closing the zipFileSystemthe engine holds open ongenerated-lang.zip. Plugin unloading cascades through dependents on Update 6, and a stranded handle made the next boot's rebuild fail on Windows with anAccessDeniedException. - Dangling camera ids are loud. An unresolvable
Presentation.Cameraid on a jar-shipped ability is an audit ERROR now (pack/owner content stays a warning: their content is not this jar's to hard-fail), and a newfeedbackvalidator auditsfeedback.json's configuredcameraEffectids against the engine'sCameraEffectregistry at boot. The shake service deliberately no-ops on an unknown id - correct at cast time - so a typo was previously felt only as a silently missing shake, reported nowhere. - Tooltip markup learns
. The tooltip flatten converts the engine's structured strikethrough flag into a literalpair (cleared off the rebuilt node like the other style flags), and the base-colour transform tracksthrough its close-and-reopen exactly like/, so a strikethrough run crossing a colour-span boundary can no longer corrupt the emitted spans.LangFileIntegrityTestalso asserts balancedpairs across all nine languages. - The
.langline-shape guard matches the real parser. The engine skips a malformed line (missing=, empty key, empty value) with a per-line warning and keeps the rest of the file, and it keeps the FIRST definition of a duplicated key; an unterminated quote gets no diagnostic at all and ships the literal quote char.LangResources.parseLinenow mirrors that handling exactly (malformed lines drop, first duplicate wins), pinned by test, so the early-boot fallback can never disagree with what a client resolves.LangFileIntegrityTestfails the build on an empty value or an unterminated quote, and the localization hook flags malformed lines per line per locale, so the practical rule stands enforced: a key that silently never ships renders as its raw id on screen. CommentHygieneTestreads an array-form$Commentas one text (a phrase split across two elements no longer escapes it) and flags the before/after phrasings the earlier verb lists missed (raised N -> M,was a native,no longer fires/deals/..., anyused to,prior authored,dropped entirely,as first shipped,than before,is now a,per the authoring brief,rework,a later port/pass).
Technical: Update 6 compat
- The whole mod family compiles against the Hytale Update 6 pre-release server (0.6.0-pre.13): this jar,
ziggfreed-common, every sibling mod, and every content pack; each manifest'sServerVersionreads>=0.6.0-pre.13 <0.7.0. The in-repo shared-source submodule rides thepre-releasebranch for the cycle and returns toreleasewhen Update 6 ships. - The four engine-API migrations the update forced, no behavior change intended: block reads and writes route through the new
BlockOperationssurface, NPCRolecomponents are fetched per call instead of held across calls, projectile spawns author the new 5-argspawnProjectile, and UI pushes invalidate throughinvalidatePacketCache. Flags.IsUsableis dropped from every shipped block JSON. The pre-releaseBlockFlagscodec no longer declares the key, so a leftover flag decodes silently with one 'Unused key(s)' boot warning per asset;Interactions.Usealone drives a block's usability on 0.6.- A seventh migration family,
blocks, reports the retired flag from both tools./mmomigrateand the web Migration Converter each claim pack block/item JSON underServer/Item/whoseFlagsobject still carriesIsUsableand report it as work for hands with ONE mirrored fix sentence (MigrationClassifier.ISUSABLE_REPLACEMENThere,families/blocks.tsthere, each side pinning the literal character for character); the family is report-only, neither tool rewrites native item JSON, and two shared corpus cases (blocks/bounty-board-retired-isusable,blocks/sawmill-retired-isusable) pin it from both suites. - Docs: the Localization page's empty-value warning is rewritten to match the real
.langparser (a malformed line drops with a per-line warning and the rest of the file survives); the Troubleshooting/plugin reloadwarning covers Update 6's cascading plugin unloads; Getting Started and Add-ons state the Update 6 (0.6.x) server requirement; and a new Server Operations page documents world-thread crash recovery (CrashRecovery), the database backend's boot-time down policy and 10-second shutdown drain, and what a hard process crash can discard.
Technical: the migration tooling catches up with the break wave
- An eighth migration family,
mastery, covers the structured-codec break from both tools. ONE family for mastery tracks AND class definitions, because they broke identically:/mmomigratereports an ownermastery.json/classes.jsonstill carrying old-format bodies as boot-handled (the probe mirrorsMasteryConfig/ClassesConfig's schema-v3/v2 retirement exactly), reports their timestampedbackup/residue as the only surviving copy of the owner's tracks, and reports a pack'sMasteryTemplates//ClassTemplates/file or old-grammarMasteries//Classes/body as work for hands; the web Migration Converter draws every one of those lines identically and additionally CONVERTS the old owner files into the schema-v3/v2 per-leaf overlays (the extends/params tracks it cannot resolve are left out, named, and preserved under_unconverted/; the legacy modifier spellings land canonical,statSacrificelifts onto the node, and the report states the one semantic shift, replace-wholesale to merge-per-leaf). Four mirrored sentences pin both sides character for character (MASTERY_V3_REPLACEMENT,CLASSES_V2_REPLACEMENT,MASTERY_STRUCTURED_REPLACEMENT,CLASSES_STRUCTURED_REPLACEMENTinMigrationClassifier, repeated byfamilies/mastery.ts), and five corpus cases join the shared corpus, three of them verbatim mastery-pack history from the 2.0.0 conversion commit9189fe48(mastery/pack-masterytemplates-combat6,pack-fireball-payload-grammar,pack-axes-extends-template) beside two synthetic owner pairs (mastery/synthetic/owner-mastery-v3-overlay,owner-classes-v2-overlay). - The world-rules conversion converts BOTH breaks in one pass, on both sides.
WorldRulesConverter(and the webownerFoldstwin) now moves the six flat 1.5.x gameplay knobs into the nestedRulesgroup in the same run that rewrites the retired flatMatchintoWhere, on the owner file and on pack rules alike; a knob authored flat AND nested on one rule is refused for its author to settle, exactly the way a both-waysMatch/Whererule already is, and a pack rule still authoring flat knobs reports at honored-with-warning strength rather than as broken. The jar's ownbc4fde21rewrite of its shipped Default rule is the new real-history corpus pair (owner-folds/pack-worldrules-nested-rules), and the existing v1.5.1 pair now pins the whole two-break conversion in one file. A world rule that names its worlds both ways AND also names a retired selector is classified as the both-ways ambiguity on every path (the jar's converter and pack rule, the web owner file and pack rule), so the two routes never print two instructions for one file. - The retired-control-key strip covers the mastery break. The
Masteries,MasteryTemplates,ClassesandClassTemplatespack-control keys (retired when both types became keyed structured assets) joinLuckLoot/MobDrops/Dialoguesin the one list both tools read: the web converter strips each with a per-key replacement sentence,/mmomigratenames the whole set on a control file still carrying any of them. - The web Migration Converter emits the shared progression schema the 1.6.0 codecs read. A converted quest or achievement carries
Criteriaas a map keyed by criterion id (a legacy list is keyed by each entry's position written out,0,1, ..., the key every player's saved progress is already filed under, so nothing is re-keyed), oneRewardsgroup split intoAutoandClaimwithClaimthe default (a legacyautoClaimRewards: trueselectsAuto; an achievement's two legacy lists are the two buckets), noFlow.AutoClaimleaf, andHidden/RequirePrerequisitesonListing. A shared-store file already speaking the retired shapes is refreshed the same way. Both tools name those five shapes as what dates a shared-store file (MigrationClassifier.RETIRED_PROGRESSION_SHAPES/hasPre160Leaf) and print one mirroredPROGRESSION_SHAPES_REPLACEMENTsentence, pinned from both suites. - Both migration tools catch an NPC placement still spelling its gate
Requires.Conditions(the codec key isFactors; the old spelling leaves the gate empty, so the placement stood unconditionally). The web converter renames the key in place;/mmomigratereports it at fails-open strength with the mirroredPLACEMENT_CONDITIONS_REPLACEMENTsentence. - Technical:
DbSchemaManagerTestcovers the schema-2 orphan-row refusal and the v1 to v2 upgrade on H2;RepoHygieneTestpinsREQUIRED_ZIGGFREED_COMMONtogradle.properties;CommonAchievementFoldTestpins the criterion KEYS progress is filed under; the DB session guards a nullZigFlairComponent.TYPElikeZigProgressComponent; the custom-skill audit reads the raw owner file so a reserved-delimiter refusal is reported. - Eleven at-HEAD corpus fixtures refresh to the shape the exporters emit today. Every progression
after/file whose own PROVENANCE declares it as the repo file at HEAD (pluspack-worldrules-flat-match's) is re-captured, picking up the keyedCriteriamap, the bucketedRewards.Auto/Claimgroup and the later authored passes; each PROVENANCE documents the refresh under its declared at-HEAD contract, and the tests assert the AFTER files directly - the pre-wave spellings do not parse against the current codecs (an arrayCriteriaor a flatRewardslist throws at load;ClaimRewards,Flow.AutoClaimandVisibilityare unknown keys the codec skips), so the converter emits the shapes the codec reads and no SHA-pinned fixture is touched.
v1.5.1
Features
- Adds a per-world stat-bonus switch to the per-world rules (
StatBonusesEnabled, default on): a world can strip a player's persistent stat modifiers - skill-treeSTAT_HEALTH/STAMINA/MANArewards, mastery stat sacrifices, and class passives - on entry and restore them on exit, so a lobby / arena / minigame instance runs on vanilla stat pools without touching saved progress. Distinct from the existingCombatBonusesEnabled(damage-time math only); the two are independent knobs.
Balance
- Raises the default player power-level tuning by 1.5x (
PowerLevel/Default.json: every pillar factor scaled, the pillar mix and per-mode weights unchanged), so a build reads higher on the 1-200 scale the mob-scaling companion consumes as difficulty. A single maxed weapon tree (e.g. Swords L98) moves from ~45 to ~68, a mid build from ~87 to ~130; a full completionist still caps at 200. Per-leaf owner overrides inpower-level.jsonkeep their authored factor values.
Bug Fixes
- Fixes the level-100 damage-over-time capstones (
pole_capstone_hemorrhage,sword_capstone_riftblade,unarmed_capstone_endurance_strike) stacking bleeds without limit and lagging the server in raids. Two compounding causes: (1) a DOT tick re-entered the combat pipeline sourced from the applying player, so each tick firedon_hit_dealtand re-procced the very bleed capstone that applied it - a self-feeding loop that spawned DOTs geometrically; and (2) the DOT service had no per-target dedup, so even without the loop a fast weapon (or a single multi-instance strike likepower_strike+ enchantments firingon_hit_dealtseveral times per swing) piled up concurrent entries. Fixed both: DOT ticks no longer proc the attacker-side combat layer at all (no crit, armed-buff / mark consume, combo, lifesteal, oron_hit_dealt/on_finisherpassive triggers) unless the ability opts in withdotAllowProcs: true; and re-applying a DOT now builds a capped, additive stack (ONE entry per(target, source-player, ability), per-tick damagedotDamage * stacksup to a per-abilitydotMaxStacks) instead of an unbounded pile-up. Authored caps: 3 on the higher-damage capstones (Riftblade / Hemorrhage / Frostbite / Pyrocaster), 4 on the lower (Endurance Strike / Multishot); bothdotMaxStacksanddotAllowProcsare server-owner overridable. - Fixes per-world rules never matching an instanced world. Instance worlds spawn with BOTH a leading
instance-prefix and a random suffix (e.g.instance-KweebecNightmare_Chase_Dread-), which a trailing-onlyFoo_*prefix pattern could not catch, so the rule silently fell through to the unrestricted default and abilities / combat bonuses / full XP leaked into the world. The bundledKweebecNightmare.jsonrule was dead for exactly this reason; it now applies (abilities off, combat + stat bonuses off, XP limited to its allow-list).
Technical: DOT proc suppression + capped additive stacking
- DOT ticks no longer proc the MMO combat layer. Each tick is tagged with a new
ability.DotDamageSource(aDamage.EntitySourcesubclass carrying anOriginrecord: applying ability id + itsxpSkills+ anallowProcsflag) so the metadata travels with theDamageacross the CommandBuffer queue into the filter-damage handler - a thread-local (likeAbilityDamageOrigin) can't, since the tick is queued and applied later.CombatDamageEventSystem.handlereadsDotDamageSource.isSuppressed(source)and, when true, skips the entire attacker block (weapon-req gate,applyAttackerEffects' crit / armed-consume / mark-consume / combo / on-hit + on-finisher passive triggers, andapplyLifesteal); the tick still deals its flat damage and the victim still gets defense reduction. Suppression is the DEFAULT (Origin.SUPPRESSED); an ability opts back into the full pipeline withdotAllowProcs: true. TheOriginis carried on every tick (even when procs are allowed) so future XP / kill / feedback attribution can read the source ability + skills without a second lookup. AbilityDotService.applygains a(@Nullable String stackKey, int maxStacks, DotDamageSource.Origin origin)overload (the 8-arg legacy form delegates withnullstackKey,DEFAULT_MAX_STACKS, andOrigin.SUPPRESSED- a DOT never procs itself by default). WhenstackKeyis non-null, a live entry on the same target from the same source player under the same key gains ONE stack (bounded bymaxStacks) and refreshes its duration instead of appending a secondDotEntry. Per-tick damage is additive:DotEntrycarriesperStackDamage/stacks/maxStacksand effectivedotDamage = perStackDamage * stacks, so a re-applied DOT ramps toperStackDamage * maxStacksand no further. On refreshticksRemainingresets tototalTicks, per-stack damage/interval/cap adopt the newest values, andnextTickAtMsis left untouched so the tick cadence (and thus sustained-fire damage) is preserved; at the cap the entry still refreshes duration but the stack count holds. apply() andtickFrameOnceboth run on the target world's thread, so mutating a live entry can't race the ticking pass;stackExistingscans the target's world bucket (bounded by the dedup, so the O(N) scan stays cheap) and skips an invalid (about-to-be-retired) entry so a reapply lands a fresh DOT rather than one the next tick drops.- Both knobs are data-driven per ability:
dotMaxStacks(AbilityDotService.DEFAULT_MAX_STACKS = 5fallback) anddotAllowProcs(default false).ApplyDotVictimEffectreads both (added to itsParamSpec) and builds theOriginfromdef.getId()+def.getXpSkills();RaycastDotAbilitydoes the same;OnHitEffects.buildDotreads them from the on-hit map and keys by DOT shape (onhit:) with an ability-less: : Originsince the ability id isn't threaded into on-hit builders.ActiveAbilitiesConfig.applyDotVictimgains adotMaxStacksoverload; the six DOT capstones author explicit caps (3 / 4). (the 8-arg form delegates withnull= legacy per-application entries,DEFAULT_MAX_STACKS). WhenstackKeyis non-null, a live entry on the same target from the same source player under the same key gains ONE stack (bounded bymaxStacks) and refreshes its duration instead of appending a secondDotEntry. Per-tick damage is additive:DotEntrycarriesperStackDamage/stacks/maxStacksand effectivedotDamage = perStackDamage * stacks, so a re-applied DOT ramps toperStackDamage * maxStacksand no further. On refreshticksRemainingresets tototalTicks, per-stack damage/interval/cap adopt the newest values, andnextTickAtMsis left untouched so the tick cadence (and thus sustained-fire damage) is preserved; at the cap the entry still refreshes duration but the stack count holds. apply() andtickFrameOnceboth run on the target world's thread, so mutating a live entry can't race the ticking pass;stackExistingscans the target's world bucket (bounded by the dedup, so the O(N) scan stays cheap) and skips an invalid (about-to-be-retired) entry so a reapply lands a fresh DOT rather than one the next tick drops. - The cap is data-driven per ability via a
dotMaxStacksparam (AbilityDotService.DEFAULT_MAX_STACKS = 5fallback).ApplyDotVictimEffectreads it (added to itsParamSpec) andRaycastDotAbilityreads it from the def, both keying bydef.getId();OnHitEffects.buildDotreadsdotMaxStacksfrom the on-hit map and keys by DOT shape (onhit:) since the ability id isn't threaded into on-hit builders.: : ActiveAbilitiesConfig.applyDotVictimgains adotMaxStacksoverload; the six DOT capstones author explicit caps (3 / 4).
Technical: per-world stat gate + wildcard matching
WorldRulesgains a fifth orthogonal knobstatBonusesEnabled(a new 6-arg ctor; the 5-arg(xp,abilities,combat,disabled,enabled)ctor delegates withstatBonuses=truefor back-compat), plus aStatBonusesEnabledPascalCase codec key onWorldRulesAsset. Folded intoisUnrestricted()/toString().- Stat lifecycle:
MMOSkillTreePlugin.PlayerReadyEventresolvesWorldScope.rulesFor(store).statBonusesEnabled()and reapplies-or-strips inline (skill-treereapplyStatRewards/removeAllStatModifiers, masteryreapplyStatSacrifices/removeAllStatSacrifices, classapplyClassPassiveModifiers/removeClassPassiveModifiers); a newregisterGlobal(AddPlayerToWorldEvent)handler runs the same sharedsyncStatModifiersForWorldhelper on EVERY subsequent world entry (cross-world teleport into an instance and the return trip).AddPlayerToWorldEvent(keyed by world name, fired viaWorld.dispatchFor) carries aHolder->holder.getComponent(PlayerRef.getComponentType())->getReference(); the handler hopsworld.executeand PEEKS theSkillComponent(never creates one for a player with no MMO data). It reuses the exact reapply/remove pairs the ready + disconnect paths already use, so it is idempotent. WorldRulesMatchergains leading-*(suffix) and leading+trailing-*(contains) forms beside the existing trailing-*(prefix) and bare*.*is the only metacharacter and only at the ends:Foo_*(prefix),*_Foo(suffix),*Foo*(contains). Precedence stays exact > longest literal core across prefix/suffix/contains (tie-break prefix > suffix > contains) > bare*>WorldRules.DEFAULT. The bundledKweebecNightmare.jsonusesMatch:"*KweebecNightmare_*"+StatBonusesEnabled:false.
v1.5.0
Features
- Adds the real multi-pillar Power Level:
MMOSkillTreeAPI.getPowerLevelfolds combat level, claimed skill-tree stat rewards, unlocked abilities, mastery nodes, and claimed achievements through per-mode weights (COMBAT_ONLY / BALANCED default / COMPLETIONIST), replacing the combat-level-only baseline. The statRewards pillar credits EVERY claimed tree reward: all 16 surfaced stat types carry a per-stat factor (a genericStatRewards.Factorsmap keyed by stat key, so a new stat is a JSON line, not a code change), and weapon-scoped picks (a Swords-only flat-damage node) earn the same power as universal ones - a full combat skill tree lands around 10-15 power next to a maxed combat level's 40, instead of the ~3 the first cut produced. Weights, per-pillar factors, and the[1, 200]clamp are a codec asset (Server/MMOSkillTree/PowerLevel/Default.json, nestedClamp/Pillars/Modesgroups with nullable leaves; partial pack AND owner overlays inherit per leaf; owner overridemods/mmoskilltree/power-level.json). The scalar is cached per player (transient, DB-safe) and invalidated at every pillar-input mutation chokepoint (XP set/remove, tree claim/reset, mastery, ability lock/unlock, achievement unlock/claim). Consumed by the standalone mob-scaling mod, so mob difficulty tracks a player's BUILD, not just their highest combat skill. - Adds a kill-XP multiplier hook (
MMOSkillTreeAPI.registerMobKillXpMultiplier) plus the frozen power reads (statRewardSum,getCombatLevel) for companion mods; removes the elite-mobs system (breaking; install the MMO Mob Scaling mod for mob scaling).
Technical
- The PowerLevel pillar seam is real and generic:
PowerLevelService.register(copy-on-write, safe after startup) lets a companion mod add a pillar; the per-mode weight maps decode GENERICALLY through aMapCodec, so any PascalCased pillar id is a first-class weight key ("ZonesDiscovered": 0.2weights a registeredzonesDiscoveredpillar), and a pillar's registereddefaultWeightapplies whenever a mode does not mention its id (an authored0.0stays an explicit disable). New API readsMMOSkillTreeAPI.getPowerLevelMin()/getPowerLevelMax()expose the clamp so consumers calibrate against the live scale instead of hardcoding it; the mob-scaling mod cross-checks itsDifficulty.MinCap/MaxCapagainst them at settings load and warns on drift. - PowerLevel registers a
ContentValidator(power_leveldomain, surfaced by the startup audit and/mmoconfig validate): unknownStatRewards.Factorsstat keys, mode-weight keys that match no registered pillar (typo detection), a mode whose every pillar resolves to weight 0, negative factors, a degenerate clamp, the tuned-but-unweighted (inert) pillar case as INFO (theMaxHealthopt-in ships that way on purpose), and the maxHealth-plus-STAT_HEALTH double-count trap. WorldRulesstays scoped to the jar's own concerns (XP / abilities / combat / skill lists): the in-cycle draftMobScalingbaseline group (DifficultyFloor/Enabled, never released) was removed before shipping - the MMO Mob Scaling mod owns its per-world config entirely in its ownServer/MmoMobScaling/Worlds/*.jsonfiles (with native-styleParentinheritance), so mob difficulty has exactly ONE per-world authoring surface. Upgrade note for test servers that ran the in-cycle builds together: update MMO Mob Scaling to 1.0.2 in the same deploy as this jar (a 1.0.1 mob-scaling jar calls the removedWorldRulesreads and breaks at spawn).- Schema-design rule adopted repo-wide: cohesive codec field groups are nested sub-objects (own
BuilderCodecbehind one group key), never flat prefixed keys. - Redoes the dialogue schema onto NATIVE codec inheritance and drops the hand-rolled template DSL. Dialogue reuse is now a top-level
"Parent": "resolved through Hytale's own" decodeAndInheritJson(a new genericInheritMapCodecin ziggfreed-common merges theNodesmap BY KEY, and node fieldsappendInherited, so a child overrides/adds a node while inheriting the rest); theextends/params/nodeOverrides/extraNodes/PruneIfEmptyresolver was removed. State-varied greetings collapse onto per-node runtimeConditionsplus genericAnyOf/AllOf/Notcombinators and aQuestStateStatesOR-shorthand instead of duplicated Start rows + author-time pruning (the jarMmo_Hub_Introgreet went from two rows to one; the zone-quests pack'sQuestGiver_Standardparams template was retired and the quartermaster inlined as a standalone tree). A dialogue OPTION can carry aPresentation(nested{Color, Icon:{Item,Glyph}}codec): a per-option button colour (hover/press derived) and an icon by game-item id (item-grid slot) or a fixed glyph token, overriding the action-derived style. The engine,DialogueBodyResolver, page render, and.uiall live inziggfreed-common; the MMO + Kweebec consume it and loc-keys stay convention-derived. Dialogue option COLOURS + icons are now data-driven too: the per-kind style defaults moved out of theDialogueOptionStyleenum into aziggfreed-commonDialogueOptionThemeasset (Pattern A,Server/ZiggfreedCommon/DialogueOptionTheme/*.json, foldeddefaults < pack < owner), so a server owner or pack retints every dialogue option without Java; the enum is only the fail-closed fallback. The hub Guide's single "Got any quests?" line uses this via the new optionStylefield ("Style":"neutral"on both condition branches, so the one logical button reads identically), and its shared footer (return / quests / menu options) is authored ONCE as aFragments/IncludeOptionsoption fragment spliced into every steady + task node instead of inlined per node. A new NPC-awareHasReadyToTurnIndialogue condition (backed byQuestService.hasReadyToTurnInAt, no per-quest wiring) surfaces a green-check "I have returned." option on the Guide whenever the player has any quest whose last step is return-to-this-NPC. - Adds a
boolean showNotificationoverload toMMOSkillTreeAPI.addXp(addXp(store, ref, skillId, amount, showNotification); the four-arg form delegates to it withfalse). When true it surfaces the standard XP-gain banner (skill icon + "+N XP" title + level/progress line) after the raw award, through a new sharedSkillService.notifyXpGainhelper that the natural-gain path also routes through. The award stays raw (no class/world/boost/rested-XP multipliers or level-up handling); the flag controls only the banner, which is skipped when the player is offline. This is the notify seam the optional mob-scaling mod uses so the bonus kill-XP it awards through the API is visible rather than a silent number bump. - Adds data-driven combat-XP routing by damage cause, replacing the hardcoded
MAGIC_DAMAGE_CAUSES(Fire/Poison/Ice -> MAGIC) set inCombatXpEventSystem. A per-skill xp-map key carrying acause:prefix (cause:Fireunder MAGIC,cause:KweebecNightmare_EmberHitunder ARTILLERY) routes that damage cause to the owning skill regardless of the item held when the hit lands, so a player-thrown projectile whose stack is already spent still trains the right skill. Seeded so the three player-thrown Kweebec Nightmare blooms (Moonbloom / Gustbloom / Emberbloom) award ARTILLERY out of the box (inert until that mod is installed, since the causes never fire otherwise);CombatWeaponUtil.resolveCombatSkillFromDamageCauseis the resolver and item-id matching skipscause:keys. Owner-overridable throughxp-maps.json; the value is the per-hit amount used only in PER_HIT mode (DAMAGE_BASED / DAMAGE_PLUS_KILL award damage 1:1 and ignore it).
Bug Fixes
- Fixes the statRewards power pillar ignoring weapon-scoped rewards: the aggregation passed
skillId=null, whichStatSourceAggregatortreats as universal-scope-only, so every weapon-scoped FLAT_DAMAGE/STAT_DAMAGE/crit/lifesteal pick (the bulk of every combat tree, ~51 flat damage in a full Swords tree) contributed ZERO power. A newStatSourceAggregator.sumAllScopesTreeOnlycounts every claimed tree reward regardless of combat-target scope, with mastery excluded (owned nodes already feed the separate mastery pillar, so counting their modifier values would double-count). - Fixes
MMOSkillTreeAPI.removeXpleaving a stale (higher) cached power level:SkillComponent.removeByNamenow invalidates the PowerLevel cache likesetXpByNamedoes (a removal can cross a level boundary downward). - Fixes the PowerLevel pack-layer merge asymmetry: a partial pack
Server/MMOSkillTree/PowerLevel/*.jsonused to REPLACE the whole tuning wholesale (silently zeroing every factor and weight it did not repeat, unlike the owner file which deep-merged); the pack layer now overlays per leaf onto the jar default exactly like the owner layer. - Fixes a client disconnect on the Bounty Board (
Selected element in CustomUI command was not found. Selector: #BountyList[i] #StatusDot.Background): accepting a plain kill/gather bounty while any bounty sat in the "Ready to turn in" section made the accept fast-path recompute the row's index against the active rotation only, ignoring the section headers interleaved into the same#BountyList, so the partial update could land on a header row (which has no#StatusDot) and kick the player. Partial updates now target the exact interleaved index recorded by thebuild()that produced the client DOM and fall back to a full rebuild when the row is unknown; the convention is documented inpages/CLAUDE.mdand the NPC quest page's scroll-preserving updates were hardened the same way (built-order lookup instead of a recomputed index). - Fixes both quest surfaces resetting a scrolled list to the top on every CTA. Selecting a quest (
NpcQuestPage) and pin/track, claim, and turn-in (QuestPage) responded with a full page reopen (openCustomPage,clear=true), so the client re-laid-out the whole DOM and every scroll container dropped to offset 0. The fixed regions now update in place: detail buttons are pre-bound state-independently (so no transition needs a re-bind), dynamic sub-containers areclear()-ed then re-rendered,QuestPage's#ActionBtnis unified to one state-dispatchedprimaryaction (accept vs claim resolved at handle time), and select/accept/abandon/claim/turn-in/track route through a partialsendUpdate(clear=false); a quest that leaves the current list is hidden (not removed) so echoed row indices do not drift, with a full-rebuild fallback when a selector is absent. CTAs that change list membership (filter/search/status/tag/tab) keep the full rebuild by design. See [[fix-ctas-on-quest-purrfect-nebula]]. - Fixes REACH_LEVEL quest objectives rendering 0/target for a player who already had the levels.
QuestService.completeSatisfiedReachLevelonly wrote progress once the target was met; it now high-water-marks the live level into stored progress viaQuestObjectiveProgress.applyValue(idempotent, never decreases across repeated page opens), so the row shows the real current level against the target and completes when it reaches it. - Fixes an admin settings menu that could disconnect a player on open, resolved incidentally by this release's UI work.
Improvements
- Reworks
mob-drop-commandsto grant bonus mob loot through native Hytale item-drop tables, and defaults the feature to off. A drop pool can now carry a"dropList": "that is rolled through the native drop pipeline and spawned as real ground items at the corpse (walk-over pickup, layered on the mob's own vanilla loot), in addition to or instead of its console" commands. The mod ships no default pools, so a fresh install injects no bonus drops and fires no drop notification: the pre-1.5.0 always-on 5%-per-killIngredient_Life_Essencegive and its "Mob Drop!" toast are gone unless a server owner opts back in. The per-pool notification stays available but is off by default; the console-command seam is unchanged. ConfigSCHEMA_VERSION1 -> 2 (additivedropListkey; existing overrides are preserved on load). - Rested XP default accrual rate reduced from 1500 to 250 XP per hour (
SkillConfig.restedXpPerHour); the accrual mode staysflatand every other rested-XP knob is unchanged, so the offline rested pool fills more gradually. Owner-overridable in the skill config. - Adds a "My Bounties" tab to the Bounty Board: every bounty accepted from that board (ready first, in-progress after), with the full detail pane; adds an Abandon button to the bounty detail pane (shared quest abandon flow + feedback), so bounties can be dropped at the board instead of being stuck until expiry.
- Adds three optional tutorial side quests from the Adventurer's Guide, a short "homesteader" arc that unlocks alongside the existing starter chain without gating it: Make Camp (craft a couple of chests and a campfire, then lay down some blocks), Home Cooking (cook a meal on that campfire and eat it), and Prove Your Mettle (hunt the Trork). Each trains a pillar the starter chain skips (Building, Cooking, sustained combat) and hands out bonus XP. The Guide offers them through a single "Got any quests?" prompt: when it has something to start you on it opens an in-conversation offer (plus a "Show me the full list" link to the quest page), otherwise it opens the quest page directly; handing a side quest in surfaces the next available job automatically, and the Guide greets the player with a "well done" turn-in beat (the
ReadyToTurnIndialogue condition, so it appears only once the work is actually done). The single prompt is driven by a new NPC-awareHasOfferableQuestsdialogue condition (no per-quest wiring, so future giver quests need no dialogue edits). All three ship as jar server assets with full 9-language text and need no content pack; the starter chain still ends at Climbing the Ranks, which stays the hook for future questline packs. - Adds a "My Quests" tab to the NPC quest page listing every quest the player has active or ready to claim, with the existing detail pane and abandon flow, so accepted quests can be reviewed or abandoned without leaving the NPC for the main quest menu.
- Quest lists now sort by what needs the player's attention: ready-to-claim / ready-to-turn-in quests float to the top (on an NPC's page a quest whose live step is a turn-in or talk at THAT NPC counts as ready there), in-progress next, and finished (turned-in) quests sink to the bottom; applied to the main quest log and both NPC-page tabs.
- The quest log's track button and the achievement pin button are now a real waypoint-marker icon button (the game's own
RespawnPointArrowglyph, dim when off / gold when tracked) instead of an ASCII "+"/"-" text label; one sharedPinIconRendererpaints both surfaces, and no new texture assets are shipped.
- Restyles the in-world quest tracker HUD to MATCH the native Hytale objective HUD (the client's own
Data/Game/Interface/InGame/Hud/ObjectivePanel+ObjectiveSlot+ObjectivePanelTask+ObjectiveCommon): theObjectivePanelContainerframe, CONTENT-sized so the panel hugs only the quests actually tracked instead of statically reserving a fixed 5-quest box, uppercase-gray objective headings (#b7b8b9), per-task default/complete icon glyphs (ObjectiveTaskIcon*), gold task text (#ca9f37), and a separate right-aligned completion count. Uses the native textures (copied into the jar) + palette + bold/letter-spacing font family. A newHudPosition.toAnchorContentHeight+MmoHud.usesContentHeight()seam gives any HUD the content-sized-height option. No behavior change; the tracked-quest data source and render seams are unchanged.
- Adds a player
/mmohudcommand so each player can toggle their own on-screen HUDs: the quest tracker and ability-cooldown HUDs, plus the MMO Mob Scaling mod's zone-difficulty and mob-inspector cards (allflips every one at once). The choice persists per player and each HUD honors it live on the next tick; the preference is stored whether or not the optional mob-scaling mod is installed, sohide difficulty/hide npcare remembered and applied if that mod is added later.
v1.4.4
A fix for the quest tracker counting finished quests against its five-slot limit, a fix for the admin Skill Tree Overrides editor crashing on open, a fix for passive abilities showing a raw text code on the skill tree, a /mmoquestadmin reload that also refreshes pack-delivered quests, and a new admin switch to turn the on-screen quest tracker on or off.
Bug Fixes
- Fixed: "You can only track 5 quests at a time" even when the Quests menu showed fewer tracked quests. A tracked quest that completed, was claimed, or was abandoned auto-hid from the tracker but left a dead pin in the saved tracked-quest set. The tracker panel renders ACTIVE quests only (
QuestService.getTrackedActiveQuests), so those dead pins were invisible yet still counted towardMAX_TRACKED(5); once five accumulated, no new quest could be tracked. The tracked-pin set is now self-healing:QuestComponent.trackreclaims dead pins before measuring the cap, andQuestService.selfHeal(run on login and on every quest-bearing surface open) prunes them, so an existing player's stale save is cleaned automatically and the cap always reflects only quests still ACTIVE. A pin is a UI preference, not progress, so no quest state or progress is deleted. - Fixed: opening the admin Skill Tree Overrides editor crashed for almost any skill (server
IllegalArgumentException: Value can not be null). The reward-list rendered each row's DISPLAY column fromSkillReward.getDisplayName(), which isnullfor every built-in reward (built-ins auto-generate their text and carry no authored name). That null reachedUICommandBuilder.set("#DisplayText.Text", ...)->new BsonString(null)-> the exception, which escaped the page build all the way to the world task runner (Failed to run task!) and crashed the open (the same.Text/.Value-sink crash class as the 1.4.2 menu disconnects). The DISPLAY column now rendersreward.resolveNameMsg()(the@Nonnulllocalized reward name) onto.TextSpans, so it shows the player-facing name and can never pass null. The same null path on the edit form, where clicking a built-in reward copied its nullgetDisplayName()/getDescription()into#DisplayField.Value/#DescField.Value, is now null-coalesced. This admin page had been missed by the 1.4.2.Text-sink sweep. - Fixed: a passive ability reward showed a raw localization key after its name on the skill tree (
mmoskilltree.reward.ability_unlock.tag.passive).Messages.rewardDisplayMsgappendedMsg.tr("reward.ability_unlock.tag.passive")for passiveABILITY_UNLOCKrewards, gated onLocalizationConfig.hasKey(...)- which returns true even though the tag shipped empty ("", inEnglishDefaults).generateEnglishLangstrips empty values from the bundled.lang, so the client could not resolve the key and rendered it raw after the name (for exampleUnlock: Assassinate mmoskilltree.reward.ability_unlock.tag.passive). The half-built tag feature (both.tag.activeand.tag.passiveshipped empty, and.tag.activewas never emitted at all) was removed along with its twoEnglishDefaultskeys; an ability-unlock reward now renders just its localizedUnlock:title.
Improvements
/mmoquestadmin reloadnow reconciles asset-driven (pack) quests, not just owner-authored files.QuestConfig.load()re-reads the owner files undermods/mmoskilltree/quests/and re-applies a CACHED snapshot of the pack quest layer, so a reload never re-derived pack content from the authoritative source; quests shipped by a content pack (including third-party packs the server owner never authored) could only be picked up via the engine's own asset path.handleReloadnow also callsAssetMergeHandlers.reapplyQuestPackLayers(), which rebuilds the quest TEMPLATE then quest pack layers from the LIVE registered asset stores (the sameDefaultAssetMapthe nativeLoadedAssetsEventcarries) and re-merges them in dependency order, so an admin reload reconciles to the full current pack set on demand.- Asset-driven quest edits hot-reload through the engine file monitor, which the mod listens to via
LoadedAssetsEvent(AssetMergeHandlers) - but/plugin reloadis NOT a safe reload path for this mod. An unzipped/mutable pack dir is file-monitored, so saving aServer/MMOSkillTree/Quests/*.jsontriggersloadAssetsFromPaths->LoadedAssetsEventautomatically (no command, no restart). A zipped/jar pack (e.g. the mod's own bundledsrc/main/resources/Server/...quests) is immutable and not file-monitored, so those edits need a server restart. The native/plugin reloadcommand WOULD re-read a jar pack from disk, but it reloads the plugin under a fresh classloader, which re-registers the mod's 5 ECS component types (: SkillComponent/QuestComponent/AchievementComponent/StatisticsComponent/EliteComponent) at NEWComponentRegistryindices; the running world's chunk holders still reference the old indices, so the world-shutdown chunk save throwsArrayIndexOutOfBoundsExceptioninComponentRegistry$Data.getComponentCodecand kills the WorldThread (observed 2026-06-30:plugin reload Ziggfreed:MMOSkillTree->Index 162 out of bounds for length 162-> server dead). This is a Hytale plugin-hot-reload limitation for any mod that registers ECS component types and is not fixable mod-side, so the reload story is:/mmoquestadmin reload(owner files + loaded pack quests) or the file monitor (unzipped packs) for live edits, and a restart for jar/zip-bundled quests. See [[hytale-asset-hot-reload-paths]]. /mmoconfig reloadnow also reloads currencies.handleReloadaddsCurrencyConfig.getInstance().load(false)alongside the existing skill / xp-map / luck / skill-tree / command-reward / mob-drop reloads, so an admin can editmods/mmoskilltree/currencies.jsonand pick it up without a restart. Currency is a pack-layered override config whoseclearAll()preserves the pack layer, so the reload re-reads the owner override and re-applies pack-defined currencies.- New: an admin on/off switch for the on-screen quest tracker HUD. The admin config menu's Quest Tracker HUD section gains an Enable toggle beside the existing position controls, and
/mmoconfig questhud on|off|toggledoes the same from chat. The switch applies live to every online player with no reconnect and persists inhud-layout.json, so it survives restarts. Turning it off does more than hide the top-right tracked-quests overlay: the per-player updater that drives it stops doing any work while disabled (no quest reads, no panel pushes), so an admin who does not want the overlay pays nothing for it. The quest-giver map markers refresh on that same updater, so they pause while the tracker is off.
Technical
- New
QuestComponent.pruneStaleTracked()removes every tracked-quest pin whose quest is no longerACTIVE(returns the count removed).track()calls it before theMAX_TRACKEDcheck so the cap is measured against live pins only and never displaces a live pin;selfHealadds its result to the heal count, marking the QUESTS domain dirty under the DB backend so the cleaned map persists.QuestComponentTestpins the prune, the at-cap reclaim, and that an honest full house of live pins still rejects a new track. SkillTreeOverridesPagebrought up to current admin-page conventions. The tier-selector buttons and the "+" add-tier button now append a sharedPages/SkillTreeTierButton.uitemplate that carriesSounds: $C.@ButtonSounds, replacing two hand-builtappendInlineTextButtonliterals that were silent on click and duplicated a large inline style;SkillTreeRewardRow.ui's@IdButtonStyle/@RemoveButtonStylegained the sameSounds. The duplicated{Gathering, Combat, Crafting, Misc}+#Tab*arrays (repeated acrossupdateCategoryTabs/bindCategoryTabEvents) were hoisted to single index-alignedCATEGORIES/CATEGORY_TAB_IDSconstants, the scattered status hexes toCOLOR_*constants reused by the tier-status chip, the per-reward status chip, and the tier-button tint, and a deadMMOSkillTreeAPI.getSkillComponent(...)local plus its frozen-residuedata.SkillComponentimport were removed.- Quest tracker HUD gains a persisted enable flag.
HudLayoutConfigadds anenabledmap keyed byHUD_KEY(parallel to its position map, default on, only an explicit override stored) behindisEnabled/setEnabled, persisted tohud-layout.json.QuestHudTickingSystem.tickPlayerearly-returns when disabled, which gates the whole system (theQuestComponentpeek, the HUD push, AND the quest-giver waypoint refresh that piggybacks on the same peek), so a disabled HUD costs one config read per tick. This differs fromAbilityHudTickingSystem, which keeps ticking and pushes a blank frame; because the quest tick instead stops, it cannot self-correct the visible panel, so disabling broadcasts a one-time hide andQuestTrackerHud.build()starts the panel hidden for players who connect while disabled (enabling repaints on the next due tick). The broadcast goes through a new genericMmoHud.forEachOnlineHud(key, Consumer(the shared world-thread iteration that) refreshPositionForAllOnlinenow also routes through). Surfaced at/mmoconfig questhud on|off|toggleand the admin config page's#ToggleQuestHud(Englishui.admin.quest_hud_enabled_label, translated into all 8 non-English locales).
v1.4.3
A bounty quality-of-life pass: an accepted bounty can be abandoned from the Quests menu, and a finished bounty can always be turned in or claimed at the board even after it rotates out of the active set.
Updates
- Adds an Abandon button to accepted bounties in the Quests menu. A bounty surfaces in the quest log by player state (independent of the board rotation), so the Quests menu is the expiry-agnostic home for dropping one; turn-in / claim stay at the board. The shared
"abandon"handler (QuestService.abandonQuest+untrack+QuestFeedback.abandoned) already existed and was only suppressed in the bounty branch. - Adds a "Ready to turn in" section at the top of the Bounty Board left panel. It lists every bounty the player still holds a board action on for that board, regardless of the current rotation:
COMPLETED_UNCLAIMED(claim) andACTIVEwith a pending non-NPC-lockedTURN_INobjective (deliver). Previously these vanished when the bounty rotated off the board and an inventory-parked or un-handed-in reward was lost. A still-live entry may also appear in its rotation slot below; the duplicate is intentional so the slot keeps its position. No reroll button on these rows (reroll renders forNOT_STARTEDonly).
Bug Fixes
- Fixed: bounties auto-claimed their reward in the field instead of at the board, which is what stranded rewards on expiry. Bounties shipped with
autoClaimRewards: true, so completing the objectives granted the reward on the spot (only the inventory-full case parked it).QuestService.checkQuestCompletionnow forces every bounty (BountyService.isBounty) down the manual-claim path - it always parks inCOMPLETED_UNCLAIMEDand is collected at the Bounty Board (the rotation-independent "Ready to turn in" section), regardless of the authored flag. This makes the board the single claim point and removes the field-grant path that could lose a reward. A dedicatednotify.bounty_ready_to_claimtoast points the player at the board (all 9 locales). - Fixed: quests with no
autoClaimRewardsflag defaulted to auto-claim. The structured-quest default was inverted (an omitted flag resolved totrue); most quests should never auto-claim. An unspecifiedautoClaimRewardsnow resolves tofalse(park for manual claim); only an explicittrueopts in. Every shipped quest sets the flag explicitly, so no shipped content changes behavior.BountyBoardPage.doTurnInhard-rejected any quest id not in today's active set; it now validates board ownership (BountyService.isBountyFor) instead, so an already-accepted (ACTIVE) bounty can be turned in from the new outstanding section after it expires.doClaimgained the same board-ownership guard. - Fixed: a bounty could show an Accept button in the quest log, bypassing the board's combat-level gate and active-set anti-forgery. The quest log's
NOT_STARTEDbranch now points the player at the Bounty Board (board-accepted only) instead of offering Accept; this also closes the path where a just-abandoned bounty would flash Accept on the partial refresh (a bounty abandon does a full reopen). - Fixed: the auto-spawned hub guide opened the hub menu directly instead of the welcome dialogue. The hub role uses
OpenMmoUi Target: "auto", which resolved the press-F destination from the routing target frozen intospawn-hub-placements.jsonat spawn time. A world that spawned the guide before thedialoguefield existed recordedtarget = "hub", and the once-per-world auto-spawn gate (MmoNpcPlacementStore.hasAutoSpawned) means the NPC never re-spawns - so the stale"hub"target pinned press-F to the menu; a missing / reset placements file dropped it to the same"hub"fallback. Either way themmo_hub_introintro dialogue was reachable only through the hub page's onboarding link. The dedicated hub role does not need the placement indirection (that exists for the ONE shared quest-giver role), soActionOpenMmoUi.resolveTargetnow identifies the hub by theRoleitself (role.getRoleName()==SpawnHubConfig.getRole()) and resolves its destination LIVE fromSpawnHubConfigvia a newhubTargethelper - no placement-record dependency at all. The configured dialogue (or a later config change / a disable viadialogue: "") applies immediately, with no world or NPC reset. Quest-giver NPCs (a different role) are untouched and still read their recordedgiver:target.
Technical
- New
BountyService.getOutstandingBounties(quests, boardId)- the single authority for "bounties this player can act on at this board, rotation-independent" (iteratesQuestComponent.questStates, filtersisBountyFor+ the two outstanding states, sorts by sortOrder then id). Used byBountyBoardPage.build. - Lifted
findActiveTurnInObjectiveintoQuestService.firstActiveTurnInObjective(it was duplicated verbatim inQuestPageandBountyBoardPage); both pages andgetOutstandingBountiesnow share it. BountyBoardPageleft panel switched to a running-index render with interleaved section headers (reusing the sharedPages/ShopSectionHeader.uitemplate viaappendSectionHeader, mirroringTokenShopPage); the empty-panel guard and selection-validation now consider the outstanding set as well as the active set.- Added lang keys
ui.bounty.section_outstanding("Ready to turn in") andui.bounty.section_available("Available") across all 9 locales. QuestDefinition.Draft.toDefinitionno longer defaults an omittedautoClaimRewardstotrue(autoClaimRewards != null && autoClaimRewards);QuestCodecDecodeTestpins the manual-claim default. The bounty-contracts-packQuestTemplates/Bounty_*_Standard.jsonnow shipautoClaimRewards: falseto match the engine force (the engine is authoritative either way).
v1.4.2
A hotfix for a Quests-menu crash (the same client-side-localization sink class as 1.4.1), an audit-driven sweep of every other menu carrying that same sink, plus a restored compatibility shim for third-party mods that read MMO skill data.
Bug Fixes
- Fixed: opening the Quests menu could disconnect a player on any server with quest categories. Each dynamic category tab set its localized name (a client
Message) on aTextButton.TextString sink; the client rejected it ("CustomUI Set command couldn't set value. Selector: #DynamicCategories[N] #CatBtn.Text") and dropped the player at the loading screen. The twin tab on the Achievements menu was already converted in 1.4.0; the Quests menu's#CatBtnwas the one left behind. - Fixed: the same disconnect (or a raw
{0}placeholder) lurked on several more menus, mostly on servers with custom skills or content. An audit of every menu turned up the identical sink wherever a dynamic / parameterized name reached a clickable button: the mastery "Pay with X" buttons, the Token Shop and Bounty Board offer rows, the leaderboard skill filter, the tracked-achievement list, the new-player onboarding preview rows, and several admin skill pickers. All are converted to the safe rich-button form. (Bonus: the admin skill pickers and leaderboard filter, which previously showed raw English skill names, are now localized.) - Fixed: a startup error and broken integration in third-party mods that read MMO skill data (e.g. Perfect Parries). The 1.4.0 client-side-localization migration removed the per-player language field from
SkillComponent, including itsgetLanguage()accessor. A mod that resolves that method by reflection (Perfect Parries probes it in the same block as the parry-bonus API) logged aNoSuchMethodExceptionat boot and could drop the whole MMO integration. A compatibilitygetLanguage()is restored; it returns the server default language and reads no per-player locale, so localization stays client-side.
Technical: client-side-localization sink fix (.TextSpans / ZigRichButton)
- Converted
QuestCategoryTab.ui's#CatBtnfrom aTextButtonto a vanillaButton+ innerLabel #Label, and pushed the category name throughZigRichButton.text(.TextSpans) inQuestPageinstead ofset("#CatBtn.Text", Message). Mirrors theAchievementCategoryTab.ui/AchievementPageshape exactly (applyCatTabActiveviaZigRichButton.color+#Label.Style.RenderBold); the#FilterAllTextButton chip keepsapplyCategoryTabStyle. Authority:.claude/research/hytale-rich-text-textspans.md. - Audit sweep of the same sink class across all menus. Three parallel read-only audits cross-checked every
set(".Text",site against its) .uielement type. The confirmed offenders, all converted to aButton+ innerLabel #Labelpushing text viaZigRichButton.text(.TextSpans):#PayBtn1..3(MasteryPage.ui/MasteryPage, parameterized "Pay/Contribute with {0}"),#ShopBtn(ShopRow.ui/TokenShopPage, dynamic offer title + parameterized skill-level),#BountyBtn(BountyRow.ui/BountyRowRenderer, consumed byBountyBoardPageANDOnboardingPagepreview rows),#TrackedNameBtn(TrackedAchievementRow.ui/ViewXpPage),#SkillBtn(the sharedSkillButton.ui, used by the 4 admin skill pickers +UIUtil.buildSkillSelector), and the 12#SkillFilterN(LeaderboardPage.ui/LeaderboardPage).CustomSkillWizardPage's#PageTitleLabel moved from.Textto.TextSpans. - Two shared stylers in
UIUtil(the Button+Label twins of the TextButtonsetTabStyle):setRichSkillTabStyle(active/inactive skill chip: PatchStyle.Background.Colorretint +#Labelcolor/bold viaZigRichButton.color) andselectRichRow(apply-only selected tint for shop/bounty list rows). Both replace the old per-state.Style.Default.Background(a bare String, which red-Xes on aButton) +.LabelStyle.TextColor(aButtonhas noLabelStyle) blocks.UIUtil.buildSkillSelectornow pushesMessages.skillNameMsg(localized) instead of a rawregistry.getDisplayNameString. The skill chips / list rows adopt the shared ziggfreed-common$Z.@ZigRichRowBtnStyle/@ZigRichPrimaryBtnStyle; the inline#LevelSelector/#TierButtonsTextButtons (literal"Lv N"text) are unaffected and stay TextButtons.
Technical: external-API compatibility shim
- Re-added
SkillComponent.getLanguage()as a@Deprecated, stateless shim returningSkillConfig.getDefaultLanguage()(the server-wide default; no per-player locale read or persisted), co-located withgetSummedTotalLevel()(the otherSkillComponentmethod Perfect Parries reflects). The 1.4.0 migration deleted thelanguagefield, itsLanguagecodec entry, and theget/setLanguageaccessors when display text moved client-side; an external consumer that resolvedgetLanguage()in the same reflection try-block as the parry-bonus API methods (OptionalMmoSkillTreeApi.resolveApi) failed its entire API resolution on the one missing method. The shim restores resolution without re-introducing a per-player locale; no internal caller uses it.
v1.4.1
A hotfix for a skills-menu crash (and an admin-page crash) plus a few buttons that rendered a raw {0} placeholder, all surfaced by the 1.4.0 client-side-localization migration.
Bug Fixes
- Fixed: the skills menu could disconnect a player who had an XP boost active. The skill overview's boost button rendered its active "+N%" badge as a raw client
Messagecarrying a literal%; on a.Textsink the client rejected it ("CustomUI Set command couldn't set value. Selector: #SkillGrid[0] #BoostBtn.Text") and dropped the player at the loading screen. The admin Item Rewards editor could disconnect the same way on open (a tier-labelMessageset on a Label.Text). - Fixed: several buttons rendered a literal
{0}placeholder instead of their value. The bounty board Turn In button (itsX/Yprogress count), the mastery Refund button (its percentage), and the ability Restore-Items cooldown button showed the raw template (Turn In ({0}/{1}),REFUND ({0}%),On cooldown ({0})) because a parameterized translation does not substitute{0}on aTextButton.Text.
Technical: client-side-localization sink fixes (.TextSpans / ZigRichButton)
- Root cause: the 1.4.0 client-side-localization migration turned
.TextString sinks intoMessagesinks. On the live 0.5.6 client a Label.Textcrashes on ANYMessage, and aTextButton.Textresolves only a single key / raw string - it does NOT substitute{0}/{1}params (a parameterizedMsg.trprints the literal template) and chokes on a raw string carrying a bare%. Authority:.claude/research/hytale-rich-text-textspans.md. - Converted the four affected buttons from
TextButtonto a vanillaButton+ innerLabel #Label, pushing text throughZigRichButton.text(.TextSpans, which substitutes params and renders markup):#BoostBtn(SkillCard.ui/ViewXpPage),#TurnInBtn(BountyBoardPage),#RefundBtn(MasteryNodeCard.ui/MasteryPage),#RestoreCastItemsButton(AbilityBindPage). Each keeps its look via aButtonStylemirror of its oldTextButtonStyle(the shared blue Turn-In style added toMmoButtons.ui; Refund adopts the shared@ZigRichRowBtnStyle). ItemRewardsPagetier separators moved from#TierLabel.Textto.TextSpans(a Label takes aMessageonly on.TextSpans).- Added the
ui.boosts.percentlang key (+{0}%, symbolic so identical across all 9 locales) so the boost badge's%lives in the template; the boost text usesMsg.tr("ui.boosts.percent", pct). - Gated the skill overview's Total-Level card boost button on an active boost or a usable token (matching the per-skill cards), instead of showing it unconditionally.
v1.4.0
A quests-and-NPCs update. Quests can now be handed out by quest-giver NPCs that hold branching conversations (greetings that change with your progress, option branches, lore, one-time handouts), and a TALK_TO_NPC objective is always author-configured rather than a bare press-F. Three new objective types (reach a location, eat or drink an item, spend currency) join the set, and any objective can be scoped to a world zone, so a hunt only counts "in the Howling Sands". The built-in quest catalog is trimmed to two tutorials and the real content moves into packs: a brand-new free MMO Skill Quest Pack adds two campaign questlines (the Emerald Wilds and the Howling Sands), while the MMO Skill Bounty Pack adds a fast Quick Contracts board and daily training contracts. Achievements get a refresh: every achievement now renders an icon, the per-board daily / weekly chains retire to Feats of Strength, and new chains track eating, spending, and talking.
Updates
- New: quest-giver NPCs. Quests are handed out by NPCs placed three ways (a hybrid model): SPAWN givers stand beside the world-spawn Adventurer's Guide, ZONE_DISCOVERY givers appear once per world the first time any player discovers a named zone, and MANUAL givers are placed with
/mmonpc spawn giver. Pressing the interact key resolves live: a configured dialogue if one exists, otherwise the NPC's quest list. Configured in the newquest-givers.json. - New: branching NPC dialogue. A full dialogue system (
dialogues.json): each tree is nodes of text + options, options gated by conditions (quest state, a one-shot flag, a unified requirements block) and firing actions (talk, accept / turn in a quest, grant a one-time reward, set a flag, open a menu, jump to another node, close). Dialogue is the canonical source forTALK_TO_NPCobjectives, so a talk objective is always intentional. Still interoperates with any external dialogue mod that can run/mmoquestui. - New: three objective types.
REACH_LOCATION(stand within a radius of a world coordinate, polled once a second with inside / outside edge detection),CONSUME_ITEM(eat or drink a consumable, with a craft-window veto so crafting never counts as consuming), andSPEND_CURRENCY(fires whenever the cost service drains a currency, item- or counter-backed). All three are first-class across quests, achievements, and statistics. - New: zone scoping on objectives and criteria. Any objective or achievement criterion can carry a
zone, matched case-insensitively against the engine's current zone name or its region. A volatile zone-scope index gates resolution so zone reads only cost anything when some content actually scopes to a zone. - New: rotation selection type "all". A board can run its whole pool every period (no slots, no reroll), available for an owner-authored all-at-once board.
- The free MMO Skill Quest Pack is announced (arfemia/MMOSkillQuestPack): two campaign questlines (the Emerald Wilds and the Howling Sands) with their own givers, dialogue, and achievements. Its first release (1.0.0) ships with MMO Skill Tree 1.6.0; the pack's own notes carry the per-questline detail.
- MMO Skill Bounty Pack: new contracts. The Bounty Pack adds a fast-rotating Quick Contracts board and folds the migrated daily quests into its Daily board as training XP slots; the pack's own notes carry the detail (arfemia/MMOSkillBountyPack).
- Achievements refresh. Every achievement resolves a display icon (authored, else derived from its chain / skill / category / first reward). The per-board Daily and Weekly bounty chains became Feats of Strength (kept byte-identical but flagged, worth zero points, stamped with a
legacySincedate), and new chains track consuming items, drinking potions, spending currency, and talking to NPCs. - New Feats of Strength for the retired one-time quests. The skill-chain quests removed from the jar (mining, fishing, the warrior / mage paths, the scattered one-offs, and the Champion of Orbis finale) are recognized by five new Feats of Strength, granted retroactively at login from a player's saved quest history (the removed quests' completion events can never fire again, so a login scan is the only path). Cosmetic only - zero points, no reward - so a returning player keeps the badge without it touching their point total.
- Built-in quest catalog trimmed. The jar now ships only two tutorial quests (
getting_started+forgot_something); the old skill-chain and daily quests are gone (the dailies live on the Bounty Pack boards now). - Game-native dialogue and quest menus. The new NPC dialogue and quest-giver menus use the game's bordered button look with meaningful colors (green to accept, gold to claim, blue to hand in, red to abandon) and a small icon on each dialogue choice; the conversation window has a decorated frame and shows your current objective for that NPC at the top, so a "talk to" quest reads clearly. The Adventurer's Guide greets newcomers and points them to the first quest giver, so the first step reads clearly. (Admins can clear a player's dialogue state with
/mmoquestadmin resetdialogues; conversation progress is keyed to quest state, so it always recovers on its own after a quest reset.) - New: a hub onboarding arc the Adventurer's Guide runs out of the box. The guide offers a new player a short tutorial or a skip, and both paths leave you with the Skill Menu Scroll - so the menu is always one item-use away. The tutorial ships in the jar and works with no pack installed; breaking your first block hands over the scroll automatically. Finishing or skipping the tutorial opens the Emerald Wilds campaign (with the free MMO Skill Quest Pack installed), so the jar tutorial and the pack campaign read as one continuous start.
- New: dialogue options can complete a quest or run a command. A dialogue option can now force-complete a quest and grant its rewards (the skip-tutorial path uses this) or run a console command, so a conversation can do more than just talk and hand out quests.
- New: per-world rules. A new
world-rules.json(and pack-authoredServer/MMOSkillTree/WorldRules/*.json) scopes gameplay per world, matched by world name. Matching supports a trailing-*prefix, so an instanced world that spawns with a random suffix (Dungeon_01_) is caught byDungeon_01_*(precedence: an exact name beats the longest prefix beats a bare*default). Each rule sets an XP multiplier (0turns XP off,0.5halves it,2.0doubles it), a combined abilities toggle (off blocks ability casting - silently, the cast item just behaves like a normal item - AND hides the ability-cooldown + quest-tracker HUDs, for a lobby or story instance), a weapon-combat toggle (off runs vanilla combat in the world: no crit, lifesteal, skill-tree / mastery damage bonuses, or defense reduction), and a list of skills whose XP is disabled in that world (on top of the global disabled-skills list). A world with no authored rule is unaffected. Surfaced in/mmoconfig validate.
Migration notes
- Required: install the ZiggfreedCommon companion jar. 1.4.0 adds a hard runtime dependency on
ZiggfreedCommon(the shared dialogue engine, shipped as a SEPARATE jar that loads before the MMO and is never bundled). DropZiggfreedCommon-1.1.1.jarinto your server's mods folder alongside the MMO Skill Tree jar; without it the mod will not load. (Fresh installs: add both files.) - Some achievement points drop. Retiring the daily / weekly bounty chains to Feats of Strength sets their point value to zero, so a player who had earned those chains loses roughly 1,685 achievement points. Point *milestones* and every other achievement are unchanged; this only affects the retired chains' contribution to a player's point total.
- Removed-quest progress is orphaned, not surfaced. Players who were mid-chain on a now-removed built-in quest keep that progress in their save, but it is never shown again (the quest no longer exists). No data is deleted. A player who *finished* a retired one-time chain is recognized retroactively by a new Feat of Strength (see above).
- Daily quests now require the Bounty Pack. The built-in daily quests moved onto the Bounty Pack's new boards. A server without the pack loses those dailies; installing it restores them (now as bounty-board contracts).
- No new crafting-objective content. The
CRAFT_ITEMobjective type remains, but the underlying craft event is unreliable engine-side, so 1.4.0 ships no new crafting quests or contracts pending an upstream fix. - The in-MMO language picker was removed; locale follows the game client. Display text is now resolved by the player's own client in the language they set for Hytale, so the welcome-screen and Settings language selectors are gone and a player's stored language is no longer persisted (the orphaned
"Language"save key loads clean, no migration). Server owners keep the default-language fallback (skill-config.json) for the residual server-side strings (floating combat text). The owner-sidelocalization/messages-overrides no longer reach the client..json
Bug Fixes
- Hardened the consume-item detection against false positives from crafting: a 250ms craft-window veto suppresses
CONSUME_ITEMprogress right after a craft, and an inventory-add in the same tick vetoes a same-tick remove, so only an actual eat / drink counts. - Multi-world ability fixes. On a server running more than one world (instances / raid arenas), ability damage-over-time, heal-over-time, ground-zone, beam, and projectile effects now apply in the world they were cast in instead of being silently dropped by whichever world ticked first. Ground-zone / beam / projectile damage is queued through the engine's gather phase, removing the dropped-damage + "Store is currently processing" log spam those abilities could produce under load, and the colored combat-text restore no longer rescans every player tick.
- "Reach level in any skill" objectives complete for players already past the level. A quest objective that targets any skill (the
*target, e.g. the Skill Mastery Pack's "reach level 15 in any skill" starter quest) now completes for a player who already meets it - retroactively when the quest is accepted or the player logs in, and when the quest menu is opened to re-validate - instead of never completing because*was read as a literal skill name. The same any-skill target now also drives the matching achievement criteria and renders as "in any skill".
Technical: NPC dialogue system
- New
dialogue/package.NpcDialogueis the runtime dialogue model (Start+Nodes), still decoded through its real codec (DialogueActionis a type-dispatched polymorphic list viacom.hypixel.hytale.codec.lookup.CodecMapCodec("Type")- the engine'sItemDropContainerpattern - with subtypes Talk / AcceptQuest / TurnInQuest / OpenPage / RewardGrant (with a once-flag) / SetFlag / Goto / Close;DialogueConditionsgates options on quest-state, a unifiedRequirementsblock viaGateEvaluator, and flag / not-flag).NpcDialogueAssetis now a rawPayload-wrapped asset like every other content type: the body is resolved through the template + sugar pre-pass (below) and THEN decoded viaNpcDialogue.CODECinDialogueConfig.decodeBody, so the codec is still the single schema authority for both the pack and owner (dialogues.json) layers (asset-pack-driven: one codec, one decode path).DialoguePage+DialoguePage.uirender the tree;DialogueActionExecutorruns an option's actions and returns an outcome.@selfresolves the talking NPC; one-time rewards use areward:flag.: : - Self-heal + reset hygiene. Dialogue progression is keyed to quest state, never a parallel story flag: intro/talk options gate on
QuestState(theACTIVE dialogue/convention), so a re-offered auto-accept quest's talk option is always reachable and the system has no flag-desync soft-lock class.QuestComponent.resetAllQuests()clears alldialogueFlags;resetQuest(id)clears the quest-scopedq:namespace (the implicit:* reward:*once-guards survive a single-quest reset, cleared only by a full reset).QuestComponentexposesclearDialogueFlags+questFlag;QuestCommandcarries aresetdialoguessubcommand;DialogueValidatordocuments the convention and adds anEMPTY_AFTER_RESOLVEfinding. - Dialogue template DSL + sugar. The existing
extends/paramsengine (JsonTemplateUtil) now covers dialogues:DialogueTemplateResolverresolves aDialogueTemplates/skeleton (params,nodeOverrideswith order-preservingOptionsmerge-by-index plus anOptions!wholesale-replace,extraNodes), prunesStartcandidates and options whose quest param resolved empty, and drops unreachable nodes.DialogueSugardesugars option-level shorthand (Open/Goto/Talk/Accept/TurnIn/Reward/Close/Do) into canonicalActionsbefore the codec, so the validator and executor are unchanged. NewJsonTemplateUtil.mergeKeyedMap/appendKeyedMapprimitives;DialogueTemplateAsset+ aDialogueTemplatesstore loaded beforeDialogues. - Hub-to-dialogue hook.
SpawnHubConfiggains an optionaldialoguefield (schema v3); when set,MmoNpcSpawnServicerecords the hub NPC's routing target asdialogue:andMMO_Hub.jsonswitchesOpenMmoUitoTarget: "auto"(the same placement-store resolution quest givers use). Default null keeps the hub opening its menu. - Flag storage:
QuestComponentcarries the appendedDialogueFlagsstring-set codec field (the shop-fields precedent - additive, no migration), reached through the player-data seam. TALK_TO_NPCwiring: a jar-registeredMmoQuestTalkNPC action (mirrorsActionOpenBarterShop) plus the dialogue intro option both fireTalk;NpcTalkObjectiveUtilde-dups per player+target over a 2s window.DialogueValidatorregisters adialoguecontent-audit domain.- Repeatable quests re-offer correctly across every quest surface (shared effective-state authority).
QuestStateUtil.effectiveStateis the one authority for a quest's display / acceptance state: aCOMPLETEDrepeatable quest off cooldown reads asNOT_STARTED(re-acceptable), and asON_COOLDOWN(with remaining time) while cooling down. The quest log, the NPC quest page, and the dialogueQuestStateconditions all route through it (the log already did; the NPC page and dialogue conditions read raw stored state, so a finished daily would otherwise present as a dead "Completed" with no accept option at the giver).QuestService.markCompletedis the single set-COMPLETED-plus-cooldown rule shared by the live-completion, claim, and/mmoquestadmin completepaths (so an admin complete stamps the repeatable cooldown like a manual one).QuestService.selfHeal(run on login and on opening the quest log / NPC quest page / achievements / dialogue) resets a finished off-cooldown repeatable toNOT_STARTED, with bounties excluded and deleted / retired-quest progress +COMPLETED_UNCLAIMEDleft untouched (so "no data is deleted" holds)./mmoquestadmin statusadditionally reports the effective state, cooldown remaining, and[ORPHAN]/[DISABLED]tags.
Technical: shared UI styles + dialogue page
- New mod-shipped
Common/UI/Custom/Common/MmoButtons.uiholds the ONE set of native bordered button styles (@MmoAcceptBtnStylegreen /@MmoClaimBtnStylegold /@MmoTurnInBtnStyleblue /@MmoNeutralBtnStyle/@MmoAbandonBtnStyle/@MmoOptionBtnStyle, all theSecondary.pngpatch tinted per state) plus the shared@MmoRowIconGridStyle/@MmoChipIconGridStyle. Imported per page as$M = "../Common/MmoButtons.ui"and adopted acrossBountyBoardPage/BountyRow/TokenShopPage/ShopRow/NpcQuestPage/MmoHubPage/CostChip/DialogueOptionRow- no page redefines a button style any more (DRY). DialogueOptionStylederives each option's button tint + leading glyph from its action type (AcceptQuest -> green + check, TurnInQuest -> blue + check, Goto -> chevron, Close -> X), set fromDialoguePagevia per-row.Background.Colorsub-property overrides (the proven farewell-row mechanism); the click binding stays index-based.DialoguePageadds an active-objective hint row (reusingQuestConfig.getQuestsByNpcId+ObjectiveTextRenderer) and a decorated container + fancy separator.
Technical: objective schema + zone scoping
ObjectiveTypegainsREACH_LOCATION,CONSUME_ITEM,SPEND_CURRENCY.ObjectiveSpecbecomes a 6-component record with a first-classzoneand a 5-arg delegating ctor;zoneMatches(specZone, ZoneContext)is a static case-insensitive compare against zone name OR region.ProgressEvents.fireresolves aZoneContextlazily viaZoneUtil.currentZoneonly whenZoneScopeIndex(volatile EnumSets published byQuestConfig.buildIndex+AchievementConfig.buildIndex) says some content is zone-scoped;QuestService/AchievementServicethread a@Nullable ZoneContextthrough their handlers. Serializers round-trip the authoredzoneonly.LocationTarget.parse("world:x,y,z[,radius]")(default radius 5.0);ReachLocationTickingSystemis aDelayedEntitySystem(1.0F)with inside-map edge detection and an unparseable-target cache, gated on both scope indexes.- One REACH_LEVEL target resolver, one any-skill sentinel.
TargetNameResolver.isAnySkill(*/ANY/ empty - disjoint fromisTotal) joinsisTotalas the shared sentinel authority, andSkillComponent.levelForReachTarget(target)is the single "current level for a REACH_LEVEL target" resolver (TOTAL -> summed, any -> newgetHighestSkillLevel, else the named skill).QuestServicecollapses its three inlined TOTAL-or-named branches (onLevelReached+refreshReachLevelObjectivesnow share onecompleteSatisfiedReachLevelcore;acceptQuestretro) onto it, andAchievementService(the off-thread livecheckLevelReached+ the page-openrevalidateReachLevelProgress) routes through the same sentinel / resolver.ObjectiveTextRendererrendersobjective.text.reach_level.anyfor the any-skill sentinel. Previously a*target fell through togetLevelByName("*") = 0, so the objective could never complete (and aTOTALachievement criterion silently never progressed).TargetNameResolverTestpins the sentinel contract.ConsumeItemEventSystemwalksInventoryChangeEventREMOVE transactions on the active hotbar slot, requiresItem.isConsumable(), vetoes on a same-tick add, and honors aCRAFT_VETO_MS = 250window set byCraftRecipeEventSystem.SPEND_CURRENCYfires fromCostService.takeAllOrRollback(both the all-take success path and the any-combine branch), mirroring theEARN_CURRENCYdispatch.ValidationChecks.checkObjectiveSpecsflags an unparseableREACH_LOCATIONtarget (BAD_LOCATION_TARGET).
Technical: quest-giver infrastructure + jar trim
QuestGiverDefinition(Placement SPAWN / ZONE_DISCOVERY / MANUAL) +QuestGiverConfig(quest-givers.json) +QuestGiverAsset.MmoNpcPlacementStorerecords auto-spawned givers as"markers and resolves a giver UUID back to its routing target.| " ActionOpenMmoUiresolvesTarget: "auto"from the spawned NPC's recorded target (giver:, elsehub);MmoUiRoutergainsnpcquests:andgiver:(live dialogue-or-quest-list resolution).QuestGiverSpawnServicespawns SPAWN givers beside the hub and ZONE_DISCOVERY givers fromZoneDiscoverySpawnSystem(marks before the deferredworld.executespawn, clears the marker on failure).MmoNpcCommandgainsspawn giver|givers|reset;FeatureRegistrygainsquestgivers;QuestGiverValidatoradds aquestgiverdomain.QuestDefaultstrimmed to the two tutorials;NpcQuestPageresolvesquest.localized markdown before any raw field..md.incomplete|active|complete
Technical: achievement icons + feat retirements
AchievementDefinitiongainsicon(with anICON_DISABLEDsentinel) andlegacySince;AchievementConfigparses + serializes both authored-only, adds a rootcategoryIconsmap (merged first-wins, defaults backfilled), and the meta-disable rebuild now carries icon / legacySince / requiresFeatures / chainId / tier / classRequired through.AchievementIconResolverfalls back authored -> chain-lowest-tier ->REACH_LEVELskill icon -> category -> first-reward-derived (catchingThrowable, sinceGiveCommandParserstatic-init can throw anErrorin unit JVMs) -> a generic fallback. UI rows render anItemGridicon; the unlock notification gets a 4-arg overload with anItemWithAllMetadata(quantity 0).AchievementDefaultsadds the consume / potion-drinker / spender / chatterbox chains plus aneconomysubcategory; thequest_daily/quest_weeklyfeat retirements moved to jar-bundled JSON (see the asset-driven migration below).AchievementValidatoraddsICON_MALFORMED/ICON_UNKNOWN_ITEM.- Quest achievements are asset-driven, with new legacy feats (clean-up + feature). The
quest_any/quest_daily/quest_weekly/quest_repeatablechains (20 defs) moved out ofAchievementDefaultsinto jar-bundledServer/MMOSkillTree/Achievements/Quest_*.json(byte-identical ids / triggers / qualifiers / amounts / rewards), merged additively as a pack layer over the shrinking Java defaults (the jar already declaresIncludesAssetPack). Five new legacy quest feats ship beside them (Legacy_Quest_*.json:featOfStrength+legacySince("1.4.0"), 0 points, no reward, multi-criteriaCOMPLETE_QUESTover the 16 retired one-time quest ids, criterion labels reuse the orphanedquest.keys). They unlock ONLY via.title AchievementService.backfillQuestCompletionFeatsat PlayerReady - a silent, idempotentapplySingleUnlockover the player'sCOMPLETEDquest history (the removed quests'COMPLETE_QUESTevents can never fire again), driven by the precomputedAchievementConfig.getQuestCompletionFeats()(feats whose every criterion targets a specific quest id).QuestAchievementAssetsTestguards the shipped JSON + the pure selector;AchievementDefaults14Testretargets off the migrated chains.
Technical: localization
- New jar keys (the consume / spender / chatterbox achievement chains, the
objective.text.reach_location/consume_item/spend_currencytemplates and their.anyvariants,objective.zone.suffix,ui.achievements.feat_since, and theui.dialogue.*strings) are translated into all 8 non-English locales. The new MMO Skill Quest Pack ships all nine languages (quest titles / flavor, full dialogue trees, NPC names), and the Bounty Pack's new board + contract keys are translated across all nine.LangFileIntegrityTest+DefaultContentLocalizationTeststay green; the removed built-in quests carried no per-quest lang keys (they used the sharedobjective.text.quest.*templates), so no keys were stranded.
Technical: client-side localization (display text is now resolved by the client)
The whole display layer moved from server-side string resolution to emitting Hytale Message keys the CLIENT resolves in its own locale (the kweebec-nightmare model). The engine already pushes each player the full .lang table on connect, so the server never reads, caches, or persists a player's locale - a DB-readiness prerequisite. The .lang files themselves are unchanged (they are what the client resolves).
- New
i18n/Msgfactory (tr(key, args)/key(fullKey, args)/raw/join) is the singleMessagebuilder. A localized arg (a skill / currency / item / NPC name, a nested template) is passed as a NESTEDMessageso it resolves in the viewer's locale; only data (formatted numbers, ids) is a flat param. Every name/title/desc/objective/cost/gate/reward helper grew aMessage-returning twin (Messages.*Msg,LocalizedText.resolveMsg/resolveFormatMsg,TargetNameResolver.*Msg,CostRenderer.*Msg,ObjectiveTextRenderer.renderMsg,GateReasonRenderer.renderMsg, the*TextUtil*Msg,SkillReward/Reward/PointMilestoneReward.resolve*Msg);cmd.set/sendMessage/ the feedback + toast APIs all carryMessageend to end. Key precedence (explicit vs by-convention) is chosen by a locale-independent en-US existence probe (LocalizationConfig.hasKey), never a per-player read. - Item / NPC / zone names now emit the game's native keys (
server.items.,.name items.,.name npcs.,.name server.map.zone.), so they render in the client's own locale instead of a server-side canonical lookup. - Deleted (DB-readiness + dead-code cleanup): the persisted
SkillComponent.languagefield + its"Language"codec entry (legacy saves load clean - the codec ignores the orphaned key, no migration); the seam locale projection (PlayerDataRepository.language(id)/language(ref)/setLanguage, thelocaleByUuid/localeByRefHashcaches,PlayerSession.language()/tr()/msg()); the in-MMO language picker (LanguagePickerUtil.applyPlayerLanguageand the Onboarding + Settings dropdowns /LangButton.ui);Pluralizer(English-only runtime pluralization cannot move client-side, so objective lines read with the singular name, e.g. "Mine 50 Stone"); and every server-side String display helper now that the*Msgtwins replace them.LocalizationConfig/Messages.getForLangsurvive only for the residual default-language sinks. - The two String-only sinks stay default-language (no per-player read): floating combat text (
CombatTextUpdate.text) and item-icon hover tooltips (ItemGridSlot.setName) resolve a default-language string (or flatten aMessageviaMessageUtil.formatMessageToPlainString). The admin "default server language" config is retained as the residual fallback knob. - Tests:
PluralizerTestremoved;PlayerDataRepositoryContractTest/FakePlayerDataRepositorydrop the locale projection;TargetNameResolverTest/DefaultContentLocalizationTestassert via theMessagetwins (flattened to en-US in the unit JVM). Fullgradlew build+ suite green.
Technical: engine hardening (pre-content)
A pre-content pass that future-proofs the quest / dialogue / objective / reward engine before heavy pack authoring locks the asset schemas and save formats. Driven by a multi-agent extensibility + robustness audit (digest: .claude/research/1-4-0-engine-hardening-audit.md).
- Quest authoring consolidated to one path. The editor-native
StructuredQuests/asset store (added earlier this cycle, never shipping any content) is removed, so a quest is authored one way: a templatedQuests/entry. The typed quest body codec (QuestDefinition.CODEC, PascalCase, including the recursivePrerequisiteGroupgate) is retained as the single quest schema authority - the decode target the templated path folds onto and the machine-readable schema for a future in-game editor / docs site. This removes the dual-parser drift surface (a quest field no longer has to be wired in two places policed by a parity test). COMPLETE_QUESTandBOUNTY_STREAKwork as quest objective types. Both were achievement-only; they now have a real quest producer (a completed quest, and the daily bounty streak), fired quests-only so achievements are not double-counted.BOUNTY_STREAKis a value objective:ObjectiveType.isValueBased()routes it through a new high-waterQuestObjectiveProgress.applyValue(a streak of 5 then 4 stays 5, never sums), distinct from the accumulatingadvancepath. A new closedObjectiveType.isQuestProducible()set backs a/mmoconfig validateERROR (NO_QUEST_PRODUCER) so a future objective type added without a producer is caught before it ships a dead objective.- Save-format robustness.
Objective.hasReservedDelimiteris the one authority for the characters the per-player save serialization reserves (|,=,:,,); the structured quest + objective load paths now reject a delimiter-bearing id like the templated path already did, so a content id can never corruptquestStates/ progress on round-trip.QuestComponent.dialogueFlagsserializes through a self-describing base64-per-element format, so aSetFlagvalue or thereward:once-guard containing a: : |round-trips as one flag instead of splitting (which would re-enable a once-only dialogue reward). Objective progress is keyed by stable authored objective id (not position), so editing a live quest's objectives self-heals. - Validation hardening.
/mmoconfig validatenow cross-checks a quest's gate references against the live registries: unknownrequiresFeatures(an ERROR-severity permanent hide), and unknown quest / achievement / mastery-node / currency leaf ids in the recursive prerequisite group +resetsOnComplete(warnings). The deadUNKNOWN_SKILL_IDcheck is revived (validators now passisKnownSkill, notisSkillAvailable, which returned true for any typo), and a magnitude objective amount above the int-bounded quest progress range warns.DialogueValidatorflags reserved-delimiter dialogue / node ids +SetFlagvalues. - First engine unit-test suite for the stateful runtime. New tests pin the previously-untested core:
QuestComponentcodec round-trip (incl. the delimiter-safe flag format),QuestObjectiveProgress.advance/applyValue,QuestStateUtil.effectiveStaterepeatable-cooldown re-offer (the soft-lock bug class),QuestServicestate machine +classifyQuestTypeboundaries,PlayerQuestDataprogress round-trip, thePendingRewardStoreoffline spool, and theDialogueActionExecutor@selfresolution + once-guard key.PendingRewardStorelogging is now guarded for unit JVMs (mirrorsDialogueConfig). - Quests unified onto the codec (Pattern B); the hand parser is gone. Building on the store removal above,
QuestConfig.parseQuestno longer hand-builds the model: it normalizes the raw body (camelCase + the polymorphic legacy shapes - array|object prerequisites, object|array requirements, therequiresSkillsmap,tags->QuestTags) to canonical PascalCase, then decodes throughQuestDefinition.CODECand assembles viaDraft.toDefinition(id), exactly asDialogueConfig.decodeBodydoes for dialogue. The ~225-line hand parser plusparseObjective/parseReward/parsePrerequisiteGroupare deleted; the codec is the one place a quest field is wired. Behavior parity (legacy vs codec over the shipped corpus) was proven by a temporary gate, then locked by the permanentQuestCodecDecodeTest. The reframed mandate: one schema authority per type expressed as a codec, the template DSL a pre-parse rewrite before it, never a twin store beside it (rootCLAUDE.md). util.SafeLog- one unit-JVM-safe logging facade.MMOSkillTreePlugin.LOGGERthrowsNoClassDefFoundErrorin a log-manager-less unit JVM (anErrorthat escapescatch (Exception), which the new no-objectives decode path tripped); the four ad-hoc per-class guards (QuestConfig.warn,RewardParser.warn,DialogueConfig.WARN_LOGGER,PendingRewardStore.safeLog) collapse intoSafeLog.warn/info/severe/fine. Parse / validate / load paths a unit test can reach log through it; the convention is documented in rootCLAUDE.md.
Technical: objective text auto-generation (verb-by-material)
ObjectiveTextRendererderives theBREAK_BLOCKverb from the block's gathering skill (MINING -> Mine, WOODCUTTING -> Chop, EXCAVATION -> Dig, HARVESTING -> Harvest), resolved by the SAME longest-pattern xp-map match the XP award uses:SkillService.processTrigger's match loop is extracted into a pure, side-effect-freeSkillService.bestTriggerMatch(identifier, triggerType)shared by both, so the rendered verb can never disagree with the skill the block trains. Falls back to the neutralobjective.text.break_block("Break {0} {1}") when no gathering skill resolves. New jar keysobjective.text.break_block.{mine,chop,dig,harvest}across all 9 locales.- Pack quests stop hand-authoring objective text.
TALK_TO_NPCalready auto-renders "Talk to {1}" via the existing convention plus the giver-id name keys the Quest Pack ships innpcs.lang; with verb-by-material covering block breaking and the zone suffix auto-appended, the Quest Pack's objectives drop their per-objectivetextKeys and the taming / mastery quests drop their raw EnglishdisplayText, so objectives read naturally AND localize in every language. The half-wired genericobjective.text.quest.*verb vocabulary (16 keys: mine / chop / slay / place_blocks / catch_fish_any / ...) that the renderer now auto-generates or that was unused is removed from all 9 locales; onlyquest.break_any(the tutorial-quest line) stays. Engine vocabulary lives in the jar (schema); only genuinely per-quest narrative (e.g. "Eat a hearty meal") stays in a pack.
Technical: display-text auto-generation (rewards, abilities, command rewards, mastery)
- Skill-tree reward text auto-generates; the ~986 hardcoded literals are gone.
SkillRewardstores authored display text only (null when absent);resolveName/resolveDescriptionroute through the oneMessagesgenerator (explicit key, then authored literal, then auto-gen from reward type + value), and the fragile English-string-match heuristic that defended against baked English deletes itself. The serializer round-trips authored fields only - a built-in reward writes nodisplayName/description, closing the invariant-#3 leak that baked derived English into saved override configs. Also fixes a latent FLAT_* description bug (flat magnitudes were x100'd as a percent) and guards empty target/skill names so an auto-gen line never renders "()" / "with ". Newreward.*/reward.desc.*keys for the flat / dodge / ability-mod types in all 9 locales;DefaultContentLocalizationTestnow asserts no built-in reward carries raw display text and the serializer never bakes it. - MAGIC and ARTILLERY are now true built-in skills. They were "built-in custom skills" whose
skill.name could resolve empty in a server-less context; promoted intoBUILTIN_SKILL_DATA/BUILTIN_SKILL_NAMES(mirroring ENCHANTING) so the name always resolves and no user custom skill can shadow them. Player XP migrates transparently - the unifiedxpMapis string-keyed, so an old save's data loads from theCustomXpblob and re-saves under the named key.getSkillNameForLangByIdalso falls back to the static registry display name, so a built-in skill name can never render empty. - Ability flavor moves onto keys. All 73 ability
descriptionliterals are stripped; flavor resolves throughability.(33 capstone / promoted-passive abilities that had no key get one, migrated from the description with em-dashes normalized to " - "). Display names stay as the tolerated raw fallback (already mirrored by.flavor ability.)..name - Command-reward cards render through the seams. Boost rewards drop the
displayNamethat baked balance data ("2x All XP (45m)"); the card renders the data-free localized boost line, the Global scope restored (aglobalflag onBoostCommand.BoostGive+reward.line.boost_global*keys) and a generic per-skill line for the{skill}-placeholder boost. Item rewards drop the hand-derived English names plus the 141 danglingreward.keys and resolve through the canonical item-name seam;.levelNN formatItemNameis deleted from bothCommandRewardsDefaultsandItemRewardsPage. - Mastery node effect lines cover every modifier. The mastery page already auto-renders a node's effect + cost; mapping the last two unmapped modifier paramKeys (
iframesDurationMs-> "I-frame Duration",durationMs-> "Duration") makes every pack node render a clean localized effect line, so the hardcoded balance-data node descriptions (stripped in the skill-mastery-pack) are fully redundant. - Command-reward defaults stop baking English; the XP-token milestone reward is keyed.
CommandRewardEntrygainsgetAuthoredDisplayName()/getAuthoredDescription()(null when blank) and the serializer (RewardData.fromEntry) round-trips those, closing the invariant-#3 leak that baked a derived English name into the_reference/defaults-command-rewards.jsondump. The per-skill XP-token milestone reward carries areward.command.xp_tokenkey instead of a hardcoded literal (its{skill}-template item id cannot resolve a concrete name in the skill-agnostic milestone preview, and the granted token keeps its own localized inventory name), and the deadgiveBoostTokenhelper is deleted. - Custom-skill names + descriptions round-trip authored-only.
CustomSkill.getAuthoredDisplayName()nulls the id-cased placeholder the loader synthesizes (FORAGING->Foraging), andCustomSkillsConfigserializes the authored accessors, socustom-skills.jsonround-trips owner text only, never the machine placeholder (invariant #3); player display already resolves theskill./skill.convention keys..desc - The last raw display getters route through their seams. The achievement detail page's reward-row fallback resolves through
Reward.resolveName(lang), and the/quest status//playerquest statusadmin lines render objective text throughObjectiveTextRenderer(localized to the target's language) instead of the raw machinegetDisplayText(). - Taming Pack quests + achievements localize. The pack's six quests and nineteen achievements drop their raw English
displayName/descriptionforquest.and.title|flavor achievement.convention keys across all nine languages; a description that only restated the goal is dropped (the objective / criterion line auto-renders the count), keeping the lang values data-free flavor (invariant #6)..title|desc - 46 new English keys translated into all 8 non-English locales (the capstone ability flavors plus the reward / boost-line / mastery-noun keys).
- The remaining new keys are translated into all 8 non-English locales too: the jar's
reward.command.xp_token+points.milestone.*rank titles, the Taming Pack's quest / achievement keys, and the Mastery Pack'smastery.mag_archmagus.title.LangFileIntegrityTeststays green.
Technical: hub onboarding arc
- Jar-resident hub intro dialogue. New jar-bundled
Server/MMOSkillTree/Dialogues/Mmo_Hub_Intro.json;SpawnHubConfigdefaults the hub dialogue tommo_hub_intro, so the Adventurer's Guide opens the tutorial-or-skip conversation with no pack installed.getting_startednow setsautoClaimRewards, so breaking the first block reachesCOMPLETED(notCOMPLETED_UNCLAIMED) - that both auto-grants the Skill Menu Scroll and satisfies the wilds-campaign prerequisite gate (isQuestCompleted). - Two new author-extensible dialogue actions.
CompleteQuestforce-completes a quest and grants its rewards (the skip-tutorial path), andRunCommandruns a console command from a dialogue option; both register through the same action registry as the existing dialogue actions, so a pack can author them. A sharedQuestService.completehelper is the one set-COMPLETED-plus-grant authority behind bothCompleteQuestand the/mmoquestadmin completecommand. - Jar tutorial links to the pack campaign. The Quest Pack's
wilds_meet_the_guidegainsprerequisites:[getting_started], so finishing OR skipping the jar intro tutorial opens the Emerald Wilds campaign. - Quest hand-in opens the giver's dialogue. A quest can name an optional
CompletionDialogue(a newQuestDefinitionfield /CompletionDialoguecodec key); when it completes at the NPC quest view,NpcQuestPageopens that dialogue instead of reopening the quest list, so the Adventurer's Guide reacts on hand-in (the quest-complete toast carries over via the shared per-playerToastStore). The starter-tools tutorial (craft_starter_tools) sets it tommo_hub_intro, whose new quest-state-routedtools_donenode congratulates the player on their first tools when there is no bounty to pitch (no pack / empty boards).
Technical: cross-mod DRY pass (behavior-preserving)
A behavior-preserving refactor that lifts shared primitives into ziggfreed-common, deletes dead/orphan asset types, and collapses the duplicated pack-layer fold scaffolding. Full session summary: .claude/plans/1-4-0-dry-pass-and-hub-onboarding-arc.md.
- Shared pure-JSON template primitives in ziggfreed-common. New
util/JsonTreeUtil(deepClone/substituteParamsInPlace/substituteString/deepMergeInto) is the single home for the four template operations both the common dialogue resolver and the MMO content resolvers needed; common'sJsonTemplateUtiland the MMO'sconfig/template/JsonTemplateUtilboth delegate to it. The MMO'sJsonTemplateUtildead dialogue-merge methods (mergeKeyedMap/appendKeyedMap/OptionMode/mergeOptions) and the deadJsonTemplateUtilMergeTestare removed. Newinventory/InventorySectionsdedups the six section-id list shared byInventorySnapshot+InventoryStripPolicy, with a newInventoryStripPolicyTest. - Shared asset infra. The MMO's duplicate
asset/type/AbstractRawJsonAssetis deleted - its 25 subclasses now extendcom.ziggfreed.common.asset.AbstractRawJsonAsset, and the MMO's privateregisterStoredelegates to common'sAssetStoreRegistrar.registerStore. - Removed three dead/orphan asset types.
AbilityAsset,XpTokenAsset, andSkillTreeNodeAssethad a CODEC but no store / config / handler; removed along with theirPackContentTypekeys (SKILL_TREE/ABILITIES/XP_TOKENS) and theirPackControlAssetfields / codec-keys / case-branches. - One data-driven store table.
asset/AssetStoreRegistrarcollapses its two hand-synced parallel lists (CONTENT_CLASSES+ the 26registerStore/ listener blocks) into ONEStoreSpectable; adding a content type is now one table row, removing the parallel-list drift that produced the orphan types above. - Shared pack-layer fold base. New
config/AbstractPackLayeredOverrideConfigowns thedefaults < pack < ownerfold scaffolding the pack-aware configs all duplicated (thepackLayer/packReplacefields,mergePackLayer,shouldDropDefaults, and a templateapplyPackLayerwith afoldEntryhook).CurrencyConfig/BountyBoardConfig/ClassesConfig/MasteryConfigmigrate fully;TokenShopConfig/DialogueConfigmigrate fields-only (they keep their ownapplyPackLayerfor an extra template layer). The pattern-map family (Luck / ItemRequirements / ActionRequirements / MobKillXp / XpMaps / BoostTemplate) andQuestGiverConfigstay explicit (divergent fold / field-type).
Technical: per-world rules + multi-world tick partition
- Multi-world tick partition. The six singleton ability tick services (DOT / HoT / ground-zone / beam / projectile / colored-combat-text) move from one global
ConcurrentLinkedQueueto a per-worldConcurrentHashMap, routed at enqueue by the relevant ref's world (DOT/HoT by target, beam by caster, projectile by creator, colored text by viewer; position-anchored ground zones take an explicitWorld). World identity is the engine-stableRef.getStore() -> EntityStore.getWorld(). Newworld/WorldScopemaps each liveWorldto aWorldRuntimecarrying ONE per-worldcurrentTimeMillisframe stamp (replacing the six global CAS stamps, so two worlds ticking in the same millisecond no longer steal each other's drain).AbilityDotTickingSystemgates once per world per frame viaWorldRuntime.beginFrame()then drains each service for that world; the multi-world DOT regression is fixed by construction (a world only drains its own bucket, so a DOT applied in a raid instance can't be deleted by the overworld's pass). Each service registers aWorldScopeevictor in its constructor and aRemoveWorldEventlistener drops a removed world's buckets + cached rules (uncancellable even on a crashed world thread, so the map only holds live worlds). Ground-zone / beam / projectile damage now queues through theCommandBuffer(the engine gather phase, matching its ownFallDamage/OutOfWorldtick systems); the recursive mark-detonate stays on the live store (it runs in the damage-event group, not the tick lock). Disconnect cleanup (clearMatching/clearForPlayer, a UUID only) sweeps every world bucket. - Per-world rules config. New
world/WorldRules(immutable:xpMultiplier, a combinedabilitiesEnabledgate, a disabled-skills set) +WorldRulesMatcher(exact > longest trailing-*prefix > bare*> unrestricted default, case-insensitive) +WorldRulesValidator(registered withContentAuditasworld_rules).WorldRulesAssetis a Pattern A structured codec (PascalCaseMatch/XpMultiplier/AbilitiesEnabled/DisabledSkills);WorldRulesConfigextendsAbstractOverrideConfigand foldsdefaults < pack < owner, resolving oneWorldRulesper world cached onWorldRuntime(anonRulesChangedcallback drops the cache on reload / hot pack re-import). The jar ships an unrestrictedServer/MMOSkillTree/WorldRules/Default.json(Match: "*") as an owner-overridable anchor. Gates:SkillService.addXpByName(multiplier + disabled-skills, after the class multiplier, before the boost pipeline) plus the flat-XP-token bypass inXpTokenConsumeInteraction(with a zero-guard so a fractional multiplier can't burn a token for 0 XP);ActiveAbilityService.tryActivate(reuses theDISABLEDresult as a backstop) plus the cast entry pointsMmoAbilityCastInteraction/HotbarAbilityPacketFilter(the combined flag is checked there so a disabled-abilities world makes the binding behave like a plain item - no per-keypress toast - mirroring the global toggle);AbilityHudTickingSystem+QuestHudTickingSystem/QuestTrackerHud(the one combined flag hides both HUDs via the existing invisible-update path).CombatBonusesEnabledgates the MMO weapon-combat layer with an early-return inCombatDamageEventSystem.applyAttackerEffects/applyLifesteal/applyDefenseReduction/applyFallDamageReduction(vanilla base damage when off). Deferred: the single-passCombatStatBreakdowncombat-getter consolidation (perf-only, separate verified pass). Full design + adversarial review:.claude/plans/1-4-0-per-world-rules-and-tick-partition.md. - World-rule skill allow-list + Kweebec Nightmare default. Adds an optional
EnabledSkillsallow-list toWorldRules/WorldRulesAsset: when non-empty, ONLY those skill ids gain XP in a matching world (every other skill is treated as disabled). It folds into the existingWorldRules.isSkillDisabled, so it rides the same XP gates (SkillService.addXpByName+ the flat-token bypass) with no new read site;WorldRulesValidatorflags unknown allow-list ids and an enabled/disabled conflict. The jar shipsServer/MMOSkillTree/WorldRules/KweebecNightmare.json(Match: "KweebecNightmare_*",EnabledSkills: ["ARTILLERY","DEFENSE"],AbilitiesEnabled:false,CombatBonusesEnabled:false) so the Kweebec Nightmare minigame instances grant only Artillery + Defense XP with no MMO abilities or skill-tree combat passives; dormant on servers without that world.
v1.3.2
A reliability fix for command rewards and admin commands that target a player by name. XP and currency rewards now reach players who are online in a different world or fully offline, instead of being silently dropped.
Bug Fixes
- Command rewards no longer vanish across worlds or while a player is offline. XP (
mmoawardxp), currency (mmocurrency), and the player-targeting admin commands (mastery, class, achievement) resolved the target in only one world, so a reward for a player online in a *different* world, or offline, logged "Player not found" and was dropped. Targets are now found in any world, and XP / counter-currency grants are written to an offline player's saved data so they apply on next login (item-backed currencies still require the player online). Quest, achievement, and shop rewards that dispatch these commands inherit the fix.
Technical: cross-world + offline player resolution
- New
PlayerLookupUtil.runForTarget(name, domain, onOnline, onOffline, onNotFound)is the single resolver for player-targeting commands: it finds the player universe-wide via the engine'sUniverse.getPlayerByUsername(exact, case-insensitive, so a name prefix never resolves the wrong player) and runs the rich action on the target's own world thread, else writes to their persisted data offline, else reports not found.runForOnlineTargetis the online-only half, used by mastery/class/achievement whose grants need the live entity. - Offline writes route through the DB-ready player-data seam:
PlayerDataRepository.updateOffline(PlayerId, domain, work)now performs a real UUID-keyed read-modify-write of the offlinevia Hytale's.json PlayerStorage.updateunder the ECS backend (online players still mutate the live component). A new UUID-keyedPlayerDirectory(persistedplayer-directory.json, populated on connect, refreshing the username on external rename) resolves an offline player's name to their stable UUID, implementing the seam's newresolveId.CurrencyServicegainsSkillComponent-based counter give/take/set for the offline path. - Cleanup: four linear "find player by UUID" scans (
AchievementService, the taming/fishing/anglers integration adapters) now use the O(1)Universe.getPlayer(uuid). - Known follow-ups: boost-token cross-world delivery still uses the existing username-keyed pending-token queue (tokens are delivered, but a cross-world online player receives them on next login rather than immediately); mastery/class/achievement offline writes are not yet supported (their grants apply live only).
v1.3.1
A small follow-up to 1.3.0. The Adventurer's Guide now only auto-spawns in worlds you choose (defaulting to your main world), and the in-menu Help button is bigger, clearer, and reachable from more places.
Updates
- The Adventurer's Guide is world-scoped. The guide NPC used to auto-spawn once in every world a player joined, which dropped it into creative, build, and minigame worlds on multi-world servers. It now spawns only in the worlds listed in the new
worldsfield ofspawn-hub.json, which defaults to justdefault. Add more world names to the list, use a single"*"entry to spawn it everywhere (the previous behavior), or leave the list empty to disable the auto-spawn and place the guide yourself. - A clearer, shared Help button. The Adventurer's Guide's header button is bigger, reads "? Help", and sits at the top-right of the menu. The same button now also appears on the left panel of the Skills menu, so every player can re-open the guided tour from there too. The first-time welcome step previews the button right under its description.
- The admin command reference is now "Admin Help". The Skills menu's admin-only help tab (which opens the command reference) was relabeled from "? Help" to "Admin Help" so it no longer shares a name with the new all-players Help button.
Bug Fixes
- Fixed a client crash ("Selected element in CustomUI command was not found. Selector: #MmoToast.Anchor") when claiming a reward and then leaving the menu within a few seconds. An in-menu toast schedules a hide ~4s after it shows; if the player had navigated to another page (or closed the menu) in the meantime, the hide was pushed to whatever page was now open, and any page without the toast element disconnected the client.
ToastablePagenow marks itself dismissed via theonDismisslifecycle hook and skips the stale push (the toast state still clears, so the next page opens clean). Most visible on the per-skill / total-level command-reward pages, but it covered every in-menu toast (claims, purchases, rerolls). - The first-time guided tour no longer shows the Ability Mastery or Currencies steps when there is no content for them. They were gated on the feature being enabled but not on any tracks or currencies actually being configured, so a server with the feature on but nothing set up would still see an empty step. They now follow the same "enabled AND has content" rule the Bounty Board and Token Shop steps already use.
Technical: world-scoped spawn + shared Help button
SpawnHubConfiggains aworldslist (default["default"]) andshouldSpawnInWorld(name)(a"*"entry matches all worlds; an empty list matches none).SCHEMA_VERSIONbumped to 2; the field is additive and unknown-key-tolerant, so existingspawn-hub.jsonfiles keep the["default"]default on upgrade.MmoNpcSpawnService.maybeAutoSpawnHubshort-circuits when!cfg.shouldSpawnInWorld(world.getName()), before the once-per-world tracker check. The gate covers the auto-spawn only;/mmonpc spawnand the in-world blocks are unaffected.- The Help button is a single shared
Common/UI/Custom/Pages/HelpButton.uifragment pluspages/render/HelpButtonUtil(installbinds it to theopenTutorialaction;previewrenders the same pill statically). It is reused byMmoHubPage(header, right-aligned, replacing the old inline 24x24 "?" glyph),ViewXpPage(left panel, all players, above the admin block), andOnboardingPage(step-1 static preview). Each host declares an empty slot Group and handlesopenTutorialby openingOnboardingPage. - New i18n keys (all 9 languages):
ui.hub.help_button(the shared pill label) andui.onboarding.welcome.help_demo(the step-1 caption). The Skills menu's admin help tab now reads from a newui.admin.help_tab("Admin Help") instead of the sharedui.admin.help, leaving the AdminConfigPage help button unchanged. OnboardingTopicsmastery/currency predicates now callMasteryConfig.isAvailable()/CurrencyConfig.isAvailable()(enabled AND content present) instead ofFeatureRegistry.isFeatureAvailable(...)(enabled only), matching how the bounty/shop topics already gate on content.
v1.3.0
A game-feel and content update. Critical hits and level-ups now show colored floating text (red CRIT!, gold LV-up), routed through a unified feedback layer that can also play sounds and optional screen effects; a new Adventurer's Guide hub NPC stands at world spawn and introduces the mod with a first-time guided overview; a new rotating Bounty Board offers daily and weekly contracts across many task types (including deliver-style turn-in contracts), gates its harder contracts behind combat progression, pays out in Bounty Tokens you can spend at a new Token Shop, and has its own achievements and a two-panel board UI; quest and bounty objectives read as clean, localized sentences; plus a new Items Picked Up stat in the Statistics view and behind-the-scenes cleanup.
Updates
- New: colored floating text. Land a critical hit and a red "CRIT!" flashes on the target; level a skill and a gold "
LV " floats above your character. Both are on by default, colored, and fully localized into every supported language. XP gains show in the corner notification feed (they no longer float above mobs). Server owners can turn the feature off, or toggle crits and level-ups individually, in the combat-text.jsonconfig. - Critical-hit corner notifications now default off. The red floating "CRIT!" already marks every crit, so the separate corner notification ships disabled. The new default reaches existing players too, not just fresh saves; anyone who wants both back can re-enable the toggle in Settings and the choice sticks.
- New: the Adventurer's Guide hub. The mod spawns a guide NPC at world spawn automatically, once per world (relocate or disable via
spawn-hub.json). Talking to it opens a two-panel hub: a grid of icon tiles for your skills, quests, achievements, abilities, mastery, and boosts on the left - joined by tiles for every available bounty board and the Token Shop when those are installed - with the main board's active bounties previewed on the right, so you can see your live contracts and jump anywhere from one place. The hub is feature-aware and only lists what the server has enabled. - New: a guided mod overview. The first time you talk to the Adventurer's Guide, a short walkthrough introduces the whole mod - skills and leveling, the skill tree, quests and achievements, abilities, mastery, currencies, bounties, the shop, and more - before dropping you at the hub. Each step shows a faithful preview of the matching screen built from your real data - your actual skill cards on the skills steps, real example perk and ability cards on the tree and abilities steps, your server's real currencies and balances in the wallet, your live quests, achievements, and bounty rotation with real progress, and real Token Shop offers and mastery nodes with their prices drawn as the same affordability-colored cost chips the real pages use - so you know what to look for, teaches you the /xp command (and /skills) for opening your skills menu, and gets you a Skills Menu Scroll to keep on your hotbar - you earn it from the "break a block" starter quest, or claim it right in the walkthrough on servers that have quests turned off. It only covers the features your server actually has turned on, shows once per player automatically, and you can re-open it any time with the "?" guide button, now tucked into the window header.
- New: Bounty Boards. A rotating board of contracts you visit in the world. Kill, gather, train-a-skill, and turn-in (delivery) tasks refresh on a daily or weekly schedule and pay Bounty Tokens plus skill XP. Every player sees the same board each rotation; accept the ones you want, finish them anywhere, and they auto-claim. You reach a board by interacting with a placed Bounty Board poster or from the Adventurer's Guide hub (bounties appear as options on it once the pack is installed), never a command. Each board opens a two-panel view: the contract list on the left, and on the right each contract's full objective list with live progress, reward icons, difficulty, an Accept button, and a header tracking your lifetime bounties-completed count and Bounty Token balance, plus a per-contract reroll - swap a single contract you don't want for another, keeping the rest, up to a daily cap (the pack allows 3 rerolls/day on the daily board, 2/week on the weekly). Reroll results and the cap persist on the player across relogs and restarts, and a slot never re-offers something you already rolled away that period (no A to B and back to A); the same guarantees back the Token Shop's featured rerolls. Harder contracts ask for a minimum combat level (the pack's boards use level 25 for Normal and 60 for Hard) and show as Locked until you train up, and completing bounties earns its own achievement group: a Bounty Hunter ladder up to 1,000, a Hard Target chain, a "First Contract" server-first, and an On a Roll daily streak. Run several boards in one world (each its own block), and turn the whole feature on or off from the in-game admin menu. The boards, contracts, blocks, and NPCs ship in the free MMO Skill Bounty Pack; install it to switch bounties on, or author your own contracts and boards as standard content-pack assets.
- New: the Token Shop. A place to spend the Bounty Tokens you earn. Visit a placed Token Trader poster or NPC to open a two-panel shop of XP boosts, item caches delivered to your inventory, and currency conversions, each reward shown with its amount. Offers can be gated by combat level or capped per day, a rotating Featured set (with a per-offer paid reroll, capped per day) sits on top of the always-available catalog, and locked offers explain why inline. Buying an offer plays a coin-spend sound. It has its own admin on/off toggle. The catalog, Featured pool, and Token Trader block ship in the same free MMO Skill Bounty Pack.
- Bounties in your quest log; prices as coins. Accepted Bounty Board contracts appear in your quest log by their real name with a simple Bounty + difficulty tag, where the log just tracks their progress (you accept, finish, and hand them in at the board). On the shop and board, each price shows as a coin icon + amount beside the Buy / Reroll buttons, so a long price or a "no rerolls left" note is never cut off, and each board contract shows its reward icon.
- New: per-skill XP exchanges. The Token Shop's XP Exchange offers a rotating set of single-skill XP packets covering every skill on your server (a few featured each day, rerollable), alongside the always-available all-skills bundle. Each exchange costs both Bounty Tokens and Life Essence and is capped per day, so it's a steady sink rather than a shortcut.
- New: unified feedback layer. Crit, finisher, kill-XP, level-up, reward, and shop-purchase moments route through one system that pairs the floating text (colored, where shown) with an optional sound and an (off by default) camera effect, configurable per event in the new
feedback.json. Combat sounds ship off by default (those events fire often), so nothing changes unless you turn them on. - Clearer, pluralized objective text. Quest and bounty objectives that don't set custom text read as proper sentences ("Defeat 12 Zombies", "Mine 25 Iron Ore", "Reach total level 50") in every supported language, instead of a raw "KILL_ENTITY Zombie x10". Creature and item nouns pluralize with the count ("12 Zombies", "6 Wolves"), while material nouns stay natural ("100 Stone", "25 Iron Ore"). The built-in quests' authored objective lines join in too: text that used to be English in every language ("Dig 200 soil blocks", "Slay 10 Skeletons") now reads localized with its authored verb kept ("Creusez 200 blocs de terre"), and objectives with no specific target read naturally instead of leaving a gap ("Defeat 100 enemies"). Achievement criteria rows get the same treatment (they were English in every language), the achievement detail view's CRITERIA and CHAIN TIERS headers are translated, and the quest log's tag pills and tag filter ("Beginner", "Chain", "Mining") now display in your language - skill-named tags reuse the skill's own translation.
- New: Items Picked Up stat. The Statistics view's Gathering section now tracks your lifetime Items Picked Up total alongside blocks broken and fish caught.
- Achievement rewards are claimed from the detail view. The Claim button on the achievements list and summary pages now opens that achievement's detail page, where its full reward breakdown is shown and you claim from there, instead of claiming instantly from the list.
- Rebalanced achievement XP and point milestones. Achievement-point milestones now take more points to reach (500 / 3,000 / 7,500 / 14,000 / 24,000, up from 300 / 1,500 / 5,000 / 12,000 / 22,000) and grant far less flat XP. The flat XP on the milestones, on the top achievements (legendary metas, the per-skill "legendary"/level-100 caps, the quest and bounty chain caps), and especially on the server-first achievements was cut sharply. The root issue: an "all skills" XP reward is granted once per skill, so the old Diamond milestone's "2,000,000 all skills" was really ~42,000,000 XP (and the players who earn the top tiers are already near-maxed, so most of it was wasted overflow). Server-first rewards in particular are now a prestige boost plus a token amount of XP rather than a game-warping dump that snowballs whoever is already winning. Boost tokens, announcements, and achievement point values are unchanged.
- Polished shop and board UI. Every price now renders as compact coin chips (icon + amount, colored by what you can afford) in both the flat catalog and the per-skill exchange, with a remaining-stock badge ("3 left", "None left today", "Sold out") right on capped offers. Status dots share one color language across the shop and the bounty board, including a new purple "limit reached" state. Paid rerolls now ask for a second click to confirm (the button flips to "Confirm?" and a banner shows the price; free rerolls stay one-click), an offer or contract that rotates out while you had it open says so instead of silently doing nothing, and an empty category tab explains itself instead of claiming the whole shop is empty. The rotating shelf's list header can carry the pool's own localized name (the XP Exchange's reads "Today's XP Deals") and gives its refresh countdown a roomier, more readable line; the detail panel's cost chips sit left-aligned under COST, and the reroll price chip stays inside its own box beside the Reroll button instead of sliding under it.
- Purchases can no longer eat your tokens. Buying from the shop is now a guarded transaction: if a reward fails to deliver mid-purchase it is queued and arrives on your next login (the toast says so), and if nothing could be delivered at all your payment is refunded automatically.
- New: in-menu toast banners. Action feedback you trigger inside a menu - accepting or rerolling a bounty, claiming a quest or achievement reward, buying from the Token Shop, turning in a delivery, binding an ability, activating a boost - now appears as a colored banner across the top of the open menu (red for a problem, green for success) and clears itself after a few seconds. Reward toasts list each granted reward as an icon + text row (the same icon and wording the reward panels show); a skill-tree claim's headline names the skill ("Mining reward claimed") with the reward as its row below. The Ability Mastery page's purchase, deposit, and refund actions confirm with these toasts too (localized, kind-colored), replacing its old footer status line. Previously this feedback went to the corner notification feed, which the open menu covered, so you often couldn't see why an action failed. Notifications outside of menus are unchanged.
Bug Fixes
- Fixed the turn-in bounty objective reading "Turn in 250 Essence of Lives": the English pluralizer inflected the last word of an "of" phrase (the irregular life/lives rule), so "X of Y" names now inflect their head noun instead ("Loaves of Bread") and a mass-noun head stays put ("Essence of Life"). Also fixed objective target names being English-pluralized in every language - the objective renderer was missing the English-only guard the cost renderer already had, so translated names could get an English "-s" bolted on; non-English locales now show the translator's canonical form.
- Fixed multi-currency mastery nodes being able to half-charge: when an ALL-combine cost's second currency failed mid-purchase, the first currency was kept without the node unlocking. Drains now roll back everything already taken.
- Quest, bounty, and achievement reward payouts no longer abort mid-list when one reward fails - the failing reward is queued for next login and the rest still deliver (previously everything after the failure was silently lost).
- Menu category tabs (Gathering / Combat / Crafting / Misc) now hide consistently across the Skills, Boosts, and Quests pages when you have no available skills in that category.
- Fixed Defense skill XP token rewards silently granting nothing. Several built-in quests, an achievement-points milestone, and the bundled config presets handed out a "Defense XP" token using an item id that does not exist (the Defense skill's tokens are the "Block XP" item family), so the reward gave nothing; they now deliver the correct tokens.
- Fixed the server-wide broadcast for major achievement unlocks (Crown of the Realm and the rest) rendering garbled. The announcement text was missing a closing quote and never filled in the achievement name; broadcasts are now well-formed and name the achievement.
- Fixed the "reach max level in every skill" achievements (Crown of the Realm, Sovereign of Skills, Master of All) being impossible to earn on servers without the Taming skill. Skills disabled on a server (such as Taming when Alec's Animal Husbandry is not installed) are now excluded from those milestones and no longer generate their own unreachable per-skill leveling achievements.
- Fixed command help and docs showing positional syntax that Hytale's parser rejects. Optional/default command arguments only bind as
--name=valueflags; an extra bare token fails the whole command (or, for/give, is silently dropped). Every usage message and help key that still showed the old positional form -/mmoboost give Steve|mining|2|30,/quest accept,/mmoquestadmin give,| /mmopacks export,/mmoability bind <1|2|3>,/mmonpc spawn board,/mmoconfig basexpand friends - now shows the working named-arg form (--args=,--questId=,--action=,--arg1=, ...), across all 9 languages and the docs.
Technical: colored floating combat text
- The native channel is single-colour. Floating combat text reuses Hytale's entity-UI combat-text channel (build a
CombatTextUpdate, queue it on a viewer'sEntityViewer, mirroringDamageSystems.EntityUIEvents; pure server-side, zero reflection). But the packet carries only a hit angle + text - no colour - and the client renders one popup perCombatTextUI component an entity exposes. So colour can only come from the entity'sCombatTextasset, and only one shows cleanly at a time. - Per-event colour via a per-viewer component swap.
ColoredCombatTextServicecolours a popup by sending only the acting viewer aUIComponentsUpdatethat swaps the target's whiteCombatTextcomponent for a mod-shipped coloured one (Server/Entity/UI/MMO_CombatText_Redfor crits,MMO_CombatText_Goldfor level-ups; the health bar is kept), firing theCombatTextUpdate, then restoring white ~0.5s later (onedrainRestorescall onAbilityDotTickingSystem). The swap is a per-viewer client packet (the server component is never changed), so other players and later hits are unaffected; it is multi-world safe (restores keyed byRef.getStore()). EntityCombatTextSanitizeSystem(aHolderSystemon living entities) forces each entity'sUIComponentListto a singleCombatTextcomponent (the vanilla white one) at spawn, stripping the mod's coloured assets from the default identity set. Without it, becauseUIComponentList.update()puts every loaded UI asset on every entity and the client renders one popup perCombatTextcomponent, a normal damage number would stack once per colour. Decompile-grounded (UIComponentSystems/NetworkSendableSpatialSystem); the colour assets are validated (scale range) byCombatTextUIComponentScaleAnimationEvent's codec at load.- Scope. Only crits (red) and level-ups (gold) float. Kill XP and per-action XP show only the action-bar notification (
SkillService.addXpByName); the combo finisher keeps its combat bonus + notification but no longer floats; lifesteal keeps its notification. One colour per entity per instant: a held crit colour also tints that hit's damage number red (intended). Strings resolve throughMessages/SkillDisplayUtils(ui.combattext.crit/ui.combattext.levelup), ASCII-only. CombatTextConfig(SCHEMA_VERSION = 1,mods/mmoskilltree/combat-text.json) - masterenabled+ per-event togglescrits/levelUps+ per-event style overridescritStyle/levelUpStyle(any authoredCombatTextasset id; an unknown id degrades to plain white).FeedbackEvent.combatTextStyleholds each event's built-in default colour;CombatTextConfig.getStyle(event)returns the override or that default, read byFeedbackService.- Crit toast default-off, migrated by key rename.
NotificationCategory.CRITflipsdefaultEnabledto false and moves its toggle-map key fromcrittocrit_toast. Reusingcritwould have pinned every upgrading player to the old default: each 1.1.6+ save carries an explicitcrit=truebaked in by the legacyShowCritNotificationscodec read (it replays the resolved value into the unified toggle map). That legacy key stays registered (old saves parse; the write side still emits the resolved value for downgrade safety) but its read side now drops the value, since replaying it would re-bake the old default under the new key. Orphanedcritentries ride along in saved maps harmlessly; a player who re-enables the toast persists undercrit_toast.
Technical: DRY + cleanup
- Shared tab-nav helper. The category-tab→
SkillCategorymapping (isCategoryTab/getCategoryFromTabId) that was copy-pasted intoViewXpPage/BoostPage/QuestPageis now oneTabNavHelper, and the three pages' category-permission check now routes through the existingSkillFilterUtil.hasAvailableSkillInCategoryinstead of three near-identical inlined loops. SkillTypeinternal migration. The last internal usages of the deprecatedSkillTypeenum (BoostTarget.skillByName/appliesByName) now use string skill IDs + the unifiedSkillRegistry. The deprecated publicBoostTarget.skill(SkillType)/MMOSkillTreeAPIoverloads are kept for back-compat.- Dead-code + logging. Removed a large dead commented-out "Blacklist" UI block in
AdminConfigPage; surfaced the previously-stubbed Items Picked Up counter in the Statistics view's Gathering section (the data was already tracked); converted several previously-silentcatchblocks in the class/quest services to traceableLOGGER.atFinelogging. - Reroll persistence + no-cycle exclusion.
SlotRerollStore(the shared per-position reroll state behind bounty boards and shop pools) write-throughs each successful commit into a new append-onlySlotRerollsmap onSkillComponent(keys; stale periods pruned on write, hydrated on connect, pre-existing saves decode without migration), so a paid reroll's outcome and the per-period cap survive relogs and restarts. Each position also records every id it has shown this period and the replacement draw excludes that history, so a reroll can never cycle back to a previously rolled-away item within one rotation.: # # #cur|seen# RotationRerollTestcovers the persist/hydrate round-trip, live-state authority, domain isolation, and the no-cycle exclusion. - One reward command builder.
RewardExecutor's three typed executors (XP / boost token / currency) each re-built the same console command and offline-queue tail thatretryCommandOfalso builds for the purchase pipeline's failed-grant requeue;executeRewardnow routes every type throughretryCommandOfas the single command builder (~125 lines dropped), so the executed, queued, and retried command forms can never drift. Typed rewards offline withqueueIfOffline=falsenow log the skip (previously silent).TokenShopPagesimilarly folds its board-title and pool-label key resolution into onelocalizedOrhelper. - Achievement announcement template. The 35
announcementCommandliterals inAchievementDefaultseach re-declared the sameeventtitle --title=... --secondary=...string, and every one was missing the closing quote on--secondaryplus the{achievement}placeholder. They now route through a singleannounce(title[, secondaryBody])helper that builds the balanced command once, so the quote can't drift and the achievement name is always interpolated.buildAllSkillsServerFirstand the per-skill leveling loop now skip!registry.isSkillAvailable(sid)skills;MMOSkillTreePluginregenerates achievement defaults after integration resolution (mirroring the Citizens quest reload) so a Tamework-gated TAMING is included only when actually active.
Technical: localization-key display text + DRY resolver
- One resolver for all content display text. New
i18n.LocalizedText.resolve(lang, explicitKey, conventionKey, rawFallback)is the single source for the "explicit key -> by-convention key -> raw fallback" precedence every content surface uses, keyed by language code (viaMessages.getForLang), not aSkillComponent- call sites read the locale from the player-data seam's O(1)players().language(ref)projection, so resolving a label never allocates a throwaway component. It folds together four hand-rolled copies (QuestTitleUtil, the bounty-board flavor block,TokenShopPage's title/desc, and a previously-missingNpcQuestPagepath) and unifies their divergent empty-checks.QuestTitleUtilgains a sharedflavor(...);ObjectiveTextRenderer/TargetNameResolvergainlangoverloads (theSkillComponentones delegate via the now-@NonnullMessages.getLanguage, so the ~30 existing callers are untouched). - Token Shop offers use localization keys.
ShopEntrygains atitleKey(parity with its existingdescriptionKeyand withShopBoard/ShopPool); every shipped offer now carries explicittitleKey/descriptionKeyand the pack ships the text in.lang, with the rawdisplayName/descriptiondropped. The fanned per-skill XP packets keep their{{SKILL_NAME}}template (one static key can't cover every skill). - Currency names localize at the chokepoint.
CurrencyDefinitiongains anameKey;CostRenderer.currencyNamenow resolvesnameKey -> currency.(so every cost line, chip, and gain toast localizes through the one method), with a.name -> displayName langoverload for per-player surfaces. The three shipped currencies move their names tocurrency.keys..name - Item-backed currency names derive from the native item. An item-backed currency with no authored name resolves its display name from the backing item's own native, already-translated lang key (
server.items.) - the same way its icon already derives from the item - slotting in just before the deprecated raw.name displayNamefallback. The mastery pack drops its 9 hand-maintainedcurrency.life_essence.nametranslations: the essence currency now reads "Essence of Life" (and each locale's native form) everywhere, so the chip, reward line, objective, and inventory tooltip can never disagree, and Hytale's own translations carry it. - Non-bounty quest flavor matches the board.
NpcQuestPagerenders a quest's flavor via the sharedflavorKey -> quest.precedence (it previously used only the raw description); the mastery pack's quests drop.flavor -> description displayName/descriptionforquest.keys in a new pack.* .lang. - Achievement display-text resolver.
AchievementDefinitiongainstitleKey/descriptionKey; newAchievementTextUtilresolvesachievement./.title .descby convention; the detail + list pages route through it (rawdisplayNamekept as the fallback). - Mastery tracks + nodes resolve through keys.
MasteryTrack/MasteryNodegaintitleKey/descriptionKey; newMasteryTextUtilresolvesmastery./.title .descby convention, and everyMasteryPagetrack + node title/description site routes through it. The track titles ship asmastery.keys; node English names stay on the pack template's.title {{trackName}}-substituteddisplayName(already DRY across the 11 combat tracks that shareCombat6_Standard), so a translator adds per-nodemastery.keys only where wanted instead of writing ~120 by hand..title - Reward lines auto-render localized; no baked amounts. The renderers own every reward line's amount/quantity ("+{0} ... XP" via
reward.line.*, "x{0}" item suffixes, currency names viaCostRenderer.currencyName), so the built-in quest + achievement defaults and the mastery/taming pack rewards drop their authoreddisplayName/descriptionliterals entirely - a baked "2 Excavation XP Shards" or "+7,500 Defense XP" would double-render next to the renderer's own amount AND pin the line to English.RewardPreviewBuilderalso parses/mmoboost givegrants (a newBoostCommand.parseGiveCommand, kept in lockstep with the command, both the--args=and positional pipe forms) into the localized boost line + skill icon, so boost command rewards need no authored name either; entries with an unsubstituted{skill}placeholder keep theirs (only resolvable at execution time). - Mastery lock reasons + action results localized.
MasteryService's purchase-check reasons ("Already owned", "Requires node: {0}", "Need {0} more {1} (have {2})", ...) and purchase/contribute/respec result messages now resolve throughMessageskeys (ui.mastery.locked_*,ui.mastery.toast.*) in the viewer's language, with cost-component names through the sharedCostRenderer.currencyName/TargetNameResolver.itemNameresolvers (lang-keyed helpers, locale projected once per check; noSkillComponentthreading). The admin-onlyrespecTrack/respecAllsummaries stay raw English. All new keys ship translated in all 9 languages.
Technical: exhaustive translations + structural mastery node titles
- Every language is now key-complete. The 8 non-English jar
.langfiles were ~900-1,000 keys behind en-US (mostlyability.*names/flavors plus newer UI families); all of them are filled, so all 9 languages carry the full key set. The bounty pack's 12 missingshop.*keys and its English-onlyitems.lang/npcs.langare translated into all 8 locales too, and the classes pack gainsServer/Languages/withclass.convention keys (new.* ClassTextUtilresolves them). Missing keys still fall back to English per key (LocalizationConfig.rawI18nretries en-US), so partial third-party packs keep working. - Structural mastery node titles. Template-fanned node ids share a structural tail (
arc_t1_dmg->t1_dmg), soMasteryTextUtilnow composes a node title from the localized track title + a pack-shippedmastery.node.tier name via.title mastery.node_title.format("{0}: {1}") when no per-node key exists - ONE key per template tier localizes all 11 combat tracks and any future custom-skill fan-out. Same mechanism for descriptions (mastery.node., args = track title + target skill name) covers the fanned Eternal nodes. Hand-authored nodes (ability tracks, gathering tracks, capstones) ship per-node.desc mastery./.title .desckeys, which win over composition - required for ids that reuse a template tail (fb_t1_dmg,min_eternal).nodeTitle/nodeDescnow take the track (callers all had it in scope). - Currency render sites unified on the resolver. Several Mastery page surfaces (cost chips, deposit/pay-with buttons, the confirm panel's "you have" line, the route-lock label) and
/mmocurrencyhad kept rawdisplayNamereads; once this cycle's localization pass moved the pack currencies ontocurrency.keys (no literal left), those sites regressed to raw ids ("3 mastery_point"). All of them now route through.name CostRenderer.currencyNamein the viewer's language, and the "(pay any one)" annotation becameui.mastery.pay_any_one. (Never visible in a shipped release; the regression and fix are both in this cycle.) - Mod-item name namespaces.
LocalizationConfig.canonicalItemName/canonicalNpcNamenow fall back from the nativeserver.items.*/server.npcRoles.*keys to theitems.*/npcs.*namespaces that jar- and pack-shippeditems.lang/npcs.langfiles load under, soTargetNameResolverand the reward renderers resolve mod-authored item and NPC names instead of prettifying ids ("XpToken Woodcutting Token" -> "Woodcutting XP Token"). This only became visible when this cycle dropped baked reward displayNames in favor of renderer-resolved names, so nothing shipped broken. - Generated XP-token translations. The 8 non-English
items.langfiles carried the XP-token names/descriptions as untranslated English copies;tools/i18n/generate_xptoken_langs.pynow composes the 152XpToken_*name+description pairs per locale from that locale's ownskill.names plus an 8-word tier table ("Jeton d'XP de Bûcheronnage", "Holzfällen-XP-Marke") - ~20 table lines per locale instead of ~300 hand-maintained ones. Rerun after adding a skill or tier;LangFileIntegrityTestguards the output. - Quest objective displayText retired.
QuestDefaults' 41 objectives drop their raw EnglishdisplayTextfor either the per-typeobjective.text.templates or explicit textKeys in the new sharedobjective.text.quest.*flavor family (14 keys x 9 languages, "Mine {0} {1}" / "Slay {0} {1}" / "Dig {0} soil blocks");DEAL_DAMAGEjoins the pluralizing target types. The deprecated raw-text path still works for server-owner JSON. The quest defaults serializer (_defaults/default-quests.json, read back as the base layer each start) now round-trips only AUTHORED text plus thetextKey- it used to bakegetDisplayText()'s machine fallback ("BREAK_BLOCK Soil_ x200") as authored text, which surfaced the moment the authored strings were removed (this-cycle regression, fixed same cycle). - Achievement criteria de-texted; empty-target templates.
AchievementDefaults' 15 multi-criteria displayTexts (plus the generated all-skills server-first criteria) drop to""soObjectiveTextRenderertemplates localize them. A newobjective.text.family ("Defeat {0} enemies", 7 types x 9 languages) is tried first whenever an objective/criterion has an EMPTY target - generic, so it also covers pack content and owner JSON. Quest tag pills localize via.any QuestTitleUtil.tagLabel(ui.quest.tag.->skill.-> raw; colors keep keying on the raw tag); the bounty pack's four deliver quests drop their authored "Turn in ..." overrides for the localizedturn_intemplate. - English-only pluralization guard.
CostRenderer.currencyNameonly appliesPluralizerfor the default language;Pluralizeris English-only by design and would inflect translated names ("Lebensessenzs"). - Guardrails. New
LangFileIntegrityTestfails the build on placeholder mismatches vs en-US, em-dashes, or duplicate keys in any jar.lang; key COVERAGE is intentionally advisory - a new.claude/hooks/check-localization.ps1PostToolUse hook reports per-locale missing-key counts when EnglishDefaults or any.lang(jar or pack) is edited, and flags hardcoded display literals in UI pages. - No balance data in lang values (invariant + guard). Lang values must never hardcode amounts, durations, multipliers, quantities, or a currency's name (outside
currency.) - a balance pass must never require a localization edit, and the renderers already draw every amount from JSON. The failure was live: the all-skills boost moved to 45 minutes in JSON while 8 translations still titled it "(2x, 30m)", and cache descriptions baked quantities ("64 Life Essence") that drifted on every economy pass. All shop/quest/achievement/mastery content values across the jar and packs were swept to data-free flavor (~530 hits in 18 pack lang files), and the check-localization hook's new check 4 scans the edited file's whole language family for digits (placeholder args exempt; Roman numerals for ranks) and per-locale currency-name restatements on every lang edit..name - Content packs are git submodules now.
content-packs/is no longer gitignored; all six packs are recorded as submodules of the monorepo (workflow: commit inside the pack, then bump the pointer in the root repo).
Technical: toast engine + validation
- One shared toast fragment. The
#MmoToastmarkup block that was copy-pasted into 11 page.uifiles is now a singlePages/MmoToast.uifragment appended byToastablePage.renderToastIntoas the last root sibling (same z-order as the old static blocks), so everyToastablePagesubclass gets the overlay with zero per-page markup - which is howMasteryPagejoined (it extendsToastablePagenow; itsstatusMsg/#StatusLabelfooter is gone). - Toast body rows carry icons.
ToastSpec.linesis nowList(a transport-agnosticiconItemId/quantity/textrecord inui/toast);RewardListRenderer.toastLine(s)maps the sameRewardPreviews the panels render into toast rows, so toast rows and panel rows can never drift (textLinesis gone). The fragment pre-bakesToastRenderer.MAX_LINES(6) icon + label rows because asendUpdatecan only restyle existing elements; rows (and an icon-less row's icon cell) show/hide via the established Anchor expand/collapse trick, with slots filled per toast. - Skill-tree claim toast headline. New
ui.skilltree.toast.reward_claimed("{0} reward claimed") carries only the skill name; the reward renders as its icon row (the chat/feed message keeps the fullnotify.reward_claimedsentence). - Toast framing, kind color + placement.
MmoToast.uinests an outer border frame around a darkContainerFullPatch.pnginner panel - the dark inner keeps the headline and reward rows clear of the frame (no more title-over-border overlap). The frame is a mod-shipped WHITE double-line 9-slice texture (MmoToastFrame.png+@2x, beside the.uiso the relative path resolves) thatToastRendererTINTS per kind viaBackground.Color- so the whole toast is color-coded: gold = REWARD (newToastKind.REWARD, used by every reward/claim toast), green = SUCCESS, red = ERROR, matching the headline color. The game's ownGoldBorderDecoris a decorative segmented frame whose edge gaps tore holes when 9-slice-stretched, and the dark container frames tint muddy, so a white tintable texture is used instead.ToastRenderercenters the panel in the top third of the screen (topFor(height)recomputesTopfrom the panel height each show). Spacing is deterministic - a fixed-height headline, a real title->reward gap (row 0 carries a larger top margin), and a panel height (panelHeight) sized to fit content exactly, so the panel never shows empty space below the last row (FlexWeightdoes not expand a Label vertically). Headline is 18px; reward rows are larger (15px) and brighter (#eef3fa). The renderer drives the same#MmoToast/#MmoToastText/#MmoToastRow*ids by descendant selector, so the inner wrapper needs no Java change. - Quest + achievement validators; baked-amount check.
ValidationChecks.checkRewardsgains aBAKED_AMOUNT_DISPLAYwarning (a rewarddisplayNamestarting with a+amount,Nxboost, or bare leading count double-renders next to the renderer-added amount), and itsOPAQUE_COMMAND_REWARDrule now recognizes/mmocurrency giveand/mmoboost givecommands as auto-renderable (no nameKey needed). New minimalQuestValidator(non-bounty quests; bounties stay withBountyValidator) andAchievementValidatorregister withContentAudit, so/mmoconfig validateand the startup audit run the shared reward checks over every quest and achievement. - Positional command-reward audit.
ValidationChecks.checkRewardsgains aPOSITIONAL_COMMAND_ARGSwarning beside the existingPOSITIONAL_GIVE_QUANTITYone: a COMMAND reward whose/mmoboostor/mmocurrencystring carries a bare token after the subcommand would fail at dispatch (the engine binds optional args only as--name=value), so server owners' existingcommand-rewards.jsonoverrides get flagged at startup and by/mmoconfig validate. The token scan lives inValidationChecksitself (class-loading anAbstractCommandsubclass needs the Hytale runtime, which validator unit tests lack), kept in lockstep withBoostCommand/MmoCurrencyCommand's lenient preview parsers.
Technical: unified FeedbackService
- New
FeedbackService.emit(FeedbackEvent, FeedbackContext)consolidates the scattered floating-text + sound + camera + notification calls behind one enum-keyed dispatch. The colored-text call sites (CombatDamageEventSystemcrit,SkillServicelevel-up) route through it, withFeedbackEvent.combatTextStylenaming each event's colored asset (null = none); the remaining events (kill XP, combo finisher, bounty, shop) keep their sound/camera/notification hooks but no floating text. The text channel stays gated bycombat-text.json(CombatTextConfig); the newfeedback.json(FeedbackConfig,SCHEMA_VERSION = 1) owns the per-event sound + camera ids. - New
CameraShakeServicewrites aCameraEffectshake packet to a player's packet handler (CameraEffect.getAssetMap().getAsset(idx).createCameraShakePacket(intensity)thenplayerRef.getPacketHandler().writeNoCache(...)), mirroring the engine's own damage-shake path. Zero reflection, off by default (no shake assets shipped; the channel exists for servers that want it). - New
feedback/FeedbackEventenum +feedback/FeedbackContextbuilder. Every channel is independently try/guarded so a misconfigured asset can never throw into a damage-pipeline call site.
Technical: Bounty Board
- New content-asset type
BountyBoardAsset(Server/MMOSkillTree/BountyBoards/*.json) folds intoBountyBoardConfig(SCHEMA_VERSION = 1;bounty-board.jsonowner layer; precedence defaults < pack < owner), wired through the standard pipeline (PackContentType.BOUNTY_BOARD,PackControlAsset.BountyBoards,AssetStoreRegistrar,AssetMergeHandlers.onBountyBoardAssetsLoaded).AssetCodecInitTestcovers the new CODEC. BountyBoardmodel carries type-discriminatedrotation(interval / period / anchor) +selection(weighted-random) +slots(difficulty/tag filters), so new cadences and algorithms slot in without breaking existing configs.BountyServicecomputes a board's active set as a pure deterministic function of(boardId, epoch-period, selection seed)with weighted slot selection, so every player sees the same board with no persisted state and it survives restarts. Per-player reroll offsets are in-memory only (noSkillComponentchange).- Bounties are ordinary repeatable quests tagged
board:/diff:/weight:withnpcViewId: "bounty_board"; accept / track / complete / cooldown / reward all reuseQuestService+ProgressEvents. The bounty content (templates, contracts, Bounty Token currency, daily board) ships in the standalone MMOSkillBountyPack. - In-world launch, gated like the quest-NPC pattern. The board never opens from a player command. The standalone MMOSkillBountyPack ships the custom
MMO_Bounty_Boardblock (a wall-mounted poster reusing the vanillaLobby_Wall_Poster01model / texture / icon, tinted per block,Support: North;Flags.IsUsable+BlockType.Interactions.Use → MMO_BountyBoard_Open). ThatRootInteractionruns the registeredBountyBoardOpenInteraction(SimpleInstantInteraction, mirrorsMmoMenuOpenInteraction) which opensBountyBoardPagefor the interacting player. A console/admin/mmobountyuicommand (mirrors[board] NpcQuestUICommand) is the NPC-wiring/admin entry point a server owner's own mod can also call; the NPCOpenMmoUiaction and the block both route through the sameBountyBoardPage.openFor(...)./mmobountyis console/admin-only (nobountyalias) and now carries only thereset/validatediagnostics; the old player-stylelist/accept/rerollself-serve subcommands (and their now-unusedui.bounty.*strings) were removed, since the board is reached only through the block / NPC / hub UI. - Per-block board targeting (several boards per world).
BountyBoardOpenInteractionreads an optionalBoardcodec field, so each placed block opens a specific board. ARootInteractionin object form ({"Type":"mmo_bounty_board_open","Board":"weekly"}) names the board; the bare-string form falls back toBountyService.defaultBoardId()(BountyBoardConfig.getDefaultBoardId()= the configureddefaultBoard, else the lowest-orderenabled board). The pack ships adailyand aweeklyboard, each with its own block +RootInteraction. Verified against the 0.5.3 decompile:RootInteraction.Interactionsis a string-or-object union (ContainedAssetCodec) over the polymorphicInteraction.CODECkeyed onType, so a custom subclass-codec field is populated on the same instance passed tofirstRun(precedent:AddItemInteraction'sItemId/Quantity). - Enriched board schema.
BountyBoardgains optionalenabled(per-board on/off, AND-combined intoisBoardAccessible),descriptionKey(rendered as a page subtitle),icon(reserved), andorder(sort + default-board resolution).BountyBoardConfig.parseBoardnow logs aWARNINGinstead of silently defaulting on a malformed value (unknown rotation/selectiontype, non-numericorder/offsetMinutes/count, out-of-range slot count); the deterministic active set is byte-identical, soBountyServiceStrategyTeststill passes. - New
BountyValidator(service/bounty/) cross-checks every board against the quest pool +CurrencyConfigand reports empty pools, unfillable required slots, unknown rotation/selection types, malformedweight:tags, and missing or disabled reroll/reward currencies as coded findings. It runs once on the firstPlayerReadyEvent(after the Quest/Currency/BountyBoard packs have merged, so it never races the async load), on demand via/mmobounty validate, and as an issue count in the admin menu. The singleton-free core is covered byBountyValidatorTest. - Admin toggle.
BountyBoardConfig.setEnabled(persists tobounty-board.json) backs a new Bounties toggle inAdminConfigPage, mirroring the Elite Mobs toggle.FeatureRegistry'sbountyid reads it live, so flipping it gates the block interaction, the page, and the commands at once. - Bounty content fully externalized. The baked-in default
dailyboard was removed fromBountyBoardConfig, so with no pack installedboardsis empty,isAvailable()is false, and the feature is dormant (matching the pack's stated design). The block(s), theirRootInteractions, and the blockitems.langstrings moved from the mod jar into the standalone pack; only the page markup (BountyBoardPage.ui/BountyRow.ui) and the Java glue stay in the jar. The pack declares a hardDependenciesentry on MMOSkillTree (its block needs the jar-registered interaction type) and itsbuild.ps1emits explicit zip directory entries so the bundled.langloads. BountyBoardPage(InteractiveCustomUIPage+Pages/BountyBoardPage.ui/BountyRow.ui) is a two-panel master-detail page modeled onNpcQuestPage: a selectable bounty list on the left (difficulty badge, status dot, inferred objective viaObjectiveTextRenderer) and a detail panel on the right with all objectives showing liveX/Yprogress (reusingQuestObjectiveRow.ui+PlayerQuestDataprogress), reward icons (RewardPreviewBuilder→ItemGrid, text fallback for counter rewards), difficulty, status, and Accept. A header strip shows the player's lifetime bounties-completed (StatisticsComponent) and reroll-currency balance (CurrencyService.get); selection re-opens the page (instanceselectedBountyId). Accept / reroll reuseQuestService+BountyService.- Bounty achievements via the existing fan-out. New
ObjectiveType.COMPLETE_BOUNTY(board = target, difficulty = qualifier) is fired once per completed bounty throughProgressEvents.fire, so a single call drives the new bounty achievements, abounties_completedstatistic (StatisticsServiceStatSpec, zero codec bump - thetotalsmap is generic), and even a "complete N bounties" quest objective for free. The two scattered bounty-completion sites inQuestService(auto-claim + manual claim) collapse into onefireCompletionHookshelper. A second value-basedObjectiveType.BOUNTY_STREAK(modeled onREACH_LEVEL) backs the daily-streak achievements, persisted onSkillComponentas three appendedCodec.LONGfields (bountyStreakCurrent/bountyStreakBest/lastBountyEpochDay, no player-data migration). The board-agnostic chains (bounty_any/bounty_hard/bounty_streak+ a server-first) live inAchievementDefaultsunder a newquests › bountiessubcategory; the pack ships per-boardDaily Contractor/Weekly Warrantchains via aBounty_Board_CounterAchievementTemplate. - Rotation/Selection strategy interfaces.
RotationStrategy+SelectionStrategy(service/bounty/, type-keyedBountyStrategiesregistry; shippedinterval+weighted_random) now backBountyService, so a new cadence/algorithm registers a key with no service edit. Behavior is byte-identical, locked byBountyServiceStrategyTest. - Feature gating + feedback. A board's
requiresFeaturesis now enforced (viaFeatureRegistry, which gains abountyid) at the block interaction, the open command, the page, and listings, through oneBountyService.isBoardAccessible.FeedbackEvent.BOUNTY_COMPLETE(on the COMPLETED transition for bounty quests) andBOUNTY_REROLLnow fire throughFeedbackService. - Turn-in bounties + a board hand-in button. Deliver contracts use the existing
TURN_INobjective (player-initiated item consumption viaQuestService.attemptTurnIn). Because bounties arevisibility.hidden(no normal quest-log entry),BountyBoardPagegains a Turn In button:populateDetailfinds the active non-NPC-lockedTURN_INobjective (afindActiveTurnInObjectivemirroringQuestPage), a newturninaction onBountyEventData(thirdObjectiveIdcodec field) runsattemptTurnInand re-opens the page. The pack ships aBounty_TurnIn_Standardtemplate (matchModeEXACT) plus deliver contracts; deliver-bounty rewards are intentionally CURRENCY + XP only, since a board bounty parked inCOMPLETED_UNCLAIMEDfor lack of inventory space would be unclaimable from the board. - Per-difficulty combat-level accept gate.
BountyBoardgains an optionalcombatLevelRequirementsmap (difficulty -> min combat level, opt-in, defaults empty).BountyService.maxCombatLevel(max overSkillRegistry.allSkillIdsByCategory("COMBAT")) +meetsCombatRequirement(board, difficulty, skills)enforce it at accept time only (BountyBoardPage.doAccept+MmoBountyCommand.doAccept); the deterministic active-set selection is untouched, so every player still sees the same board andBountyServiceStrategyTestis unchanged. Locked contracts render with alocked_levelstatus + a "Requires combat level N" detail line and no Accept button. The pack's boards declare{normal: 25, hard: 60}. Covered byBountyCombatGateTest.
Technical: Token Shop
- New content-asset type
ShopEntryAsset(Server/MMOSkillTree/ShopEntries/*.json) folds intoTokenShopConfig(SCHEMA_VERSION = 1;token-shop.jsonowner layer; precedence defaults < pack < owner), wired through the standard pipeline (PackContentType.SHOP_ENTRIES,PackControlAsset.ShopEntries,AssetStoreRegistrar,AssetMergeHandlers.onShopEntryAssetsLoaded).AssetCodecInitTestcovers the new CODEC. Mirrors the Bounty Board split: engine in the jar, catalog in the pack, so the shop is dormant (no entries) without a pack. ShopEntrymodel + unified reward shape. An immutableShopEntry(id, localization keystitleKey/descriptionKey, a cost OBJECT in the sharedCostschema ({"currencies":{...},"items":[...]}),category,order, the sharedRequirementsblock { minCombatLevel, per-skill map, requiresFeatures },limitPerDay,limitTotal,rewards) pays out through the unifiedRewardmodel (content/reward/:RewardParser+RewardExecutor), so a shop reward is exactly a quest reward (XP / BOOST_TOKEN / COMMAND/giveitem / CURRENCY conversion).TokenShopServicemirrors the mastery purchase flow:canPurchasereturns a concrete lock-reason key (disabled / feature / combat-level / per-skill / per-day limit / total limit / insufficient balance / no inventory space), thenpurchasere-checks,CurrencyService.takes the cost, grants each reward, and records the purchase. The/giveinventory-space check is a hard gate beforetake, so a space-blocked item reward never spends tokens. A pureevaluate(skills, entry, balance, hasSpace, epochDay)core keeps the decision matrix unit-testable (TokenShopServiceTest). The combat-level helper is the sharedBountyService.maxCombatLevel.- Per-player limits via codec append (no migration).
SkillComponentgainsshopPurchaseTotals/shopPurchasesToday(String->Longmaps) +shopPurchaseEpochDay, appended after the bounty-streak fields as twoCodec.STRINGmaps + oneCodec.LONG; the per-day counters reset on a UTC day change inrecordShopPurchase. Pre-1.3.0 saves decode to empty maps / day 0. TokenShopPage(InteractiveCustomUIPage+Pages/TokenShopPage.ui/ShopRow.ui) is a two-panel master-detail page modeled onBountyBoardPage: a category-grouped catalog (affordability-colored cost, locked rows) on the left and a detail panel with cost, requirement/limit lines, reward icons (RewardPreviewBuilder), and a Buy button (or a localized lock reason) on the right, with a Bounty Token balance header. Buy routes throughTokenShopService.purchaseand re-opens for state refresh.- Gated launch + admin toggle + validation. A registered
TokenShopOpenInteraction(mmo_token_shop_open) backs the pack'sMMO_Token_Traderblock (RootInteraction-> the interaction), and a console/admin/mmoshopuimirrors/mmobountyui; both route through oneTokenShopPage.openFor./mmoshopis console/admin-only (list/reset/validate).TokenShopConfig.setEnabledbacks a new Token Shop toggle inAdminConfigPageand aFeatureRegistryshop/tokenshopid. A newShopValidator(service/shop/) cross-checks entries vsCurrencyConfig(missing/disabled cost currency, non-positive cost, missing reward currency, unknown category) on the firstPlayerReadyEvent, via/mmoshop validate, and as the admin issue count; the singleton-free core is covered byShopValidatorTest.
Technical: spawn NPCs (press-F → MMO UI)
- Custom NPC interaction action
OpenMmoUi(npc/ActionOpenMmoUi+BuilderActionOpenMmoUi) mirrors the engine'sActionOpenBarterShop: registered once insetup()viaNPCPlugin.get().registerCoreComponentType("OpenMmoUi", BuilderActionOpenMmoUi::new)(before NPC role assets load), it reads aTargetstring (hub|shop| a board id, defaulthub) and, on the NPC'sInteractionInstructionfiring, resolves the interacting player viarole.getStateSupport().getInteractionIterationTarget()and hands theTargetto the sharedMmoUiRouter.open(...)(one routing table:hub→MmoHubPage,skills/quests/achievements/abilities/mastery/boosts→ that feature's page,shop[:id]→TokenShopPage, else a board id →BountyBoardPage). This is the engine-sanctioned route - an NPC'sUseinteraction is hard-wired to*UseNPC, which runs the role'sInteractionInstruction, so opening a custom page is done by an interaction Action, not aRootInteractionoverride. MmoHubPage(InteractiveCustomUIPage+Pages/MmoHubPage.ui+HubTile.ui) - a feature-aware launcher. It appends one icon tile per active destination from a single in-class table: every accessible bounty board + shop storefront (BountyService.accessibleBoards()/ShopAccessService.accessibleShops()), then the always-present core feature pages (skills / quests / achievements / abilities / mastery / boosts, each gated bySkillConfig/FeatureRegistry), plus a right-hand panel previewing the primary board's active bounties (hidden when no board is accessible, the tile grid takes the full width). The hub is never empty (skills + the guide always render), so it always opens and makes sense in the base mod with no bounty pack. On a player's first open it routes toOnboardingPageinstead (latched by the appended-codecSkillComponent.hasSeenOnboarding, no migration);openForDirectis the latch-bypassing entry the overview's Finish calls. Every tile + the NPC action route through the sharedMmoUiRouter.open.OnboardingPage(InteractiveCustomUIPage+Pages/OnboardingPage.ui) - a short Next/Back/Finish walkthrough whose steps are composed from live features (Welcome always; Bounties whenaccessibleBoards()is non-empty; Shop when the shop is available, enabled with a loaded catalog), so disabled features are never explained; Finish latches "seen" and opens the hub. Each step renders a faithful mini-replica of the real page (see the previews note below).- Shop-purchase feedback. New
FeedbackEvent.SHOP_PURCHASE(defaultSFX_Coins_LandinFeedbackConfig) fires fromTokenShopPage.doBuyon a successful purchase, keepingTokenShopServicefree of UI/feedback deps (mirrors how the board emitsBOUNTY_ACCEPT). The bounty turn-in path no longer emitsBOUNTY_COMPLETEfrom the page -QuestService.fireCompletionHooksalready emits it on the COMPLETED transition, so a bounty turn-in routes the same single completion cue as a normal quest. Long list-row names wrap (Wrap: trueon the row label styles, with the redundant per-row difficulty/category labels dropped so the title gets the width). MmoNpcSpawnService+MmoNpcPlacementStore-spawnRole(...)resolves a role by name (NPCPlugin.getIndex, guarded byhasRoleNameso it's inert when the role isn't registered), callsNPCPlugin.spawnEntity(position fromworld.getWorldConfig().getSpawnProvider().getSpawnPoint, offset by config), and records the spawned NPC's UUID.maybeAutoSpawnHubadds a once-per-world JSON marker (mirroringPlacedBlockTracker, keyed byWorld.getName()); the hub spawns by default as the general guide (gated only byspawn-hub.json, no bounty/shop content required) and the hub page self-gates which destinations it offers. It runs from the existingPlayerReadyEventworld.execute(...)block - valid becauseaddEntity/removeEntityrequire being *outside* the ECS processing window, which the world task queue guarantees.SpawnHubConfig(spawn-hub.json,SCHEMA_VERSION = 1) -enabled(default true),role(defaultMMO_Hub),offset,yaw; aFeatureRegistryspawn_hub/hubnpcid maps to it./mmonpc(admin/console) spawns/lists/removes/resets viaMmoNpcSpawnService/MmoNpcPlacementStore(despawn viaEntityStore.getRefFromUUID+Store.removeEntity).- Jar-resident hub role; pack per-board/shop roles. The hub role (
MMO_Hub) + itsnpcs.lang(name "Adventurer's Guide" + F-hint) ship in the mod jar (src/main/resources/Server/NPC/Roles/Passive/MMO_Hub.json), so the auto-spawn works in the base mod with no pack (loads via the jar'sIncludesAssetPack: true).MMOSkillBountyPackships the three per-board / shop roles (MMO_Bounty_Daily/MMO_Bounty_Weekly= per-board,MMO_Token_Trader_NPC= shop). Each is a stationaryGenericrole modeled onKweebec_Merchant(only ever issuesBodyMotion: Nothing) with anInteractionInstructionrunning{ "Type": "OpenMmoUi", "Target": "..." }; names + F-hints innpcs.lang. Roles auto-load underIncludesAssetPack: true.
Technical: localized objective text
- New
ObjectiveTextRenderer+TargetNameResolverrender an objective from per-ObjectiveTypei18n templates (objective.text.*, 24 keys) plus a localized (skills) or prettified (mob/item/block id) target name. Used byQuestPage,NpcQuestPage, the quest-progress notifications, and the bounty list, so the old machine fallback (KILL_ENTITY Zombie x10) is gone for all quests, not just bounties.QuestObjective.getRawDisplayText()distinguishes author-set text from the fallback. English keys live inEnglishDefaults; the other 8 languages fall back per-key. - Pluralization. A new
Pluralizer(English rules + irregular map + an uncountable/mass-noun set) inflects the target noun by amount for countable objective types (kill / break / place / craft / pickup / catch / turn-in), so "Defeat 12 Zombies" / "6 Wolves" read right while "100 Stone" / "25 Iron Ore" stay natural. Pure + unit-tested (PluralizerTest); English-only by design (other locales supply their own plural template). Reused by the reward renderer for item/currency names.
Technical: shared rotating-pool core + shop pools
- Extracted a shared rotating-pool engine (
service/rotation/) so the bounty boards and the new shop pools ride one implementation:RotationSpec(period math),SelectionSpec,PoolSlot,PoolSeed.mix(the FNV seed, with a 4-arg per-position overload for single-slot rerolls),SlotRerollStore(in-memory per-player per-position override store + total-per-period cap),SlotRerollEngine(the override-layering + exclusion-build glue both consumers share),WeightedSlotDraw(the generic weighted distinct draw, typed overTwith id / weight / slot-match functions;drawDetailedalso returns the per-position governing slot, anddrawReplacementdraws one replacement for a single position excluding the others), andRotationConfigParser(JSON → spec).BountyBoard/BountyService/WeightedRandomSelectionStrategywere refactored to delegate to these (the base draw is byte-identical;BountyServiceStrategyTest/BountyValidatorTest/BountyCombatGateTestunchanged in intent). - Token Shop rotating pools (infra for many, ship one). New
ShopPoolmodel +ShopPoolAssetcontent type (Server/MMOSkillTree/ShopPools/*.json, wiredPackContentType.SHOP_POOL/PackControlAsset.ShopPools/AssetStoreRegistrar/AssetMergeHandlers.onShopPoolAssetsLoaded/TokenShopConfig.mergeShopPoolLayer).ShopEntrygains optionalpool/tier/weight; an entry with nopoolis a static catalog offer (unchanged), a pooled entry rotates.TokenShopService.getActivePoolEntries(poolId, now, player)is a pure function of(poolId, period, reroll)via the shared engine;reroll/canRerollhonor the pool'smaxRerollsPerPeriod.TokenShopPagerenders the active pool's offers + a refresh countdown + a reroll button above the static catalog;TokenShopOpenInteractiongains an optionalPoolcodec field so a trader block can focus a specific pool (mirrors the board'sBoardfield). The pack ships oneFeatureddaily pool + six pooled offers alongside the four static offers. - Per-slot reroll. Reroll swaps a single contract / featured offer rather than the whole set:
SlotRerollStoreholds per-(player, pool, period) position overrides + aspentcounter;BountyService.rerollSlot/TokenShopService.rerollSlotredraw one position from its slot's candidates excluding the others (seededPoolSeed.mix(pool, period, position, count)), so the rest of the set is untouched, and a stale override falls back to the base draw. The reroll button lives in the detail panel for the selected item (bounties only when NOT_STARTED; shop only for featured/pooled offers). Both boards and pools accept an optionalrerollCost.maxPerPeriod(0 = unlimited; the pack ships 3/day daily, 2/week weekly, 2/day Featured); a pre-chargecanRerollSlotprobe + thespentcap mean tokens are never spent on a no-op, with a refund guard on the (single-player-impossible) post-charge cap race. Covered byRotationRerollTest.
Technical: reward rendering + poster blocks + icon validation
- All rewards render, with amounts.
RewardPreviewBuildernow synthesizes a localized, amount-bearing line for non-icon rewards (XP / boost / counter-currency) via newreward.line.*keys, and a sharedRewardListRenderer(used by bothBountyBoardPageandTokenShopPage, replacing the duplicated blocks) renders every reward (icons + a text line built fromRewardPreview.fallbackText()), so a reward without an authordisplayName(the flat XP on every bounty) is no longer silently dropped. - Localized quest/bounty titles + flavor.
QuestDefinitiongains optionaltitleKey/flavorKey(parsed inQuestConfig);BountyBoardPageresolves a bounty's title astitleKey→ the by-conventionquest.key → the inferred objective sentence, and flavor as.title flavorKey→quest.→ the raw description (rendered as markdown). The pack ships.flavor Server/Languages/en-US/mmoskilltree.langwith a title + flavor blurb for all 32 bounties; no per-bounty JSON edit needed (the by-convention key is picked up automatically). - Poster blocks. The three pack blocks (daily / weekly board, token trader) now base their
BlockTypeon the vanillaLobby_Wall_Poster01model / texture / icon (Support: North,ISS_Items_Paper, distinctTextureComputedColorper block) instead of the crude sign. No Java change (the interaction handlers are model-agnostic). - Icon validation.
ShopValidator+BountyValidatorflag structurally malformed icon ids (blank-after-trim / whitespace) on entries / pools / boards (MALFORMED_ICON, WARNING).ShopValidatoralso now validates rotating pools (empty pool, unfillable required slot, missing/disabled reroll currency, orphaned pooled entries pointing at a missing pool) and accepts the newfeaturedcategory; the inline list now colors a locked offer's status dot red (parity with the bounty board).
Technical: shop/bounty UI polish + per-skill XP exchange + onboarding overview
- Per-skill XP exchange via a
forEachSkillfan-out.TokenShopConfig.applyPackLayerexpands a single pack entry flagged"forEachSkill": trueinto oneShopEntryperSkillRegistry.isSkillAvailable(...)skill, substituting{{SKILL}}/{{SKILL_NAME}}through the sharedJsonTemplateUtil(the Token Shop analogue of CommandRewards'{{ALL_SKILLS}}sentinel); the generated id isshop_xp_and an explicitly authored offer of that id wins. The pack ships oneXP_Packet_PerSkill.jsontemplate + anXpExchangerotating pool (replacing the six hand-authoredXP_Packet_*files); per-skill offers cost equalbounty_token+life_essence(multi-currencyCost,combine: ALL, item-backed essence required in inventory) and arelimitPerDay-capped. NoSCHEMA_VERSIONbump (additive, override-based). - One currency-name renderer (the "Bounty Tokenses" fix).
CostRenderer.currencyName(id, count)+currencyAmount(id, amount)are the single source for a currency's text, pluralizing the configured (now singular) display name viaPluralizer. Every site routes through it: cost summaries, the shop/board reroll buttons + reroll-fail toasts,TokenShopService'slocked_balancereason,CurrencyService's gain toast, andRewardPreviewBuilder's currency reward line (its duplicate local helper removed). Pack currency display names (bounty_token,mastery_point) are now singular. - List-row icon fallback. New
ShopEntryIconResolverresolves a shop row's icon as expliciticon→ first reward's icon (CURRENCY →getEffectiveIconItemId, COMMAND/give→ the item) so iconless offers like Mastery Infusion render art;TokenShopPage.appendListRowuses it. - Readable action buttons.
@BuyBtnStyle/@AcceptBtnStyle/@RerollBtnStyleswitch to bold white labels; newUIUtil.paintButtonLockedgreys a gated button (Buy when!check.passed, Reroll when capped) by tinting the patch + lightening the label in the fullbuild()re-open. Both pages widen to 1360x880, and#EmptyPanelbecomes a fixed-height (notFlexWeight) swap panel so#BodyRowis the sole flex filler and the left list scrolls. - Feature-driven onboarding overview.
OnboardingPagewalksOnboardingTopics.visible()- an ordered topic list (Welcome, Skills, Skill Tree, Quests, Abilities, Mastery, Currencies, Bounties, Shop, Taming, Elite Mobs; Class System omitted as unreleased) each gated throughFeatureRegistry, so disabled features are skipped. A"?" guide button(the game'sInfoButtonBackgroundtexture) onMmoHubPagere-opens it any time. Newui.onboarding.*keys inEnglishDefaults(regenerated into the JARmmoskilltree.lang).
Technical: quest-log + cost-chip + onboarding polish
- Shared quest title resolver. New
quest/QuestTitleUtil.resolve(skills, quest)(titleKey →quest.→ first-objective sentence →.title getDisplayName()) lifts the oldBountyBoardPage.bountyTitleout so the quest log + NPC quest page resolve bounty titles the same way;QuestPageandNpcQuestPage(both previously showed the raw id for bounties) now route through it. - Quest-log bounty cleanup.
QuestPage.populateQuestRowgates onBountyService.isBounty: it swaps a bounty's internalbounty/board:/diff:/weight:tags for a friendly "Bounty" pill + a localized difficulty pill (BountyService.difficultyColor, promoted fromBountyBoardPage), and theACTIVEcase skips the Turn In / Abandon / pre-bound accept blocks for bounties, showing a "manage at the board" hint instead. Non-bounty quests render unchanged. - Unified cost chip. New
util/CostChipRenderer+Pages/CostChip.ui(icon + amount, affordability-colored) is the single cost-chip vocabulary;MasteryPageis migrated onto it (itsappendChip/setChipText/applyChipIconnow delegate;MasteryCostChip.uideleted).TokenShopPageBuy/Reroll andBountyBoardPageReroll drop the price from the button label (ui.shop.btn_buy_plain/ui.*.btn_reroll_plain) and render it as a chip; the reroll-exhausted message moves from the button text to a#RerollLimitLabelhelper so it can't truncate. - Row-icon fix + bounty row icons.
ShopRow.ui's@RowIconGridStyledrops toSlotSize:24 / SlotIconSize:22inside the 28px anchor so the slot patch no longer clips the icon;BountyRow.uigains the same#RowIcon, populated inBountyBoardPage.appendListRowfrom the first icon-bearing reward (RewardPreviewBuilder, tooltip = the bounty title; hidden for XP/stat-only bounties). - "?" guide button in the title bar.
MmoHubPage's#GuideButtonsits in a#Titleoverride (top-left, clear of the close button) and re-opens the onboarding overview via theopenTutorialaction. - Onboarding teaches
/xp+ the scroll. A new always-on "Skills Menu"OnboardingTopicsstep teaches/xp(the existingGetXpCommand, aliasesxp/skills). When quests are enabled the body points at the auto-acceptedgetting_startedbreak-a-block quest (which already rewardsMmo_Menu_Scroll); when disabled,OnboardingPageshows a Claim button that grants the scroll viaCommandExecutor.executeAsConsole("give … Mmo_Menu_Scroll …"), latched by a new appendedSkillComponent.hasClaimedMenuScrollboolean (no migration). Theui.onboarding.skills.bodycombat-level sentence is corrected to "the highest of your individual combat skill levels" (matchesBountyService.maxCombatLevel). - Per-step faithful previews.
OnboardingTopics.Topicgains aPreviewKind(MASTER_DETAIL/DASHBOARD/HOTBAR/WALLET/GENERIC) + accent + an optionalPreviewSample(mockup content: real item-icon ids + short illustrative labels), so the topic table is the single source for both gating and preview content.OnboardingPage.uiis a two-column step (enlarged to 1180x680): left text, right#PreviewColumn.build()appends one of five preview fragments (Pages/Preview*.ui) that are faithful mini-replicas of the real pages - authenticContainerPanelPatchpanels, realItemGridslots filled with genuine item icons (cmd.set(sel + ".Slots", List.of(new ItemGridSlot(new ItemStack(id, 1))))), realProgressBarwidgets (dashboard viaProgressBar.png/ProgressBarFill.png), and borderedSecondary.pngpatch buttons. The master-detail preview reuses the realBountyRow.uitemplate for its list rows;CurrencyIconUtil-style icon fills drive the wallet/hotbar grids. No new i18n keys (preview labels are literal mockup text).
Technical: player-data seam - generic multi-domain accessor + PlayerSession context (partial)
- One within-tick lazy-create authority for every domain.
PlayerDataRepositorygained a generic+> getOrCreateComponent(store, cmd, ref, type, factory, onCreate) peekComponent(store, ref, type)carrying a single duplicate-add guard keyed by(ComponentType, ref.hashCode()); theSkillComponent-typed methods are now one-line forwarders.QuestService.getOrCreateQuestComponentroutes through it (its privatependingComponentsmap deleted), andMMOSkillTreeAPI.getAchievementComponent/getStatisticsnow read throughpeekComponent, so all four per-player components flow through the one seam. PlayerSessionis now the per-player context. It exposesskills()/quests()/achievements()/stats()(live components, lazily resolved through the seam),language()plus ergonomictr(key[,args])/msg(...)default methods (localize without dragging the 2,300-line component),markDirty(domain)/flush(), and online-only ECS sugar (store()/ref()/cmd()/playerRef()).EcsPlayerSessioncarries thePlayerHandlefields;tr/msgaredefaultso a future DB session inherits them free.- DB-readiness hooks. Added
updateOffline(id, domain, Consumer(the unified offline-write seam: applies in place when online, no-op offline under ECS, a direct row write under a DB backend) and a backend-announce log line in) setup()(the swap mechanism is the existing volatile field +setPlayerData). MMOSkillTreeAPIinternal DRY (public signatures frozen).getAllXp/getAllLevelsnow delegate to their*ByNamesiblings; the four weapon-filtered parry getters collapse into onegetParryBonus+ aParrySkillResolverfunctional interface.- Localization DRY + write hygiene.
Messages.getSkillNameByIdcollapsed onto itsgetSkillNameForLangByIdtwin;SettingsPagelanguage change is now single-writer throughplayers().setLanguage(direct component write kept only as a no-id fallback); the two throwawaynew SkillComponent()-to-localize sites on the combat damage path now resolve language via the O(1) locale projection (players().language(ref)+Messages.getForLang), allocation-free. - Behavior-identical under ECS; no save migration. The contract test (
PlayerDataRepositoryContractTest, run against both the ECS impl and the in-memory fake) was extended for the generic accessor +updateOffline. Still pending (tracked in.claude/plans/we-seem-to-have-starry-sedgewick.md): the broad sweep adoptingPlayerSession/tracross the remaining UI pages +Messages.get(skills,…)call sites, then deleting the deprecatedMessages(SkillComponent,…)surface.
Technical: hub two-panel rebuild + onboarding live previews + reroll/give fixes
- Per-slot reroll false-negative fixed.
SlotRerollEngine.excludeOtherskept the *current* pick in the candidate set, soWeightedSlotDraw.drawReplacement's weighted draw could re-pick the very item it was replacing;computeRerollPickthen returned null and the UI showed "nothing to swap to" even when distinct alternatives existed (deterministic per period, so an affected slot stayed stuck all period). NewSlotRerollEngine.excludeAllexcludes every current pick (including the rerolled slot's), and bothBountyService.computeRerollPickandTokenShopService.computeRerollPickuse it, so a reroll always lands on a genuinely different contract / offer (or reports "no alternative" only when the slot's pool is truly exhausted). /giverewards standardized on--quantity=N. Hytale's give command only accepts the named--quantity=Nform; a bare positional quantity (give {player} Ore_Iron 32) is ignored and delivers a single item. Every shipped command string, default, in-game help example, and pack reward now uses--quantity=N(an earlier draft had standardized the other way; that was wrong).GiveCommandParserstill reads a positional integer leniently so a misauthored command previews with the intended count for the inventory-space gate (QuestService.hasInventorySpaceForRewards/TokenShopService.hasInventorySpaceForGives), and the content audit flags the positional form asPOSITIONAL_GIVE_QUANTITY.- Bounty
/giveitems can't be lost. A bounty that auto-completes while the inventory is full parks inCOMPLETED_UNCLAIMED; since bounties are hidden from the quest log,BountyBoardPagenow shows a Claim button for that state (delegating toQuestService.claimQuest, which re-checks space), and the inventory-full notice for a bounty points the player to the board. This lifts the "CURRENCY + XP only" restriction for board (non-turn-in) bounties; the pack addsBounty_Cache_Copper/Bounty_Cache_Repairsas worked item-reward examples. - Hub rebuilt as a two-panel page.
MmoHubPage+MmoHubPage.uinow render aLeftCenterWrapgrid of icon tiles (newHubTile.ui, modeled onClassHubTile.ui; bounty boards + shops + core feature pages, each tile an item icon + title + subtitle + accent bar) beside a right panel that reusesBountyRowto preview the primary board's active bounties with a completed-count + token-balance header and a "View board" deep-link. The right panel is fixed-width and hidden (builtVisible:false) when no board is accessible, so the grid fills the row. Tiles + actions still route throughMmoUiRouter. Shared row rendering (icon / status dot / token balance) is extracted toBountyRowRenderer, reused by bothBountyBoardPageand the hub. - Onboarding previews are real, live data. The made-up skill rows, combat-level stat, hardcoded Gold/Menu-Scrolls wallet, and mockup reward icons are gone. New
PreviewKind.SKILL_CARDSrenders the player's top active skills as realSkillCards via a new publicViewXpPage.renderSkillCardsInto(the live skills-page build + populate path, read-only);PreviewKind.NODE_CARDSrenders two realSkillTreeChoiceCards via a new public staticSkillTreePage.appendChoiceCard(the skill-tree step shows perk choices, the abilities step shows ABILITY_UNLOCK choices from a combat capstone, picked distinct); the wallet lists the server's real sidebar currencies (CurrencyConfig.all()+CurrencyServicebalances viaWalletChipRow.ui). Both renderers gained aninteractiveflag so the previews bind no buttons.PreviewDashboard.ui/PreviewHotbar.uiare removed. - Onboarding title-contrast fix. Every
Preview*.uititle bar now stays dark with a thin accent strip (#TitleAccent, tinted per step) and near-white tag text, instead of tinting the whole bar to a bright accent under low-contrast gray text.
Technical: in-menu toast system
- New transport-agnostic toast engine (
ui/toast/):ToastKind(severity + opaque 6-digit panel/text colors + aNotificationStylemap for a future feed presenter), immutableToastSpec(kind/message/optional title/icon/duration),ToastRenderer(the single source of truth for the#MmoToast/#MmoToastTextselectors), a one-methodToastSurfacedismiss seam, andToastController(per-player show +HytaleServer.SCHEDULED_EXECUTORauto-dismiss). Player-facing pages extend a newpages/ToastablePagebase that binds the controller to the page'ssendUpdate; the engine + schema are reused verbatim by a futureCustomUIHud-based variant (only its surface lambda + host.uidiffer). - Why in-page, not a HUD/notification. The native
Notificationpacket has no z-order field, and a custom HUD renders *behind* an open page (page modal draws over the HUD layer), so the only layer guaranteed above a menu's content is the menu's own.uitree. Each migrated page's.uigains a#MmoToastblock as the last root-level sibling of$C.@Container(document order ⇒ drawn on top);build()callsrenderToastInto(cmd)right after the template append (covers early returns). - Show/hide mechanic (no reopen, scroll preserved). The toast is a fixed-width (440px), top-centered Hytale-bordered patch panel (
ContainerFullPatch; centered viaWidth*without*Horizontal, the nativeEventTitlepattern, addingHorizontalstretches it full-width); the kind shows through a bright text color (ToastKind.textColor()), since the runtime parser (ColorParseUtil) rejects the#rrggbb(alpha)markup form forcmd.set(...). The panel is alwaysVisible; show/hide toggles itsAnchor(full size ⇄ collapsed0×0) viasetObject, the engine-sanctioned "zero-size styling" that IS settable over a pagesendUpdate(unlikeVisible, which can't gofalse→true). So a toast appears and auto-dismisses (~4s) entirely oversendUpdate, with no page reopen, keeping the player's scroll position. Toast state lives in a static per-playerToastStorekeyed by uuid, so a page that *does* reopen for other reasons re-paints the toast fromrenderInto; a monotonic generation token drops stale dismiss clears. - Migration. The 11 player-facing pages (
BountyBoardPage,NpcQuestPage,TokenShopPage,QuestPage,SkillTreePage,ItemRewardsPage,AchievementPage,AchievementDetailPage,AchievementSummaryPage,BoostPage,AbilityBindPage) swap their in-pageNotificationUtil.sendNotification(...)action feedback forshowToast(ToastKind, msg)(re-using the sameMessages.get(...)strings). Skill-tree and item-reward claim success (previously a silent in-placesendUpdatewhose only feedback was the hidden corner feed) now also fire a SUCCESS toast - the in-place card update is kept (scroll preserved) and the toast rides its ownsendUpdatealongside it.NotificationUtil/MmoNotifystay the path for world/command/interaction events.MasteryPagekeeps its persistent inline#StatusLabel(a content-flow status, not a transient overlay) and is intentionally not migrated.
v1.2.0
Translations are reworked to use Hytale's standard .lang format. The 9 supported languages (English plus Spanish, French, German, Italian, Portuguese, Russian, Hungarian, Turkish) now ship as .lang files bundled inside the mod jar at Server/Languages/, discovered natively by Hytale's I18nModule.
Updates
- All 9 languages ship in the jar. No extra download required. The Settings menu language picker shows native names (English, Español, Français, Deutsch, Italiano, Português, Русский, Magyar, Türkçe) and switching is instant.
- Same translations as v1.1.6. Identical strings, just a different delivery mechanism. Existing server
messages-admin overrides keep working unchanged..json - Standard Hytale
.langformat. Translations are now in Hytale's native localization format. Any third-party.langtranslation pack installed on the same server can override or extend MMOSkillTree's keys without code changes. - Per-key English fallback unchanged. New keys added in this release (or any future release) that aren't translated yet still fall back to English at runtime, so partial translations work fine.
- New: Reward Notification toggles in Settings. A new Reward Notifications master toggle (on by default) with two sub-toggles underneath it: Skill Tree Reward Notifications (the "New {skill} reward available" popup that fires when a skill levels past a tree tier) and Item Reward Notifications (the "item reward available" popup for command/level rewards). Turn the master off to silence both, or flip just one. The rewards themselves are unaffected; you can still claim everything from the Rewards UI.
- New: Taming skill (with Alec's Animal Husbandry). A new Taming skill that levels up as you care for your tamed companions. It turns on automatically when Alec's Animal Husbandry (the companion mod, built on the Alec's Tamework API) is installed and stays completely hidden otherwise, so servers that don't use companions never see it. Feeding and watering your animals give a small steady trickle of experience, harvesting their drops gives more, and breeding gives the most. Combat experience is kept low to discourage grinding, and experience from damage your companions take is off by default. Server owners can retune every value in the Taming XP map.
- New: Taming content pack. A standalone MMO Skill Taming Pack ships the Taming skill's quests, achievements, repeatable daily and weekly goals, per-level experience-boost rewards, and the default Taming XP map. Install it alongside Alec's Animal Husbandry; all of its content stays hidden when Taming is not active.
Bug Fixes
- Fixed: XP gain popups showed an inflated total when earning XP in more than one skill at once. During combat (dealing damage for a weapon skill while taking damage for the defense skill, for example) the popup could read something like "+1,556 Block XP" while the bar only moved by the real amount (+68). Each skill now batches and displays its own gain, so the "+N" on every popup matches that skill's progress bar.
- Fixed: skill tree rewards could get stuck on a loading spinner when claiming on a heavily customized tree. On trees where tiers had been re-authored (rewards added, removed, or reduced), clicking Claim could leave the menu spinning forever with no response. Claims that can't be granted now show a brief "that reward isn't available to claim" message and refresh the tree instead of hanging. Rewards your class can't take no longer show a Claim button at all, and a customized tree can no longer fail to open.
- Fixed: fishing-rod catches now count toward fish achievements and statistics. Catching a fish with a rod used to advance only fishing quests. It now also advances fish-catch achievements and your lifetime fish-caught statistic, matching how fish caught by killing fish mobs already worked.
Technical: XP notification throttle keying
SkillService.addXpByNamenow keys theNotificationThrottleaccumulator per skill (XP_GAIN + ":" + skillId) instead of the single sharedXP_GAINconstant. The shared key conflated every skill's batched XP into one accumulator, so a flush triggered by a small award returned the combined cross-skill total and rendered it under the triggering skill's label (while the progress line read that one skill's live state). Per-skill keying makes each skill batch independently;NotificationThrottle.removePlayeralready drops the whole player entry, so the extra sub-keys need no separate cleanup. Stored XP was always correct - this is display-only.
Technical: skill tree claim resilience
- The skill tree claim handler now always sends a UI response. Previously every non-success path (a removed/missing node, a rejected reward, or any non-
NumberFormatExceptionexception) returned silently - so Hytale sent no page packet back and the client hung on its loading state. All failure paths now notify + refresh, and the handler catches all exceptions (logged viaMMOSkillTreePlugin.LOGGER) instead of only number parsing. - Claim buttons are gated on
ClassService.isSkillRewardAllowedat render time, so the UI never offers a claim the service will reject. SkillTreePage.build()filters null padding entries before sorting nodes by level - a customized config that defines a tier at/after the default range leftnulls in the node list, which NPE-d theComparator.comparingInt(SkillTreeNode::getLevelRequired)and prevented the page from opening - and renders its tier rows inside a guard so a malformed tree degrades to a partial page rather than failing to open.- Skill-tree override loading now forces each override node's tier to its JSON map key (new
NodeData.toNode(defaultChoicesRequired, skillId, forcedTier)overload). Hand-edited map-format overrides that omitted the innertierfield defaulted it to0, colliding with the real tier-0 node and routing claims to the wrong node.
Technical: Taming skill + Alec's Animal Husbandry integration (via the Tamework API)
- TAMING promoted to a feature-gated built-in skill. It was already defined in
SkillRegistry.BUILTIN_SKILL_DATAbut dormant (absent fromBUILTIN_SKILL_NAMES). It is now a first-class built-in carryingrequiresFeatures: ["taming"]. - Skills can now declare
requiresFeatures, mirroring quest / achievement gating. NewrequiresFeaturesonBuiltinSkillData(via a 5-arg convenience constructor so the other 21 skill definitions are untouched), onCustomSkill, and oncustom-skills.jsonSkillData, plus an optional top-levelbuiltinRequiresFeaturesmap so any skill's gate can be set from JSON.SkillRegistry.getRequiresFeatures/isSkillAvailableresolve them throughFeatureRegistry, where a new"taming"/"tamework"feature reports available only while the Tamework integration is active (IntegrationRegistry.hasActiveIntegration(IntegrationDomain.TAMING)). - Availability is enforced at the same chokepoints as
isBuiltinDisabled- theSkillService.addXpByNamehard guard (and the trigger-match loop),SkillComponent.getTotalXp/getSummedTotalLevel,SkillFilterUtil.getAvailableSkillIds(which feedsViewXpPage), and theLeaderboardPage+SkillTreePageskill enumerations. A gated-off skill earns no XP and is hidden from player UI, leaderboards, and totals; admin/config surfaces keep the raw list. - New
AlecsTameworkAdapterin a newTAMINGintegration domain (IntegrationDomain.TAMING+ markerTamingIntegration), registered inIntegrationRegistry. Reflection-only soft dependency (no compile-time coupling): resolves thecom.alechilles.alecstameworkplugin instance, gates on theCOMPANION_XP_EVENTScapability, and subscribes toCompanionXpAwardedEventthroughapi.events().subscribe(...). Each event'sCompanionXpSource(FEED/HARVEST/BREEDING/COMBAT_DAMAGE_DEALT/COMBAT_DAMAGE_TAKEN/CUSTOM) maps to flat TAMING XP read from theTAMINGxp-map, awarded on the world thread after a permission check. Compiles and runs cleanly when Tamework is absent. - New
TamingDefaults(registered inSkillDefaults) seeds theTAMINGxp-map keyed by source name (FEED 1, HARVEST 5, BREEDING 25, COMBAT_DAMAGE_DEALT 1, COMBAT_DAMAGE_TAKEN -1, CUSTOM 0); a0or-1disables a source. The Taming pack'sXpMapsasset overrides these, andmods/mmoskilltree/xp-maps.jsonoverrides the pack. - Removed the dead
GoneFishingAdaptersample/template (it targeted a fictional mod and was never registered) plus itsMETA-INF/servicesline. The real fishing integrations (HyFishingAdapter,AnglersAlmanacAdapter) and theFISHINGdomain are untouched. - No schema bumps (the
requiresFeaturesfields are additive/optional). Theskill.tamingdisplay name is auto-registered; the other 8 language files fall back to English per the existing convention.
Technical: unified progress dispatch
- One fan-out, one hook per subsystem. Quest, achievement, and statistics tracking now route through a single
ProgressEventsfacade (fire/fireObjectivesOnly/fireLevel) instead of every event source hand-callingQuestService.onX+AchievementService.onX+StatisticsService.onX. Each subsystem exposes one generichandleEvent(statistics via anObjectiveTypetoStatSpectable); the roughly 14 per-type hooks were removed.ObjectiveTypeis now symmetric, every type works as both a quest objective and an achievement criterion (previouslyGAIN_XP,ABILITY_CAST,EARN_CURRENCY,CLASS_SELECTED,PLAYER_DEATH,TAKE_FALL_DAMAGE,SPRINT_DISTANCE,SWIM_DISTANCEwere achievement-only andTALK_TO_NPCwas quest-only).QuestService.handleEventadvances by the event amount via newQuestObjectiveProgress.advance(long)(amount 1 reproduces the old per-event increment). - New companion-activity ObjectiveTypes + statistics.
AlecsTameworkAdaptermaps eachCompanionXpSourceto an objective type and fires it per event, independent of that source's XP value and the skill-XP permission:BREEDINGtoBREED_ANIMAL,FEEDtoFEED_ANIMAL,HARVESTtoHARVEST_ANIMAL,COMBAT_DAMAGE_DEALTtoCOMPANION_COMBAT(CUSTOMandCOMBAT_DAMAGE_TAKENleft unmapped). Each gets a matchingStatisticsComponenttotal (breeding_count,companions_fed,companions_harvested,companion_attacks). Additive enum values plus new string-keyed stat keys, so noSCHEMA_VERSION/ codec bumps; pre-update saves load identically. - Rod fishing parity fix.
HyFishingAdapter/AnglersAlmanacAdapteronly advanced fishing quests, so rod catches never counted toward fish achievements or statistics; routing them through the facade closes the gap (fish-mob kills already fanned out to all three).
Notes for server admins upgrading from 1.1.6
- No action required for translations. All 9 languages are bundled in the jar; just drop in the new
MMOSkillTree-1.2.0.jarand restart. - Existing
mods/mmoskilltree/localization/messages-files are untouched and continue to override the bundled translations per-key, per-language..json
Technical: backend migrated to Hytale-native `I18nModule`
LocalizationConfig.get()now delegates toI18nModule.getMessage(bcp47, key). The 1,165Messages.get(...)call sites stay unchanged. The resolution order isowner (mods/.../messages-. The composed-view rebuild, atomic volatile swap, and per-pack merge handler that landed mid-cycle were removed in favor of Hytale's own pack discovery pipeline..json) -> I18nModule (Hytale-native, JAR-bundled + every installed translation pack) -> EnglishDefaults (in-code map) -> "" LocalizationPackAssetdeleted. With Hytale'sI18nModuledoing the pack-discovery + merge work, MMOSkillTree no longer needs a custom asset type. Removed:LocalizationPackAsset.java,PackContentType.LOCALIZATIONS,PackControlAsset.Localizationsfield + codec entry +getModecase, theAssetStoreRegistrarregister block +CONTENT_CLASSESentry,AssetMergeHandlers.onLocalizationAssetsLoaded+ related imports, and theAssetCodecInitTestassertion. Net: ~80 fewer lines of Java, one fewer custom asset registration.LocalizationConfigslimmed to owner-only. Pack-layer storage (packMessages,packDisplayNames), the composed-view atomic swap, and the file-seeding-on-first-pack-install machinery are all gone; the class is now ~270 lines (down from ~350). The owner layer still loadsmessages-files from.json mods/mmoskilltree/localization/and overrides any valueI18nModulereturns. NewgetOwner(lang, key)exposes the raw owner-layer value for diagnostic surfaces.- 8
*Defaults.javadeleted.SpanishDefaults,FrenchDefaults,PortugueseDefaults,HungarianDefaults,TurkishDefaults,GermanDefaults,ItalianDefaults,RussianDefaultsremoved fromi18n/lang/. Their content lives as.langfiles atsrc/main/resources/Server/Languages/(bundled in the mod jar)./mmoskilltree.lang EnglishDefaults.javaand theLanguageDefaultsinterface stay. - 9 languages JAR-bundled as
.langfiles. The mod jar shipssrc/main/resources/Server/Languages/for all 9 supported locales. English is generated from/mmoskilltree.lang EnglishDefaults.javabyEnglishLangExportTest(a JUnit test gated on-DexportEnglishLang=true; intentional empty-string entries inEnglishDefaultsare skipped becauseLangFileParserrejects empty values). Hytale'sI18nModulediscovers all 9 via the sameServer/Languages/path used for packs, so the in-runtime lookup chain treats every language identically (single code path, no special-casing). The dual-presence of English (Java code +.langfile) keeps IDE autocomplete + refactor support on the canonical key list while making English visible to any other code that callsI18nModule.getMessage("en-US", "mmoskilltree." + key). mmoskilltree.key prefix. Hytale'sI18nModule.loadMessagesFromprepends each.langfile's name (minus.lang) as a dot-separated prefix to every key in the file. Naming our filemmoskilltree.langmeans Hytale loads keys asmmoskilltree..LocalizationConfig.lookupViaI18nprepends"mmoskilltree."before everyI18nModule.getMessagecall, so the 1,165 in-mod call sites still pass bare keys ("ui.settings.title") and the prefix is invisible to them.- One
mmoskilltree.langper locale. Same convention across the mod-jar resources. Sibling.langfiles would each get a different filename-prefix and break the lookup, so the consolidated single-file layout is the supported shape. - ISO 639-1 <-> BCP 47 bridge.
SkillComponent.languagecontinues to store ISO 639-1 codes (it,es,pt) for back-compat with existing player saves and the settings-page picker UI.LocalizationConfig.ISO_TO_BCP47translates at theI18nModuleboundary (it->it-IT,pt->pt-BR). No codec bump, no player-data migration. - Portuguese chose
pt-BR. Brazilian Portuguese was picked over European; players onpt-PTclients fall back via Hytale's own per-language fallback chain (or to English if both miss). - PowerShell zip build needs explicit directory entries.
I18nModule.loadMessagesFromPackgates onFiles.isDirectory(pack.getRoot()/Server/Languages). Java'sZipFileSystemreturnsfalsefor that check when the zip has only file entries. Gradle's JAR builder writes dir entries automatically; the[IO.Compression.ZipFile]API used in the pack'stools/build.ps1does not. The script now emits a directory entry for every ancestor path before each file. - No
SCHEMA_VERSIONbumps.LocalizationConfighas never tracked a config version; the change is additive on the consumer side and back-compat for existing owner files.
Technical: reward-availability notification toggles
- Three new
NotificationCategoryconstants (data/NotificationCategory.java):REWARDS(umbrella, parentnull, default on),SKILL_TREE_REWARDandITEM_REWARD(both parentREWARDS, default on). Mirrors the existingCOMBAT_EFFECTSumbrella, soisNotifEnabled's parent-chain walk suppresses both children when the master is off with no per-call-site logic. - Two ungated call sites routed through
MmoNotify.SkillTreeService.notifyNewRewards(was a bareNotificationUtil.sendNotification(packetHandler, message, hint)) now callsMmoNotify.send(skills, playerRef, SKILL_TREE_REWARD, message, hint). Both branches ofSkillService.checkAndNotifyItemRewardsByName(per-skill and total-level) now callMmoNotify.send(skills, playerRef, ITEM_REWARD, ...). The reward-unlock hint embedded in the level-up popup and the claim/reset confirmations are left ungated (contextual text and user-action feedback, not "waiting reward" nags). SettingsPage+.ui. ThreeTOGGLE_ROWSentries added (master + two indented children);build()/handleDataEvent()untouched since both loop the map. Three@SettingCardrows added toMMOSkillTreeSettingsPage.uibetween the Luck Drop and Online Status cards (children use thePadding: (Left: 30, ...)indent established by the combat sub-toggles).- i18n. 6 new keys in
EnglishDefaults.java(ui.settings.reward_notif/..._desc;..._skill_tree_reward_notif;..._item_reward_notif). Other 8 languages fall back per-key to English until translated. - No
SCHEMA_VERSION/ codec bumps. New categories are additional keys in the existingNotificationTogglesstring-serialized map, default-resolved when absent. No save migration.
v1.1.6
Adds three new player-level notification toggles in Settings. Ability Cast Notifications (off by default; ability cast popups can be noisy at high cast frequency), Passive Proc Notifications (on by default; see when one of your passives triggers), and Luck Drop Notifications (on by default; toggle the "Lucky!" popup that fires when a luck bonus grants an extra drop).
Updates
- New: Ability Cast Notifications toggle. Settings page, off by default. When off, the popup that appears each time you successfully cast an active ability is suppressed; sounds, particles, and cooldowns still fire normally.
- New: Passive Proc Notifications toggle. Settings page, on by default. Surfaces when your passive abilities trigger (perfect-dodge counters, on-hit, on-kill, on-finisher procs). Turn off if your build has many passives and the popups get noisy.
- New: Luck Drop Notifications toggle. Settings page, on by default. Controls the "Lucky!" popup that fires when a luck bonus grants you an extra drop from gathering or mob kills.
- Settings menu unchanged otherwise. Existing toggles (XP gains, combat effects, crit, lifesteal, defense, fall, combo, online status, quest notifications, language, XP threshold) behave exactly as before. Your saved preferences from 1.1.5 carry over.
Technical: unified notification-toggle system
- New
NotificationCategoryenum (data/NotificationCategory.java). Each constant declares a persistence key, a default-enabled state, and an optional parent for umbrella checks (e.g.CRIT's parent isCOMBAT_EFFECTS, so disabling the parent disables the child without per-call-site repetition). Replaces the previous "one boolean field per toggle" pattern that had accumulated 7 entries onSkillComponentplus their getters/setters/codec entries. - New
MmoNotifyhelper (util/MmoNotify.java). WrapsNotificationUtil.sendNotification(...)with the same four overload shapes (primary; primary+secondary; +iconPath; +itemIcon), each gated bySkillComponent.isNotifEnabled(category). Nullskillsfalls through to the raw send (admin paths where the target's component isn't loaded). SkillComponentrefactor. Deleted 7 boolean fields (showXpGains,showCombatEffects,showCritNotifications,showLifestealNotifications,showDefenseNotifications,showFallNotifications,showComboNotifications); replaced by a singleConcurrentHashMapkeyed bynotificationToggles NotificationCategory.key. New API:isNotifEnabled(NotificationCategory)(walks parent chain) /setNotifEnabled(NotificationCategory, boolean). Legacy wrapper getters/setters (getShowXpGains, etc.) kept as thin delegates so external code paths (XpDisplayCommandetc.) keep compiling, slated for removal once unused.- Codec back-compat. The 7 legacy
Show*KeyedCodecentries on theSkillComponentcodec stay registered but now route through the new map (the read lambda callssetNotifEnabled(...), the write lambda callsisNotifEnabled(...)). 1.1.5 saves load cleanly; 1.1.6 saves emit both the legacy keys AND the newNotificationTogglesstring-serialized map, kept in sync from the same source of truth. NoSkillComponentcodec version bump. - New
serializeBooleanMap/deserializeBooleanMaphelpers inSkillComponentfor the new map codec entry, using the samekey:value|key:valueshape as the adjacent boost/cooldown serializers. SettingsPagerewrite. The 7 hardcodedcommandBuilder.set("#ToggleX.Text", ...)/updateToggleButton(...)/eventBuilder.addEventBinding(...)blocks collapsed into a singlefor (entry : TOGGLE_ROWS.entrySet())loop driven by a staticLinkedHashMappopulated in a static init block. The 7 individualif ("toggleX".equals(data.action))arms inhandleDataEventcollapsed into oneif ("toggleNotif".equals(data.action) && data.category != null)arm that resolves the category viaNotificationCategory.byKey(...)and flips it on the component.SettingsEventDatagains acategorycodec field for the new arm.- Ability effect classes routed through
MmoNotify. 13 ability effect files (AoeSelfAbility,BeamAbility,BuffAuraAbility,DashAbility,GroundAoeAbility,HealSelfAbility,MarkAbility,NextHitBuffAbility,ProjectileAbility,RaycastDotAbility,TauntAbility,TeleportAbility,VanishAbility) now send their cast-success notification viaMmoNotify.send(skills, playerRef, def.isPassive() ? PASSIVE_PROC : ABILITY_CAST, ...).AbilityDefinition.isPassive()already existed (passives carry a non-nullpassiveTriggermap); using it as the category split means a single line covers both manual casts (viaActiveAbilityService.activate) and passive procs (viaPassiveAbilityTriggers.firePassive/firePassiveOnHit), since both paths run the same effect classes through the samedef. CombatDamageEventSystemgates simplified. Fiveif (skills.getShowCombatEffects() && skills.getShowXxx())umbrella checks (crit, lifesteal, defense block, fall reduction, combo) collapsed toskills.isNotifEnabled(NotificationCategory.XXX). Parent-chain resolution handles the umbrella automatically, removing the per-site&& showCombatEffectsrepetition.LuckUtil.notifyLuckBonusByNamegated onLUCK_DROP. Signature now takesSkillComponent skills(passed through from the existingtryLuckBonusByName/tryLuckBonusForBlockByNameentry points) and routes throughMmoNotify.send.- i18n. 6 new keys in
EnglishDefaults.java(ui.settings.ability_cast_notif/..._desc;ui.settings.passive_proc_notif/..._desc;ui.settings.luck_drop_notif/..._desc). Descriptions render withWrap: truestyling so longer copy wraps cleanly. Other 8 language files fall back per-key to English per existing convention until real translations land. - No
SCHEMA_VERSIONbumps.SkillComponentcodec is additive and back-compat; no config-level structural changes.
v1.1.5
Adds Content Packs - third parties can extend MMO Skill Tree by dropping a .zip of JSON into the server's mods/ folder. Quests, achievements, XP tuning, loot, requirements, and boost templates can be added or overridden by a pack, with no coding required.
Also verifies and declares Hytale Update 5 compatibility - the mod targets the Update 5 prerelease server line and the manifest now refuses to load on older or newer-major server builds.
Compatibility
- Hytale Update 5 verified. Manifest
ServerVersionis now pinned to>=0.5.0-pre.0 <0.6.0, declaring Update 5 prerelease (and any 0.5.x patch) as the supported range. Players on pre-Update-5 (<0.5.0) servers will see the mod refuse to load with a clear version-mismatch message, replacing the silent*wildcard that printed Hytale's *"plugin does not specify a target server version"* warning on every boot. - Audited and migrated against the Update 5 prerelease deprecation list. Update 5 silently removes several non-deprecation breakages on top of the listed deprecations (vector package relocated to
org.joml,Rotation3fintroduced as a distinct type fromVector3f,Player.sendMessage/Player.hasPermission/HudManager.setCustomHud/CustomUIHud(PlayerRef)constructor all gone). The mod now compiles cleanly against Update 5 with these migrations applied, while the still-functionalInventory-class accessors are left in place (Update 5 keeps them as thin wrappers) and queued for a future-release sweep.
Technical: Update 5 migration
- Vector package relocation.
com.hypixel.hytale.math.vector.{Vector3d,Vector3f,Vector3i}imports moved toorg.joml.*across 20 ability / projectile / beam / raycast files. The Hytale math.vector package now holds only the*Utilhelpers andRotation3f/Transform/Location. Vector fields are JOML-public (.x/.y/.z) -getX()/getY()/getZ()are gone on bothorg.joml.Vector3dandcom.hypixel.hytale.protocol.BlockPosition, soBreakBlockEventSystem/PlaceBlockEventSystem/RaycastTargetingwere switched to field access. - JOML clone semantics.
org.joml.Vector3d.clone()is inherited fromObjectand returnsObject, breaking the oldpos.clone().add(...)chain. Replaced withnew Vector3d(other)copy constructor inMarkAbility/RaycastDotAbility/ProjectileAbility/NextHitBuffAbility/AbilityProjectileQueue(5 files, 7 sites). - Rotation3f vs Vector3f split.
HeadRotation.getRotation()now returnsRotation3f(its own class with pitch/yaw/roll accessors) rather than aVector3f.Teleport(Vector3d, Rotation3f)andParticleUtil.spawnParticleEffect(..., Rotation3f, ...)updated their signatures to match -BeamChannelService.spawnDirectionalParticleandTeleportAbilitynow take/passRotation3fdirectly. - Player → PlayerRef routing for chat + permissions.
Player.sendMessage(Message)andPlayer.hasPermission(String)were removed fromPlayer; both still exist onPlayerRef. Allplayer.sendMessage(...)andplayer.hasPermission(...)call sites (291 total across 11 files -MMOConfigCommand,BoostCommand,AwardBoostPage, etc.) now go throughplayer.getPlayerRef().sendMessage(...)andplayer.getPlayerRef().hasPermission(...). Sites that already had aPlayerRefhandle in scope are unchanged. - HUD API renamed.
HudManager.setCustomHud(playerRef, hud)is gone - replaced withHudManager.addCustomHud(playerRef, hud)(per the deprecation page). UpdatedHudRegistrar'sMultipleHUDfallback path. CommandSenderis nowPlayerRef, notPlayer. Update 5 moves theCommandSenderinterface implementation fromPlayertoPlayerRef, so everyctx.sender() instanceof Playercheck in the command layer failed silently - in-game players got "Only players can use this command" for/xp,/mmoboost,/mmoquests,/mastery,/proving, every page command and admin command. All 23 affected commands were migrated: 13 player-only commands (extendingAbstractPlayerCommand) now use the already-injected@Nonnull PlayerRef playerRefparameter and fetch thePlayercomponent via the existingPermissionUtil.resolvePlayer(playerRef)helper; 9 console-aware commands (AbstractAsyncCommand/AbstractAsyncWorldCommandshapes -/mmoboost,/mmocurrency,/mmomastery,/awardxp,/bulksetxp,/mmoquestadmin, etc.) switched theirctx.sender() instanceof Playercheck toinstanceof PlayerRef+PermissionUtil.resolvePlayer(...);MmoPacksCommand's inline check switched to the same pattern.ProvingCommand(syncCommandBaseshape, no injectedplayerRef) reads the UUID directly off the newPlayerRefand no longer needs aPlayerreference.- CustomUIHud constructor takes a key.
CustomUIHud(PlayerRef)is gone - the new shape isCustomUIHud(PlayerRef, String key).AbilityCooldownHudnow declaresHUD_KEY = "mmoskilltree.ability_cooldown"and passes it tosuper(...). gradle.propertiesbuild target.hytaleHome+patchlinenow point atD:/Games/Hytale/install/pre-releaseso the project builds against the Update 5 prerelease SDK.server_version=>=0.5.0-pre.0 <0.6.0is templated intomanifest.jsonvia the existing${server_version}expansion (previously a literal*).- Cape
DamageResistanceenum rename. Update 5 renames theDamageResistance.*.CalculationTypeenum values fromMultiplicative/AdditivetoPercent/Flat(new classResistanceCalculationType). Only theArmor.DamageResistance.*[]blocks are affected -Armor.StatModifiers.*[](Health / Mana / etc.) andArmor.DamageClassEnhancement.*[](Light / Charged / Signature) still use the unchangedAdditive/Multiplicativevalues from theStaticModifierenum. All 20 cape asset JSON files in the plugin (40 rewrites) and another 20 in the standalone skill-capes-pack content pack (40 rewrites) had theirDamageResistanceblocks switched toPercent. Without this, every cape failed to load on server startup withCodecException: Failed to find enum value for Multiplicativeand no XP-bonus capes were equippable. - Deferred to follow-up release. The 33
Player.getInventory().getX()call sites (mastery / currency / quest / ability subsystems) still compile against Update 5 becauseInventoryremains as a thin wrapper. The full migration toInventoryComponent.getCombined(...)+ directHotbar/Storage/Backpackcomponent fetches is queued for a follow-up release as removals actually land upstream. Same for the 44Player.getPlayerRef()sites - the method is deprecated but functional, and the deeperstore.getComponent(ref, PlayerRef.getComponentType())migration is non-trivial in commands that don't already have aStorein scope.
Updates
- New: third-party content packs - author a standard Hytale asset pack (a
manifest.jsonplus JSON files underServer/MMOSkillTree/) and the server discovers and loads it automatically at startup. No Java and no admin setup - just drop it inmods/. - Eight content types supported - quests, achievements, XP maps, mob-kill XP, luck loot, item requirements, action requirements, and boost templates.
- Add or replace, per type - a pack adds to your content by default; with a small control file it can instead replace the built-in defaults for a type (for total-conversion packs). Content the server owner authored locally always takes priority over a pack, and is never dropped.
- Last pack wins on conflicts - if two packs define the same entry, the one loaded later applies; brand-new entries simply add.
- New:
/mmopacks list- see which content packs are loaded and how many entries each contributes, per type. - New:
/mmopacks export- bundle your server's custom quests and achievements into a shareable content-pack zip undermods/mmoskilltree/packs/. - Safe by default - content from untrusted packs cannot run server commands: command-style rewards (and achievement announcement commands) are stripped from pack quests and achievements unless the server owner opts in.
- Author guide - see
CONTENT_PACKS.mdand the ready-to-use example inexamples/sample-content-pack/.
Technical: custom asset types
- MMO Skill Tree now declares its own Hytale custom asset types and registers their stores in
setup()(beforeLoadAssetEvent), so third-party asset packs populate them natively. Newasset/AssetStoreRegistrarregisters one store per content type viaAssetRegistry.register(HytaleAssetStore.builder(...).setPath("MMOSkillTree/, each declaring")...) loadsAfter(PackControlAsset.class). - Content is delivered raw:
data/asset/AbstractRawJsonAsset(Id + Payload) subclasses (QuestAsset,AchievementAsset,XpMapAsset,MobKillXpAsset,LuckLootAsset,ItemRequirementAsset,ActionRequirementAsset,BoostTemplateAsset) carry each entry as the existing JSON, parsed by the config's existing parser - no schema duplication.AssetBuilderCodecis used (not Gson) for the asset layer. asset/AssetMergeHandlerslistens forLoadedAssetsEventper type and folds the full merged store into the matching config via a newmergePackLayer(...). Each handler rebuilds the entire pack layer fromevent.getAssetMap()(not the per-event delta) for idempotent hot-reload.- Per-config merge precedence is
defaults < pack < owner overrides. Directory-based configs (QuestConfig,AchievementConfig) snapshot a base set and recompute; override-based configs (XpMapsConfig,MobKillXpConfig,LuckConfig,ItemRequirementsConfig,ActionRequirementsConfig,BoostTemplateConfig) re-apply via a newAbstractOverrideConfig.applyPackLayer()hook called at the end ofload(). Plain-HashMapeffective maps were moved toConcurrentHashMapand merges aresynchronized(the merge runs on the asset-load thread). asset/PackControlAsset+PackControlRegistryresolve the per-type add/replace mode from each pack'sServer/MMOSkillTree/Control/("any pack declaring replace → replace")..json asset/PackSecurityPolicygates the command-reward stripping (locked by default).- New
ConfigPresetService.exportContentPack(...)writes a valid pack zip via the NIO zip filesystem (manifest + per-entry raw-payload files) with a round-trip self-test; surfaced throughcommand/MmoPacksCommand. - Excluded from packs in this release: custom skills and active abilities (load-order / code-backed), command-rewards config (RCE risk), and XP tokens (the config can only tweak built-in tokens, not construct new ones from data). Skill-tree and ability pack support are planned follow-ups.
v1.1.0
The largest release since v0.10. Three new player-facing systems land together - Currencies, Ability Mastery, and a Stat Source Breakdown page. Mastery ships paired with a separate MMOSkillMasteryPack content pack that holds the mastery tracks, currencies, mastery-point milestones, and mastery-themed quests + achievements - drop it into mods/ alongside the plugin. The content-pack format gains four new content-type families (Masteries, Currencies, CommandRewards, and per-type Templates with an extends + params + overrides + extras DSL) so pack authors can extend or replace any of it without writing Java. Reward feedback gains a full SFX palette. Mastery launches with deliberate balance: skill-investment gates, level-92 Eternals, repeatable endgame quests, and unique L100 capstones.
Updates
- Currencies - define as many as you want. New
mods/mmoskilltree/currencies.jsondefines named currencies in two flavours: item-backed (wraps a native Hytale item likeIngredient_Life_Essence- stackable, tradeable, drops on death) or counter-backed (pure plugin-tracked number, like XP). Caps, on-death loss, daily decay, and XP-to-currency conversion are all per-currency. Counter-backed currencies carry a cosmetic icon (Mastery Points shows a purple crystal in the wallet and sidebar)./mmocurrencymanages from console / script / in-game. - Earn currency through play. Award any currency from quests, achievements, and command/level rewards via a
CURRENCYreward type (currencyId+amount) or/mmocurrency give {player}. Offline players get queued payouts on next login. Reward cards display the currency icon; receiving a counter-backed currency pops a notification with icon + amount + new total. - Ability Mastery. New
mods/mmoskilltree/mastery.jsondefines purchasable upgrade tracks targeting either a single ability (target: ability:fireball) or a whole skill (target: skill:MAGIC). Each node costs currencies + optionally Hytale items + optionally a permanent stat sacrifice (Max HP/Stamina/Mana). Buffs are permanent. Linear, branching, and flat track shapes all supported by the same node/prereq/choice-group schema. Recursive AND/OR/NOT requirements gate every node and every track - same engine quests use. - In-game Ability Mastery page. Open with
/mastery, the Mastery tab in the main menu, the Mastery button on any skill's Skill Tree page, or - new - the purple MASTERY button on any ability card whose ability has its own mastery track. Master-detail layout: track list left, node tree right, wallet up top. Each track lays out by shape - linear tracks render top-to-bottom as a connected chain, branching tracks as a side-by-side "Pick 1" fork with an OR divider, open tracks as a free grid - so you can read its structure at a glance. - Node descriptions are dynamically generated. Each mastery node's summary is rendered from its actual modifier values + cooldown clauses + HP thresholds, so the description always matches what the node does and updates the instant a server owner retunes a number.
- Refund confirmation in Ability Mastery. Refunding a node asks you to confirm first, just like buying does - a misclick can't undo a purchase. The full-width Refund button is per-node only; the bulk-refund-track flow lives behind
/mmomastery respecfor admins. - Save toward a mastery node over time (partial contributions). On any unaffordable node, a Contribute max button per cost component deposits as much as your wallet has into the node's running total. Cost chips switch to
paid / owed; once everything is funded the BUY button becomes CLAIM. Items can be deposited piecewise. For pick-one (ANY) nodes, the first deposit locks the route until you respec. Respec refunds partial deposits at the same rate as a normal node refund. - Stat-sacrifice costs are real. A node can cost permanent Max HP (or Stamina/Mana). Buying it reduces your stat immediately across logins; refunding restores it. The confirm screen shows a before→after preview ("Max Health: 100 → 90"). A sacrifice that would drop you below the configurable safety floor (
statSacrifice.minRemaining, default 1) is greyed out. A one-time current-HP cost is labelled "(one-time)" to distinguish it from a permanent Max-HP cut. - Six modifier shapes. FLAT (+10 damage), PERCENT (+10%), OVERRIDE (cooldown = 1000ms), ADD_PARAM (inject pierceCount), CONDITIONAL (only at <30% HP, reuses the v0.17 passive-tier HP-range / filter logic), TRIGGER (subscribes the player to a triggered passive - the named ability must exist in
ActiveAbilitiesConfigas a passiveAbilityDefinition). - Configurable respec. Refund a node, whole track, or everything. Refund percentages configurable per node (e.g. 25% on a legendary), per track, or globally (default 50%). Repeatable nodes refund the latest purchase at its scaled cost. Refund buttons show the actual rate ("REFUND (50%)"). Admin override:
/mmomastery respec.[trackId] - Toggle off cleanly. Set
enabled: falsein either config to disable the system; player balances and owned-node lists are preserved across re-enable. The Mastery tab in every menu auto-hides when the system is disabled or has zero tracks configured. Same for the Currency surfaces. - Mastery page tells you why. A locked node lists every reason it's locked, not just the first; multi-prereq nodes show progress ("Prereqs: 1/2"); a tier gated by an earlier tier draws a connector that turns green once unlocked. AND/OR failures collapse to
Need one of: A or Bso OR groups read distinctly from AND. Currency icon hover shows the currency's name, not the underlying icon item's. - Standalone MMOSkillMasteryPack content pack. Mastery tracks, the two starter currencies (Mastery Points, Life Essence), the per-skill mastery-point milestones, and the mastery-themed quests + achievements ship in a separate Hytale content pack you drop into
mods/alongside the plugin. With the pack installed everything works as designed; without it, the Mastery and Currency UI tabs hide and no mastery-point rewards fire - degrades cleanly to a plugin without mastery/currency. - 20 default mastery tracks ship in the pack - 11 combat-skill (Swords, Daggers, Polearms, Staves, Axes, Blunt, Unarmed, Archery, Magic, Artillery, Defense), 6 marquee ability (Fireball, Meteor, Whirlwind, Piercing Shot, Shield Slam, Shadowstep), 3 gathering (Mining, Woodcutting, Harvesting). Each track has 3-5 finite identity nodes capped with one infinitely-repeatable Eternal node. Combat-skill Eternals give +0.5% damage per purchase; marquee ability Eternals each buff their own ability (Fireball +1% damage, Shadowstep -1% cooldown, Meteor -1% cooldown + +0.75% damage, etc.); gathering Eternals give +0.5% bonus-drop chance (
paramKey=lootMultiplier, additive into the existing skill-tree LUCK roll). Three additional tracks (Fishing, Enchanting, Acrobatics) ship"disabled": trueand activate in a future pack release without a plugin update. - Earn Mastery Points by leveling. Every 15 levels in any of the 20 skills you now earn 1 Mastery Point, claimable from that skill's rewards, up to the skill's max level. Spend them in Ability Mastery. The milestones are delivered by the pack - turn off the Mastery Points currency (or remove it) and the rewards automatically hide everywhere: rewards screen, unclaimed-glow indicator, claim button.
- Mastery skill-investment gates. Every track gates T2 nodes at the relevant skill level 40 and T3 capstones at 70 - so Mastery Points alone don't buy upgrades in a skill you've never picked up. Ability-scoped tracks gate on the parent skill (Fireball / Meteor → Magic, Shadowstep → Daggers, Shield Slam → Defense, Piercing Shot → Archery, Whirlwind → Axes).
- Eternal mastery - infinite, exponential grind. Every default track ends with one Eternal node that has no purchase cap and a cost that grows 10% per buy (50th purchase ≈ 117× base, 100th ≈ 13,780× - self-limiting plateau). Eternals gate behind skill level 92 (the last 8 levels unlock the infinite tail) and cost 1 Mastery Point on top of Life Essence + thematic essence items (Fire / Ice / Lightning / Void / Water / Crystal / Voidheart). MP cost scales with the same 10% ramp - stays at 1 MP for the first 5 buys, ticks to 2 at purchase 6. Combat Eternals start at 350 LE base; marquee abilities range 250-500 LE depending on track (Shadowstep 250, most 300, Meteor 400, Fireball 500); gathering Eternals start at 200 LE. Fireball uses a steeper 1.25× scaling (the most expensive endgame Eternal); Meteor's cooldown Eternal uses soft-capped scaling that plateaus cost at -70% CDR. Per-purchase effects vary per track: combat Eternals give +0.5% damage/buy, Fireball +1% damage/buy, Meteor -1% cooldown + +0.75% damage/buy, Shadowstep -1% cooldown/buy, gathering Eternals +0.5% bonus-drop chance/buy.
- Identity-node cost curve. Combat + marquee ability tracks use T1 = 600 Life Essence, T2 = 1500, T3 = 4000 (Meteor uniquely has a T4 at 8000 LE). Gathering tracks are lighter at T1 = 500, T2 = 1200, T3 = 3500. Each track is themed (Magic/Staves/Artillery → Fire Essence; Daggers/Shadowstep → Void Essence; Defense/Shield Slam → Water Essence; Mining → Crystal Blue; etc.). Capstones layer 1-3 Voidheart (Hytale's rarest item) on top.
- Inferno (Fireball capstone) is an "all of the above" gauntlet - requires Life Essence + Fire Essence items + a permanent Max-HP cut simultaneously, showcasing every cost component in one purchase.
- Magic Mastery L100 capstone: Archmagus. Beyond the Eternal node, Magic has a one-time Archmagus purchase: 30 Mastery Points + 8000 Life Essence + 5 Fire Essence + 5 Voidheart + a permanent -10 Mana sacrifice. Grants +15% damage on all magic abilities. Only Magic has this; other tracks ship the standard combat shape.
- Defense Mastery is a tank track. T1 Pick-1: Bulwark (+25% Shield Slam i-frame window) vs Charge (+25% Shield Slam dash range). T2 Pick-1: Banner Bearer (+30% Guardian's Call aura radius) vs Swift (-15% Defense ability cooldown). Blood Pact capstone extends Guardian's Call's aura duration +40%. Eternal grants +0.5% Shield Slam i-frame duration per purchase. Every node boosts a specific Defense ability rather than a generic damage/lifesteal stat.
- Eternal Cataclysm (Meteor) grants both -1% Meteor cooldown and +0.75% Meteor damage per purchase.
- Shadowstep Mastery T3 capstone: Voidwalk - 12 Mastery Points + 4000 Life Essence + 3 Void Essence + 2 Voidheart + a permanent -10 Stamina sacrifice. Grants -20% Shadowstep cooldown. Required prerequisite for the Shadowstep Eternal, matching every other ability mastery's T3-gates-Eternal pattern.
- New repeatable quest: Mastery Tithe - auto-accepts at login, 6-hour cooldown. Slay 10 mobs + chop 10 logs → 1 Mastery Point. Endgame-only: requires Total Level 500 + the new Mastery Hoarder achievement (earn 300 Mastery Points lifetime) before it unlocks, so the trickle stays an endgame faucet rather than an early-game boost.
- New achievement: Mastery Hoarder - 50 points + 5 Mastery Points bonus on claim, awarded for earning 300 Mastery Points lifetime. Counts cumulatively across milestone rewards, quests, and admin grants; refunds don't subtract.
- Stat Source Breakdown - see exactly which nodes feed each buff. A new STAT SOURCES button on the XP Overview's buff bar opens a per-stat breakdown - one row per skill-tree reward and mastery node feeding your Damage, Crit, Lifesteal, Combo, Block, Fall Reduction, XP Boost, Flat Damage, Flat Lifesteal, or Flat Combo total. Each row shows the source's name, its contribution, and any gating clause ("vs <25% HP", "filter: Fireball"). Master-detail layout: stat rail on the left lists every stat with its total at a glance, right panel shows the source rows. Reachable from the new STATS footer button on the Skill Tree and Mastery pages too; cross-nav buttons close the loop. Landing defaults to ALL skills + ALL stats so you can survey your build before narrowing. Conditional sources stay visible regardless of current HP; mastery rows annotate their weapon scope (e.g.
scope: Swords). - Pack-author content extensibility. Drop a
.zip(or folder) inmods/withServer/MMOSkillTree/Masteries/,Currencies/,CommandRewards/,MasteryTemplates/,QuestTemplates/,AchievementTemplates/, orCommandRewardTemplates/JSON to add or replace content without writing any Java. The sameextends + params + overrides + extrastemplate DSL works for all four template families. Concrete demo: the mastery pack's per-skill milestone file collapsed from 2,386 lines (240 identical reward objects) to 6 lines using one template + a single new{{ALL_SKILLS}}catch-all that fans the template to every skill. Explicit per-skill entries always win over the catch-all. SeeCONTENT_PACKS.md. - Quests and achievements can declare a feature dependency. A new optional
requiresFeaturesfield on any quest or achievement (or its pack template) hides the entry everywhere when a listed feature is disabled - no UI listing, no event progress, no auto-accept, no claim. Drop a mastery-themed quest into a pack with"requiresFeatures": ["mastery"]and a server with mastery disabled simply never sees it. Known feature ids:mastery,abilities,currency,elite_mobs,xp_tokens,command_rewards,item_requirements,action_requirements. Templates pass the gate down to extending entries; concrete entries can override (set[]to ungate). - Reward feedback now has its own sound. Claiming a skill-tree node, picking up a level-up command reward, unlocking an achievement, claiming an achievement / point milestone / quest, a quest becoming ready to claim, leveling up in a skill, and confirming a mastery purchase each play a distinct 3D cue at your character - skill-tree gets a mystical unlock chime, command rewards a coin chink, achievement unlocks a triumphant fanfare, claims a chest-open, quest completions a discovery flourish, ready-to-claim a lighter cue, level-ups a workbench-upgrade chime. Failed mastery buys stay silent.
Fixes
- Fixed a skill's XP-page card showing the orange "Reward Ready!" indicator for skill-tree rewards that aren't actually shown - for example ability rewards while active abilities are turned off server-wide. The indicator and the "next reward at Lv N" hint now only count rewards the player can really see and claim.
- Fixed cast-ability items showing an "Active abilities are disabled on this server" message every time they were used when active abilities are turned off. They now behave like ordinary items, matching how bound hotbar keys already behaved.
- Fixed misconfigured choice tiers - a tier set to require more picks than it offers (e.g. "Pick 3 of 2") - showing a nonsensical header, never turning green, and keeping the "Reward Ready!" indicator lit forever. Such a tier now caps at the number of rewards actually on offer and completes once those are claimed. Correctly-configured multi-pick tiers also keep the indicator lit until *every* pick is taken, instead of clearing after the first.
- Fixed the Reset a Skill scroll picker listing every configured skill regardless of admin or permission setup. The picker now applies the same filter as the XP screen - skills the admin has disabled via the custom-skills config, and skills the player has no permission to gain XP in, are hidden, so players only see skills they can actually train.
- Fixed tree-specific triggered passives - Archmage, Coup de Grâce, Skewer, Butcher's Cleaver, Pummel, Drunken Master, Clean Kill, Pyromaniac, Sniper, and Power Strike Mastery - firing regardless of what weapon you were wielding, and forcing their parent tree's XP onto whatever you hit next. For example, Archmage's spell-echo would arm when you hit with a sword and then push Staves XP onto your follow-up swing. Each of these passives now only procs when you're actually holding the matching weapon class, and if a buff is still armed when you weapon-swap mid-window, the follow-up hit's XP follows the weapon you're swinging - not the passive's source tree. Punishing Dodge stays weapon-agnostic on purpose, and the Lv 100 capstones were already gated this way.
- Fixed several admin and player commands crashing with an internal error when typed in-game by a player (they worked fine from the server console).
/mmoboost,/mmoawardxp,/bulksetxp,/mmocurrency,/mmomastery,/mmoclass,/mmoquestadmin,/mmoquestui,/mmofingerprint,/mmopacks, and/mmoachadminnow run reliably whether typed in-game or from the console./mmoclass listalso now shows your current class when run in-game.
Technical: command thread-safety (off-world-thread Player resolution)
- Root cause.
AbstractAsyncCommand/AbstractAsyncWorldCommandrunexecuteAsyncon aForkJoinPool.commonPoolworker, not the world tick thread. Eleven commands resolved the sender'sPlayerentity at the top ofexecuteAsyncviaPermissionUtil.resolvePlayer((PlayerRef) ctx.sender()), before anyworld.execute(...).resolvePlayerends inStore.getComponent, whose always-onassertThread()throwsIllegalStateException: Assert not in thread!off the world thread, so every player-sender invocation crashed. Console senders skipped the call (isConsole), which is why console-dispatched commands and quest-dispatched/mmoawardxpnever tripped it. - Fix. Added store-free
PermissionUtil.hasAdminPermission(PlayerRef)(admin check viaPlayerRef.hasPermission, no entity access) andPlayerLookupUtil.resolveWorld(PlayerRef)(resolves viaPlayerRef.getWorldUuid()+Universe.getWorld, replacing the duplicated per-commandresolveWorld(Player)copies). Each affected command now captures only thePlayerRefon the ForkJoin thread (permission check, activator username, and world resolution are all connection-level, store-free) and resolves thePlayerentity lazily insideworld.execute(...)only where genuinely needed (page opens, self-status, class select / advancements). - Commands updated.
BoostCommand,AwardXpCommand,BulkSetXpCommand,MmoAchievementAdminCommand,MmoMasteryCommand,MmoCurrencyCommand,MmoClassCommand,NpcQuestUICommand,QuestCommand,FingerprintCommand,MmoPacksCommand.AbstractPlayerCommandsubclasses were already safe (the framework hands them the Store on the world thread). - Also fixed.
MmoClassCommand.runList's "Current class" line was a silently-swallowed off-thread component read that never displayed for player senders; it now reads insideworld.execute.MmoPacksCommandalso dropped a latent permission bypass where an unresolved sender entity (player == null) skipped the admin check.
Technical: pack content split + content-pack extensibility
- Three new pack content types - Masteries, Currencies, CommandRewards. New asset types
MasteryAsset,CurrencyAsset,CommandRewardAsset(data/asset/, all extendAbstractRawJsonAsset+ reuserawCodec(...)). Per-entry shape for Masteries + Currencies (one file per track / currency); per-pack map shape for CommandRewards (one file per pack; payload is the full{skillId: {level: [rewards]}}map). Registered inAssetStoreRegistrar.registerAllatMMOSkillTree/Masteries/Currencies/CommandRewards; each declaresloadsAfter(PackControlAsset.class). NewPackContentType.MASTERY/CURRENCY/COMMAND_REWARDSconstants.PackControlAssetgainedMasteries/Currencies/CommandRewardsPascalCase fields (per theKeyedCodecupper-case rule guarded byAssetCodecInitTest). - Four new template families -
MasteryTemplate,QuestTemplate,AchievementTemplate,CommandRewardTemplate. Each follows the same wiring shape: a*TemplateAsset extends AbstractRawJsonAssetindata/asset/, a*TemplateResolverinconfig/(pure static, no plugin-runtime deps so it's unit-testable), anAssetStoreRegistrarentry where the content asset declaresloadsAfter(PackControlAsset.class, *TemplateAsset.class), anAssetMergeHandlers.on*TemplateAssetsLoadedhandler, aPackContentType.*_TEMPLATEconstant, a PascalCase*Templatesfield onPackControlAsset, and amergePackTemplateLayer(layer, replace)+rebuildTemplates()pair on the consuming config that re-runs the apply path so existing extending entries pick up template edits. JsonTemplateUtil(DRY). Shared substitution + merge primitives extracted into one place:substituteString,substituteParamsInPlace(empty-resolve drops the holding key - lets templates declare optional fields callers can opt out of),deepClone,deepMergeInto, plus two id-array helpersapplyIdArrayOverrides/appendToIdArray. All four resolvers (MasteryTemplateResolver,QuestTemplateResolver,AchievementTemplateResolver,CommandRewardTemplateResolver) delegate to it. Mastery / Quest / Achievement share the id-array override pattern (nodes/objectives/criteriakeyed byid); CommandRewards uses string-keyed wholesale-replace on its level map. Per-type field naming stays explicit:nodeOverrides/extraNodes(mastery),objectiveOverrides/extraObjectives(quest),criterionOverrides/extraCriteria(achievement),levelOverrides/extraLevels(CommandRewards).{{ALL_SKILLS}}sentinel in CommandRewards pack payloads. New top-level key on a CommandRewards Payload, handled byCommandRewardsConfig.applyPackRewardsMap: fans the supplied value out to every skill inSkillRegistry.allSkillIds()not otherwise explicitly listed in the same Payload. Explicit per-skill entries always win;TOTALandGLOBAL_SKILLare not inallSkillIds()and are never touched. Drove the mastery-pack milestone file from 2,386 → 6 lines.MasteryConfig.SCHEMA_VERSION1 → 2 andCommandRewardsConfig.SCHEMA_VERSION1 → 2 (both additive). MasteryConfig adds two optional per-node JSON fields:maxPurchases(null/1= legacy single-buy,-1= infinite,>0= capped) andcostScaling(object:mode = EXPONENTIAL|POLYNOMIAL,multiplier, optionalsoftCap). New value classdata/CostScaling.javawith staticscaledCost(base, count, scaling). Legacy single-purchase identity nodes round-trip unchanged (both fields default null). CommandRewardsConfig's bump covers the new extends-form pack shape (optional; literal"form and the owner-side": [rewards] command-rewards.jsonare unchanged).MasteryConfig.validate()post-load pass rejects any repeatable node carryingcost.statSacrifice != nullor an OVERRIDE/CONDITIONAL/ADD_PARAM/TRIGGER modifier (would brick characters or zero out per repeat).QuestConfig/AchievementConfighave no schema version; their parse paths are additive-only.- Three new merge handlers in
AssetMergeHandlers-onMasteryAssetsLoaded,onCurrencyAssetsLoaded,onCommandRewardAssetsLoaded. Each rebuilds the full pack layer fromevent.getAssetMap()(idempotent on hot reload) and folds it into the config singleton via that config's newmergePackLayer(layer, replace).MasteryConfig+CurrencyConfigcapture abaseDefaultssnapshot at end ofloadDefaults()(now empty post-migration) and recompute the effective set frombase + pack + owner-overrideson every merge call. Reader threads see a synchronized swap.CommandRewardsConfigfolds each pack's payload into the effective skill / total / global maps last-pack-wins, then re-appliesskillOverrides/totalLevelOverrides/globalSkillOverridesso the owner always wins.applyPackLayer()runs at the end of everyload(boolean)path so reloads idempotently re-overlay the cached pack content. - Mastery template extension.
MasteryAssetnowloadsAfter(PackControlAsset.class, MasteryTemplateAsset.class)so tracks see templates at parse time.MasteryTemplateResolverresolution: deep-clone template → walk every string value substituting{{paramName}}(empty param drops the holding key - lets templates declare optional fields likecombatTargetthat Magic/Artillery opt out of) → overlay track-level fields (target/displayName/icon/refundPercent/requirements win over template) → applynodeOverrides(deep-merge by post-substitutionid; arrays replace wholesale) → appendextraNodes(collisions logged + skipped). Unknown templates log a warning and the track drops from the effective set. Template id lookup is case-insensitive. Canonical templateCombat6_Standard.jsonships in the pack; 11 thin combat tracks now ~20-40 lines each (vs ~210 pre-template). The 9 non-combat tracks stay hand-authored. First production use ofextraNodes: Magic'smag_archmagusL100 capstone (30 mastery_point + 8000 life_essence + 5 Fire_Essence + 5 Voidheart, -10 permanent Mana, +15% damage on all magic abilities). - Pack content removals & migrations. Deleted
MasteryDefaults.javaandCurrencyDefaults.java; their content now lives as pack JSON. RemovedaddMasteryPointMilestones+masteryPointRewardfromCommandRewardsDefaults- the pack supplies them viaCommandRewards/MMOSkillMasteryPack.json(a 6-line payload via theMasteryPointMilestonestemplate +{{ALL_SKILLS}}sentinel).MasteryConfig.writeReferenceData+CurrencyConfig.writeReferenceDataemit a notice pointing pack authors at the pack JSON.MasteryConfig.loadDefaults/CurrencyConfig.loadDefaultsare no-ops.MasteryConfig.isAvailable()+ newCurrencyConfig.isAvailable()gate UI surfacing on!effective.isEmpty(), so menu tabs auto-hide when the pack isn't installed. requiresFeaturesfield onQuestDefinition+AchievementDefinition(and both pack-template JSONs). Lenient parse viaQuestConfig.getStringList(...)- bare string or array, both accepted. Templates pass the field through;JsonTemplateUtil.deepMergeIntolets a concrete entry replace it (set[]to ungate) or inherit it. Newutil/FeatureRegistry.java- staticisFeatureAvailable(String)switches over the known id set (mastery,abilities,currency,elite_mobs,xp_tokens,command_rewards,item_requirements,action_requirements); unknown ids return false + log once per id.allAvailable(Listis the AND-combined predicate. New) QuestDefinition.isAvailable()+AchievementDefinition.isAvailable()=isEnabled() && FeatureRegistry.allAvailable(requiresFeatures). Every filter site that previously calledisEnabled()now callsisAvailable()-QuestConfig.buildIndex/getQuestsByCategory/getActiveCategories/getQuestsByNpcId,QuestServiceauto-accept,AchievementConfigindex +metaParentsByChild+validateAndIndexChains+ various getters,AchievementServicecascade unlock + manual claim.isEnabled()is preserved for raw-config introspection + the meta-validation child-existence check.QuestDefaults.questToJson+AchievementConfig.achievementToJsonround-trip the field. NoSCHEMA_VERSIONbump on either config.requiresCurrencyIdonCommandRewardEntry(new builder field; the pack-supplied mastery reward sets"mastery_point"). NewCommandRewardService.isRewardCurrencyAvailable(reward)returns false whenrequiresCurrencyIdis set andCurrencyConfig.isEnabled() && has(id)is false. Enforced at every surfacing point:ItemRewardsPage.hasDisplayInfo/parseRewardForDisplay,ViewXpPage.hasUnclaimedItemsByName/hasUnclaimedTotalItems(the unclaimed glow),CommandRewardService.claimSingleReward(defensive). Gating preserves the index-basedskillKey_level_indexclaim IDs; per-skill mastery levels hold only the mastery reward so a hidden level renders no card and the existing empty-tier skip drops the row entirely. Serialized throughRewardData(requiresCurrencyJSON field). Newreward.currency.mastery_pointlocalization key.- Currency icon on reward cards.
RewardPreviewBuilder.from(CommandRewardEntry)parses/mmocurrency give(and the/currencyalias) and emits aRewardPreviewwhose icon is the currency'sgetIconItemId()(falling back togetHytaleItemId()), with the grant amount as the item-grid stack count - so the existing#CardIconGridpath renders the live currency icon. Falls through to text when the currency defines no icon item. - Currency-gain notification.
CurrencyService.giveCountertoasts the player via theNotificationUtil.sendNotification(handler, title, subtitle, ItemWithAllMetadata)item-icon overload whenever the balance increases (added > 0). Icon item id isgetIconItemId()/getHytaleItemId()withquantity = 0(keeps toasts distinct - noxNgrouping). Counter-backed only; item-backed currencies already surface as inventory pickups. Reached by every grant path. Newnotify.currency_gained/notify.currency_balancekeys. - New
EARN_CURRENCYObjectiveType + achievement dispatcher. Generic currency-earned trigger (target = currency id) added toObjectiveTypeso achievements likepack_mastery_hoarder_300(earn 300 mastery_point) fire off lifetime grants. NewAchievementService.onCurrencyEarned(store, ref, playerRef, currencyId, amount)follows the existingonMobKill/onBlockBreakdispatcher pattern.CurrencyService.give(...)now snapshots balance before + after, computes the post-cap delta, and dispatches whendelta > 0; refactored to privategiveImpl(...)sorefund(...)skips dispatch (refunds aren't earned). Achievement dispatch failures are caught + logged atFinerso they can never break a currency grant. - Quest accept-gates support
requiredAchievements+requirements.minLevelfor Mastery Tithe. Tithe's payload picks up the existing top-levelrequiredAchievements: [...](merges into the root prerequisite group's achievement leaf, evaluated byPrerequisiteGroup.evalAchievementagainstAchievementComponent.isUnlocked) and arequirements.minLevel: 500block (compared againstSkillComponent.getSummedTotalLevel()perQuestService.canAcceptQuest). No QuestConfig parser change - both fields already existed. - PackSecurityPolicy removed. Deleted
PackSecurityPolicyand thestripCommandRewards/stripPackSecuritypaths fromQuestConfig+AchievementConfig. Custom commands are the legitimate use case for pack-supplied rewards; the strip was misaligned.CONTENT_PACKS.mdupdated to reflect the no-strip stance. - Pack content additions.
sst_t3_voidwalkadded toskill-mastery-pack/Server/MMOSkillTree/Masteries/Shadowstep_Mastery.json(-20% Shadowstep CD, -10 STAMINA permanent_max, requiressst_t2_phantom); added tosst_eternal.prerequisites. NewPack_Mastery_Tithe.jsonquest (autoAccept: true,repeatable: true,cooldownSeconds: 21600,autoClaimRewards: false,sortOrder: 42; KILL_ENTITY × 10 + BREAK_BLOCK × 10 wood-CONTAINS → 1 mastery_point). All 20 active mastery tracks carryrequirements.skillLevelsgates (T2 = 40, T3 = 70, T9 = 92) wired through the existingPrerequisiteGroup.skillLevelsleaf. Eternals carrymastery_point: 1incost.currencies, scaling via the pre-existingcostScalingconfig (1.1× multiplier rounds the MP cost up at purchase 6). - 18 new resolver tests - 9 in
MasteryTemplateResolutionTest, 9 inCommandRewardTemplateResolutionTest. Substitution, empty-param key-drop, override deep-merge, append + collision detection, unknown-template warn-and-skip, unknown-level warn, case-insensitive id lookup, end-to-end shape tests.
Technical: data + service layer
- New data + config + service classes.
data/AbilityModifier.java(6 shapes),data/AbilityModifierRegistry.java(claim source),service/ability/AbilityModifierService.java(cast-time aggregator with pluggableModifierProducer; patches theAbilityDefinitionper cast so handlers read modifiers transparently via existingdef.getNumber(...)calls - zero handler changes).data/MasteryTrack.java,data/MasteryNode.java,data/MasteryCost.java,data/MasteryStatSacrifice.java,data/MasteryStatSacrificeRecord.java,data/CostScaling.java,config/MasteryConfig.java,service/MasteryService.java.data/CurrencyDefinition.java,config/CurrencyConfig.java(schema v1),service/CurrencyService.java.data/RequirementCheck.java+data/EvalContext.java(shared evaluator return + per-player context bag). New commands:MmoCurrencyCommand,MmoMasteryCommand. - PrerequisiteGroup unified.
quest/PrerequisiteGroup.javaextended with leaf categories (skillLevels,totalLevel,playtimeSeconds,kills,deaths,achievementPoints,masteryNodes,currencyBalances,lifetimeCurrencySpent) andMode.NOT. Staticparse(JsonObject)is the single JSON parse path.QuestService.evaluatePrerequisitesrewritten to build anEvalContextand delegate toPrerequisiteGroup.evaluate- single source of truth for all requirement gating.evalMasteryNoderesolves trackId/nodeId to the node's display name. OR-mode failures collapse to a singleNeed one of: A or Bsummary; AND groups still list each item on its own line. The mastery node card shows a conciseLOCKEDstatus with the full requirement text wrapped in#NodeLockReasons. - Legacy AbilityMod removed.
data/AbilityMod.java+data/AbilityModRegistry.javadeleted.SkillTreeService.getAbilityModsForremoved. v1.0.5magic_arcane_missiles_empowered/_siphonclaims migrate to the new shape viaAbilityModifierRegistry.legacy(...). SkillComponentcodec extended with fieldsCurrencies,LifetimeCurrencySpent,PurchasedMasteryNodes,PermanentStatSacrifices,MasteryContributions. All additive - pre-1.1.0 saves load with the new fields empty; no migration.purchasedMasteryNodesvalue semantic flipped from timestamp to purchase count (codec shape unchanged,Map); a one-shot> migrateLegacyMasteryNodeCountshelper clamps any value> 1_000_000(presumed v1 ms-epoch) back to 1 on player-ready. New transientmasteryLootMultiplierCache(Map) memoizesMasteryService.getLootMultiplierForSkillresults per uppercase skill id;purchase/respecNode/refundContributions/migrateLegacyMasteryNodeCountsall callskills.invalidateMasteryCaches().MasteryServicepurchase/respec is count + scaling aware. NeweffectiveCost(skills, track, node)computes the scaled-for-next-purchase MasteryCost once per call, threaded throughcheckCost/contribute/hasAnyContributableComponent/isFullyFunded/remainingOwed/drainCost.purchaseincrements viamerge(nodeId, 1L, Long::sum).respecNodeon a repeatable refunds only the latest purchase atcost.scaled(count - 1, scaling)and decrements; on count == 1 the legacy full-removal path fires (restoring stat sacrifice + clearing ledger).respecTrackloopsrespecNodeper node up toeffectiveCapso the full integralΣ scaled(base, k)is refunded step by step.produce()andsumSwingModifiersemit one count-scaled modifier per owned modifier (not N copies) - equivalent math via additive FLAT/PERCENT, constant modifier list size at Eternal grind. NewscaleByCount(AbilityModifier, long)preserves every scope field while multiplyingnumberValue.drainCostreturns the paid route so anANY-combine node paid in currency no longer also charges the sacrifice (latent over-recording bug).MasteryService.forEachOwnedModifier(Collection- single shared walker over every owned, SkillComponent, ModifierVisitor) (track, node, modifier, purchaseCount)tuple.produce,sumSwingModifiers, andgetLootMultiplierForSkillshare it. Future modifier-scope aggregators drop in as 6-line visitor callers instead of duplicating the four-level nested loop. The walker takes the track collection as an argument so tests don't have to populateMasteryConfig.getInstance().- Partial-contribution ledger.
SkillComponent.masteryContributionsis a nestedConcurrentHashMapkeyedtrackId → nodeId → componentKey → deposited(componentKeyiscurrency:oritem:). Additive codec entry - pre-1.1.0 saves load empty; no migration. Serialization uses brackets + braces + semicolons to disambiguate the per-component=from thecurrency:/item::prefix.MasteryService.contribute(skills, track, node, ctx, componentKey)depositsmin(playerBalance, remainingOwed)viaCurrencyService.take(currency) orremoveItemStack(item).Combine.ANYnodes lock to a single route on first deposit; subsequent deposits to other components are rejected with"Locked to." isFullyFunded: ALL needs every component fully deposited, ANY needs at least one.checkCostsubtracts already-paid amounts before checking wallet; for ANY it ignores all but the locked route's key when any deposit exists.drainCostonly drains the remainder.respecNodewipes the contribution entry after refund (avoids carrying a staleANYroute-lock); newrefundContributions(...)refunds a partial deposit without touching ownership. Item draining replaced the legacy whole-listremoveItemswith per-itemremoveItemStackcalls. - Stat-sacrifice - typed ledger + entity modifier. Replaced the prerelease pipe-and-colon String ledger on
SkillComponent.permanentStatSacrificeswith a typedList(5 fields: trackId, nodeId, stat, mode, amount). The codec's on-disk format is unchanged (trackId:nodeId:stat:mode:amount|...viaCodec.STRING) but the deserializer is strict - malformed records are logged and dropped rather than silently parsing to 0. Dead helpersparseSacrifices+parseIntSafedeleted.MasteryServiceapplies a negativeMAXStaticModifier(keyedmmoskilltree:mastery:) through newStatModifierUtil.applyStatBonusByName/removeStatBonusByName(Store/Ref + Holder) /getMaxStatByName/adjustCurrentStatByNamehelpers - reuses the existing health/stamina/mana modifier path, no new EntityStatMap code. Session-scoped: re-applied onPlayerReadyEventviareapplyStatSacrifices(validates ownership, prunes orphan ledger records, lifts everything when the system is disabled), removed onPlayerDisconnectEventviaremoveAllStatSacrifices, restored byrespecNode. Floor configurable viastatSacrifice.minRemaining(per-stat, default 1).PERMANENT_MAXpersistent path;PERMANENT_CURRENTone-shot current-value drain. - TRIGGER modifiers wire into PassiveAbilityTriggers. A mastery node carrying
{"shape":"TRIGGER","targetAbility":"grants the player a triggered-passive subscription for the named ability - the ability must already exist in"} ActiveAbilitiesConfigas a passiveAbilityDefinitionwith its ownpassiveTriggermap (event / filter / weapon scope / HP gates). NewMasteryService.contributesAbilityViaTrigger(skills, abilityId)query, memoized onSkillComponent(transient set, cleared byinvalidateMasteryCacheson every mastery mutation).PassiveAbilityTriggers.eligible(...)widens its unlock check fromskills.hasUnlockedAbility(id)to… || MasteryService.contributesAbilityViaTrigger(skills, id)- both unlock paths fire the passive identically.AbilityModifierService.resolvestill skips TRIGGER shapes in the per-cast pipeline (they're subscription markers, not value mutations). - Currency earning paths.
QuestRewardgained an optionalcurrencyId(new 12-arg ctor; legacy 11-arg delegates withnull).QuestConfig.parseReward+AchievementConfig.parseRewardparse + reject CURRENCY rewards missingcurrencyId/amount.QuestRewardExecutor.executeCurrencyReward(previously log-only) dispatches/mmocurrency giveviaCommandExecutor.executeAsConsole(online) orPendingQuestRewardStore.queueRewardByName(offline). Achievements + point-milestones reuse the same executor - one method covers all three reward paths. Console dispatch bypasses the player-only admin gate on/mmocurrency. paramKey="lootMultiplier"- mastery-only convention. NewMasteryService.getLootMultiplierForSkill(skills, skillId)sums PERCENT modifiers with this paramKey + matchingtargetSkillId(count-scaled).LuckUtil.tryLuckBonusByNameandtryLuckBonusForBlockByNameadd this value to the baseSkillTreeService.getLuckChanceByNamechance - single roll, single inventory write, single "Lucky!" notification regardless of which source pushed the player over. No newRewardType.MasteryModifierRenderer.noun()learns thelootMultiplier → loot_multipliermapping; new English-only keymastery.mod.noun.loot_multiplier = "bonus-drop chance".- Counter-backed currency icon.
CurrencyDefinition.iconItemIdcosmetic field (display-only item id for the chipItemGrid, independent ofhytaleItemIdbacking). Defaultmastery_pointships withiconItemId=Ingredient_Crystal_Purple. NoCurrencyConfigschema bump (additive optional field). MasteryConfigreference serializers now emit node/trackrequirements(full recursivePrerequisiteGroupvia newserializeRequirements), modifiertargetSkill/targetAbility/valueBool/valueString, andCONDITIONALcondition/apply(newserializeCondition) - previously all silently dropped, so the pack's reference file now round-trips every showcased feature.- 15 new tests in
MasteryServiceTestcovering the walker contract (visits, skip rules, modifier order, orphan handling), aggregator visitor closures (loot filter + produce coalescing), the stat-sacrifice ledger codec (round-trip, empty/null handling, record matching), and TRIGGER-grant ability-id collection. First unit-test coverage for mastery code.
Technical: mastery & currency UI + reward SFX
- New
pages/MasteryPage.java(master-detail) + UI files:MasteryPage.ui,MasteryTrackCard.ui,MasteryTierRow.ui,MasteryNodeCard.ui,MasteryCostChip.ui, plus shape-specificMasteryTierRowChain.ui(SEQUENTIAL: spine rail + per-tier node dot + connector segments forming a 1→2→3 chain),MasteryTierRowChoice.ui(CHOICE: centered fork + PICK 1 badge, withMasteryOrDivider.uiappended between siblings and unchosen siblings dimmed).MasteryNodeCard.uiis shared across all three structures. Bare/mmomastery(alias/mastery) opens it.#TabMasterynav tab added toViewXpPage/BoostPage/QuestPage/AchievementPage/AbilityBindPage.SkillTreePagegained a contextual Mastery button backed byMasteryConfig.getFirstTrackForSkill. Re-open-on-mutate + 400ms action-cooldown debounce mirrorSkillTreePage/SingleSkillResetPage. MasteryTrack.Structure {SEQUENTIAL, CHOICE, OPEN}- derived (memoized) from the node graph: anychoiceGroup→ CHOICE; single prerequisite-chained column (one node per tier, every tier past the first gated) → SEQUENTIAL; otherwise OPEN. Pure derivation over the in-memory node list - no new config field, noSCHEMA_VERSIONbump.MasteryPage.buildTrackDetailpicks the tier-row template per structure. Added#TrackStructurecaption toMasteryPage.ui.service/ability/MasteryModifierRenderer- mastery counterpart toAbilityDescriptionRenderer. Generates a localized node summary from each node'sAbilityModifierlist (FLAT/PERCENT/OVERRIDE/ADD_PARAM/CONDITIONAL, with ms→s cooldown conversion and HP-threshold clauses), reusingAbilityDescriptionRenderer.fmtand a compact template + noun table rather than a key per param×shape. Costs/sacrifices intentionally not described here (the page renders cost chips). Removed every hardcodeddescriptionstring from the pack mastery JSONs; the page falls back to a node'sdescriptiononly when generation is empty.MasteryConfig.isAvailable()(enabled && !tracks.isEmpty()) gates#TabMasteryvisibility + open-handler in all five menu pages - wasisEnabled()only. The tab auto-hides when mastery is disabled or has zero tracks. Same gating viaCurrencyConfig.isAvailable()for currency surfaces.SkillTreePage's contextual Mastery button was already track-aware (getFirstTrackForSkill != null).- Mastery node card display.
appendCostChips(...)reads the contribution ledger and renderspaid / owedon each chip when progress exists.bindContributeButtons(...)populates up to 4 "Contribute max (...)" buttons in a new#ContribRowgroup (per-currency + per-item; hidden when funded or wallet empty). For ANY nodes with a route-lock, only the locked component is offered. Buy button text flips to CLAIM (ui.mastery.claim) whenisFullyFunded. Refund buttons readMasteryConfig.resolveRefundPercent. Locked nodes render allcanPurchasereasons in#NodeLockReasons; multi-prereq nodes show#PrereqProgress; prerequisite-chained tiers show a#TierConnectorstub (green once owned, gray while locked).sacrificeTextis mode-aware (-N Max Statvs-N Stat (one-time)). Currency chip tooltips callItemGridSlot.setName(...)with the currency's display name (not the underlying icon item's). NewUIUtil.bindButton(...)7-arg overload (action + two key-value pairs). NewhandleDataEventaction"contribute"(+doContribute) carries{NodeId, Pay=componentKey}. - Refund confirm step (node-level). New
Pending {NONE, BUY, REFUND_NODE}mode onMasteryPage; the node#RefundBtnbinds torefundAsk(opens a confirm) instead of executing immediately, withrefundConfirm/refundCancelhandlers. NewbuildRefundConfirmPanelreuses the buy#ConfirmPanelslot (fullbuild()re-open so.Visibletoggles and bindings stay valid) with a destructive-styled#RefundConfirmBtn. Page-level#RefundTrackButtonand itsrefundTrack*flow removed (MasteryService.respecTrack/respecAllremain - still used byMmoMasteryCommand).#RefundBtnis nowFlexWeight:1(full card width). New English-only keysui.mastery.refund_confirm_title/.refund_confirm_hint/.refund_confirm_yes. - Item-icon hover tooltips fixed across the mastery page. Track-rail, track-header, node-card, and confirm-panel icons resolve to an ability/skill stand-in item; tooltips previously read the raw item name (e.g. "Purple Crystal Shards"). A shared
namedIconSlots(...)helper now sets each slot's name to the track or node display name. - Reward SFX wiring. New
util/RewardSoundUtil.java- twoplayToPlayer(...)overloads (Ref, or+ Store PlayerRef-only with internalplayerRef.getReference()resolution) plus 8 sound-id constants. Lifts theTransformComponent → position + AbilitySoundUtil.playSoundboilerplate into a shared util any reward path calls in one line. Wired at 8 dispatch points, each co-located with its existing colored toast:SkillTreeService.claimRewardByName(SFX_Memories_Unlock_Local),CommandRewardService.executeSingleReward(SFX_Coins_Land),AchievementService.applySingleUnlock(SFX_Capture_Crate_Capture_Succeed_Local),AchievementService.claimAchievementRewards+claimMilestoneRewards(SFX_Chest_Legendary_Open),QuestService.checkQuestCompletion(ready-to-claim →SFX_Discovery_Z1_Short; auto-complete →SFX_Discovery_Z2_Short),QuestService.claimQuest(SFX_Chest_Legendary_Open),SkillService.handleLevelUpByName(SFX_Workbench_Upgrade_Complete_Default).MasteryPagepurchase / contribute paths use the same util viaMASTERY_CONFIRM_SOUND/MASTERY_CONTRIBUTE_SOUNDconstants.CommandRewardService.executeSingleRewardsignature widened to takeStore(8 in-file callers; method is+ Ref private static). NewQuestService.notifyQuestEvent(...)helper collapses the two near-duplicate ready-to-claim / auto-complete toast paths. Failures swallowed at FINE-level viaAbilitySoundUtil's existing guarantee - a missing or misnamed asset never breaks a reward dispatch. - Reward-visibility DRY. New
util/RewardVisibility.javacentralizes the skill-tree reward visibility rule:isVisibleReward(SkillReward, Player)(lifted fromSkillTreePage),visibleRewardCount(SkillTreeNode, Player),hasVisibleReward(...),effectiveChoicesRequired(SkillTreeNode, Player)(returnsmin(node.getChoicesRequired(), visibleRewardCount(...))- the single clamp point for misconfigured "pick N of M where N > M" tiers). Hides orphanABILITY_UNLOCKrewards and all ability rewards while the feature is disabled (bypass admins excepted).ViewXpPage.getRewardStatusByNamenow takes thePlayer, skips nodes where!hasVisibleReward(...), and treats a tier as done whenclaimedCount >= effectiveChoicesRequired(...).SkillTreePageclampsrequiredCounttoMath.min(node.getChoicesRequired(), visibleCount)via the shared helper. Newutil/SkillFilterUtil.javacentralizes the "skills visible on a per-skill UI page" chain (XP map → not disabled inCustomSkillsConfig→ player hasmmoskilltree.skill.permission);ViewXpPage.getFilteredSkillIds/hasPermissionForCategory/SingleSkillResetPage.buildSkillCardsdelegate to it (the reset scroll picker previously only checked the XP-map gate). - Cast-item disabled guard.
MmoAbilityCastInteractionearly-returns (InteractionState.Finished, no toast) whenActiveAbilitiesConfig.isEnabledFor(playerRef)is false, mirroringHotbarAbilityPacketFilter. The cast item behaves like an ordinary item./mmoabilitycommand path still reportsDISABLED. - i18n.
ui.mastery.*keys added toEnglishDefaults(other 8 langs fall back per-key, matching the abilities UI convention). Includesclaim,node.funded,contribute_max,contribute_progress,contribute_locked_to,contribute_need_more,contribute_deposited,refund_pct,refund_track_pct,sacrifice,sacrifice_preview,sacrifice_note,sacrifice_current,prereq_progress,node.locked,refund_confirm_title/_hint/_yes,step,or. Plusmastery.mod.*,mastery.structure.*for the dynamic node descriptions.
Technical: stat-source breakdown
service/StatSourceAggregator.java- read-only enumeration kernel parallel to the existing sum aggregators.StatKeyenum centralizes per-stat plumbing (reward types + masteryparamKey/shape/isPerSkillper stat).StatSourcerecord per row (origin, sourceId, label, value, context).collect(skills, key, skillId)walksSkillTreeConfig.getSkillTreeByName × claimed-ids × node.getChoiceById(mirrorsgetActiveRewardsForSkillByNameso owning-tree attribution survives) for tree rows, thenskills.getPurchasedMasteryNodes() × track.nodesById × node.modifierswithmod.appliesToSwing(...)for mastery rows. CONDITIONAL mastery modifiers are unwrapped and described into the row context rather than gated. Repeatable-node purchase count is honored (owned.getOrDefault(nodeId, 1L) × leaf.numberValue). Newsum(skills, key, skillId)returnscollect(...).stream().mapToDouble(StatSource::value).sum()- used at UI build time once perStatKeyfor the rail (10 calls) + once per skill for chip totals (~20 calls).collectMasterySourceswidened:skillId == null(ALL mode) iterates every owned modifier with swing scope via newhasSwingScope(mod)helper (mirrorsappliesToSwingminus the weapon-match step); each row carriesdescribeScope(mod)(scope: Swords/scope: All Combat).pages/StatBreakdownPage.java- drill-downInteractiveCustomUIPagein master-detail layout. Left#StatRaillists everyStatKey(plus an "All stats" pseudo-row) baked asinto a single+X% TextButton.Textso the click target stays the whole row. Right#DetailPanelis mode-aware: drill-in lists one stat's sources; landing mode (statKey == null) lists every stat with at least one source, grouped underStatSourceSectionHeader.uirows.StatKey statKeyis@Nullable; 1-arg landing constructor delegates to(playerRef, SKILL_ALL, null). New constantSKILL_ALL = "*".handleDataEventcases:back→ViewXpPage,tree→SkillTreePage(skillId),mastery→MasteryPage(firstTrackForSkill.id),selectStat→ re-renders with newStatKey,selectSkill→ re-renders. Synthetic "All" chip at slot 0 bindsselectSkillwithSkillId == "*". Each chip's text is"when drilled into a stat; landing mode omits the suffix. Footer Tree + Mastery hide when+X%" skillId.equals(SKILL_ALL). Grand-total label in#HeaderRowhides in landing mode (mixed units across percent + flat). Container width 1100 → 1280.- Five new UI files:
StatBreakdownPage.ui,StatBreakdownRow.ui,StatBreakdownChip.ui,StatSummaryRow.ui(rail row - single full-widthTextButton),StatSourceSectionHeader.ui(section header for landing-mode grouped view). SharedtotalColor(double)helper paints rail rows + section-header totals + chip totals:#4aff7fpositive,#ff7f7fnegative,#6a7a8ezero. The summary rail row's text color picks up the value's polarity when inactive (negative contributions like FALL flag at a glance). - Entry points.
ViewXpPagegets aTextButton #StatSourcesBtnin#FlatBuffsBar(right of the FlexWeight filler) openingnew StatBreakdownPage(playerRef). The handler also resolves the equipped weapon's combat skill viaCombatWeaponUtil.resolveCombatSkillFromItem(same resolverpopulateBuffsBaruses), falls back to the first filtered skill id when the weapon resolves toUNARMED.SkillTreePagegets#StatsButtonnext to#MasteryButton.MasteryPagegets#TreeButton+#StatsButtonnext to#BackButton; visibility resolved per track viaresolveTrackSkillId(track)(SKILL-scope returnstrack.targetId, ABILITY-scope returns the firstdef.getXpSkills()entry from the ability definition; null hides both buttons). BreakdownEventDatacodec inner class renamed fromEventDatato remove the long-standing name shadowing againstcom.hypixel.hytale.server.core.ui.builder.EventData. The existing FQ-name workaround is kept on event-construction sites for clarity, but the import collision is gone.- No
SCHEMA_VERSIONbumps and no save migration. The aggregator is a read-only walk over live data; nothing is persisted. The page is opened on-demand viaopenCustomPage(ref, store, this); no startup wiring. Newui.statsource.*keys inEnglishDefaults(title, stat/skill labels, empty state, origin chips, 10 stat-name keys, 4 nav button labels) +.skill.all,.stat.all,.rail.header,.detail.title_all,.scope_label. Other 8 langs fall back per-key.
v1.0.5
Restored Mark of Death to the Daggers Lv 33 choice, added a new Magic Lv 47 choice that buffs your Arcane Missiles spell, and introduced an underlying "ability mod" reward type that lets future skill-tree picks tweak how an existing ability casts.
Updates
- Restored: Mark of Death is back at Daggers Lv 33. The execute-flavored brand was wired into the Daggers tree at Lv 33 (paired against Smoke Bomb in a Pick 1 of 2 choice) but the ability itself had been cut for VFX/SFX polish. It's restored now: brand a target below 50% HP, hit them up to 2 times for a +50% damage bonus, and the second hit detonates for an extra 25 shadow damage. Costs 6 stamina, 18-second cooldown.
- New: Magic Lv 47 choice tier - pick how your Arcane Missiles cast. Both choices modify your existing Arcane Missiles spell - the ability id you bind doesn't change, but its numbers do based on which pick you take:
- Empowered Missiles - each dart of your Arcane Missiles deals +15 damage (5 → 20 per dart, up to +45 burst if all three connect). Mana cost +8 per cast (3 → 11). Cooldown unchanged. The pure-burst pick.
- Siphon Missiles - each dart that hits heals you for 3% of your max HP (up to 9% per cast if all three connect). Mana cost +4 per cast (3 → 7), cooldown +3 seconds (4 → 7s). Damage unchanged. The sustain pick - ideal for prolonged solo encounters.
- You can only have one active at a time; reset the Magic tree to swap.
Technical: ABILITY_MOD reward system (engine)
- New
RewardType.ABILITY_MODenum value plus matching passive handler registration inRewardEffectRegistry(no immediate-application work; queried per-cast). The reward'sabilityIdFilterfield carries the target ability id, so the existing aggregator gate (skip when target ability isn't unlocked) works without code changes. - New value type
data/AbilityMod.javacarrying additive deltas (damageDelta,costDelta,cooldownMsDelta,onHitHealPercent) plus the target ability id. ProvidesEMPTYconstant and a functionalplus(other)for additive merging. - New
data/AbilityModRegistry.javastatic registry mapping reward ids toAbilityModpayloads. Pre-populated with the two L47 mods (magic_arcane_missiles_empowered,magic_arcane_missiles_siphon). Reward claims store only the reward id; the deltas live in the registry, so save data andSkillRewardcodec are unchanged. - New aggregator
SkillTreeService.getAbilityModsFor(skills, abilityId)- mirrorssumFilteredRewardsiteration (every skill, every node, every claimed reward), filters toABILITY_MODrewards whose registry payload targets the requested ability, and reduces them viaAbilityMod.plus. Honors the standardhasUnlockedAbility(filter)gate so claiming the mod before the base ability silently no-ops until both are present. ProjectileAbility.execute(...)now readsgetAbilityModsFor(skills, def.getId())early and appliesdamageDeltato the projectile damage value (flat-add, before salvo enqueue so every dart in the salvo benefits). WhenonHitHealPercent > 0the per-castonHitchain gets an extraLIFESTEAL_HITconsumer appended viaOnHitEffects.fromParams(Map.of("type", "LIFESTEAL_HIT", "amountPercent", pct), ...)- chains naturally viaBiConsumer.andThen.ActiveAbilityService.tryActivate(...)now reads the same mod aggregate once, computeseffectiveCost = def.getCostAmount() + mods.costDelta, and uses it for both the insufficient-resource check and theAbilityCostUtil.draincall. Cooldown stamping now addsmods.cooldownMsDeltato the base beforeCOOLDOWN_REDUCTIONscales it (so CDR still discounts modded abilities proportionally).OnHitEffects.buildLifestealHit(...)extended with an optionalamountPercentparameter (0.0 - 1.0 fraction of the caster's max HP) alongside the existing flatamount. When both are set the heal sums (flat + percent × maxHp). New private helperOnHitEffects.readMaxHealth(store, ref)mirrorsCastConditionEvaluator.readHealthPercent's pattern (readsEntityStatMap→DefaultEntityStatTypes.getHealth()→EntityStatValue.getMax()). Class-level Javadoc updated to documentamountPercent.
Technical Details
- Uncommented the
mark_of_deathaddDefault(...)block inActiveAbilitiesConfig.java(was in a/* ... */block under a// CUT 0.17.0 prerelease - VFX/SFX polish; restore later.marker). The Daggers Lv 33 choice node inSkillTreeDefaults.javawas already live referencing the ability id, and the English i18n keys (ability.mark_of_death.name/.flavor) were already present - uncommenting the impl was the only required change. - New MAGIC
SkillTreeNode(displayTier=19, levelRequired=47, choicesRequired=1, ...)inSkillTreeDefaults.getMagicDefaults()between the existing T17 (L43) and T18 (L67) blocks. Both child rewards useSkillReward.forAbility(id, RewardType.ABILITY_MOD, 0, displayName, description, "arcane_missiles")so the unlock gate fires correctly. SkillReward.getFormattedValueandMessages.getRewardFormattedValueForLangswitches updated to treatABILITY_MODlikeABILITY_UNLOCK(empty value string - the reward'sdescriptionfield carries the human-readable mod summary).Messages.getDescriptionValuesimilarly grouped.- No
SCHEMA_VERSIONbumps. Reward claims serialize as the reward id string, so existing save formats handleABILITY_MODclaims with no migration. The newamountPercentLIFESTEAL_HIT param is additive (existing flat-amountcallers stay byte-identical).
Technical: ability damage uses ability's combat skill AND ability id
- Fixed a long-standing damage-bonus mismatch: casting an ability without holding the matching weapon (e.g. Arcane Missiles cast bare-handed, or with a sword equipped) silently dropped both:
- Combat-skill targeted rewards (e.g.
magic_dmg_6=+6 Damage (Magic)) -CombatDamageEventSystemresolved combat skill exclusively from the held weapon, so MAGIC-targeted rewards were filtered out byappliesToCombatSkill(UNARMED)(orappliesToCombatSkill(SWORDS)etc.) and never landed. - Ability-filtered rewards (e.g.
magic_passive_arcane_focus=+6 Damage on Arcane Missiles Hits, an ability-filtered FLAT_DAMAGE) - thegetFlatDamageBonuspredicate filters byconsumedAbilityId, which is set when a NEXT_HIT_BUFF slot is consumed on a melee swing but isnullfor direct ability damage (PROJECTILE / AOE_SELF / DASH / BEAM / GROUND_AOE / etc.). Without an ability id, theappliesToAbility(null)check excluded any reward whoseabilityIdFilterwas set. - Two coordinated changes plumb the ability through:
-
AbilityDamageOrigin.Payloadrecord gained a third component:@Nullable String abilityId. All eight construction sites updated to passdef.getId()(orarmed.abilityIdfor the armed-state surfacing inCombatDamageEventSystem.java:304):ProjectileAbility,AoeSelfAbility,DashAbility,BeamAbility,GroundAoeAbility,NextHitBuffAbility,SpawnAoeAtVictimEffect, plus the armed-buff surfacing site. -
CombatDamageEventSystem.applyAttackerEffectsnow derives aneffectiveAbilityId = consumedAbilityId ?? AbilityDamageOrigin.current().abilityId()and passes it togetFlatDamageBonus(wasconsumedAbilityId),getFlatComboFinisherBonus, and thePassiveAbilityTriggers.onFinisher/.onHitDealtcalls. NEXT_HIT_BUFF consumes still take precedence over the origin abilityId (their armed-buff math should win on melee swings). - New
resolveCombatSkillForDamage(...)helper (added in the previous fix) readsAbilityDamageOrigin.current().xpSkillsfor combat-skill resolution when an ability dispatched the damage; falls through to held-weapon for vanilla melee. - Both fixes are surfaced once at the top of
applyAttackerEffectsand flow through every downstream aggregator (getDamageBonusForSkill,getCriticalChanceForSkill,getFlatDamageBonus,applyLifesteal,getComboFinisherBonusForSkill,getFlatComboFinisherBonus, plus the on_hit_dealt / on_finisher passive triggers). All aggregators already iterate the materializedclaimedRewardsCacheviasumFilteredRewards→forEachClaimedReward, so the fix adds zero new allocations or tree walks per damage event - one ThreadLocal read total. - Verified: vanilla melee (no origin) keeps held-weapon resolution + null abilityId; NEXT_HIT_BUFF consumed during melee swings keeps the consumed-ability behavior because
consumedAbilityIdwins over origin.abilityId in the fallback expression; abilities withxpSkills=["$HELD"]resolve via held weapon for combat-skill purposes.
Technical: ability tooltip - collapse on cards, expand on hover
AbilityDescriptionRenderer.renderCard(...)now collapses the per-passive list to a single "Boosted by your passives" indicator when any matching rewards are claimed. The full per-reward breakdown stays inrender(...)(the hover tooltip), so cards remain scannable when many tree picks affect the same ability.SkillTreeService.getClaimedRewardsForAbility(...)widened to a new def-based overload that returns the union of (a) rewards whoseabilityIdFiltermatches the ability id (ABILITY_MOD picks, ability-filtered FLAT_DAMAGE) AND (b) rewards whosecombatTargetmatches the ability's primary skill (the first non-HELDentry indef.getXpSkills()) and whose reward type contributes to ability output (FLAT_DAMAGE, FLAT_LIFESTEAL, FLAT_COMBO_DAMAGE, STAT_DAMAGE, CRITICAL_CHANCE, LIFESTEAL, COMBO_DAMAGE, COOLDOWN_REDUCTION). The hover tooltip's damage math now lines up with whatCombatDamageEventSystemactually applies at hit time - e.g., Arcane Missiles at L50 withmagic_dmg_6claimed shows the+6 Damage (Magic)line alongside the Empowered Missiles mod.- ABILITY_MOD entries render with the reward's description (not the empty formatted-value), so the line reads as
• Empowered Missiles: Each Arcane Missile dart deals +15 damage. Costs +8 mana per cast. - Back-compat string overload
getClaimedRewardsForAbility(skills, String abilityId)retained for callers without anAbilityDefinitionin scope; uses the narrower abilityIdFilter-only logic. - One new English i18n key:
ability.desc.passives.collapsed("Boosted by your passives. Hover for details."). Other 8 langs use English fallback per existing convention.
Technical: claimed-reward iteration optimization
- Introduced a transient materialized
ListonclaimedRewardsCache SkillComponentplus a lazy-rebuild accessorgetClaimedRewardsCache(). Built on first read by walking each skill's tree once and resolving every claimed reward id viaSkillTreeNode.getChoiceById; subsequent reads return the cached list directly. Field istransient- never serialized; cache is rebuilt on next read after codec deserialize via the field-initializer defaultclaimedRewardsCacheDirty=true. - Cache invalidation wired into all five mutator paths on
SkillComponent:claimRewardByName,removeClaimedRewardByName,resetRewardsByName,resetAllRewards,clearClaimedRewardsForTierByName. Each callsinvalidateClaimedRewardsCache()after mutating the underlyingclaimedRewardsmap.clone()produces a fresh instance so the dirty flag's field-initializer default covers it without an explicit hook. - New shared iterator
SkillTreeService.forEachClaimedReward(skills, Consumerreads the cache and applies the standard ability-unlock gate (skip rewards whose) abilityIdFilterreferences an ability the player hasn't unlocked). Aggregators no longer need to redo that gate themselves. - Three hot global aggregators refactored to call
forEachClaimedRewardinstead of duplicating the per-skill / per-node / per-claim walk:sumFilteredRewards(the helper behindgetFlatDamageBonus/getFlatLifestealBonus/getFlatComboFinisherBonusand several others),getActiveRewards, andgetAbilityModsFor. Combat damage events that previously hit 4+ aggregators × O(all-tree-nodes) per swing now run 4+ aggregators × O(claims), with the cache shared across the per-frame burst. - Per-skill aggregators (
getActiveRewardsForSkillByName,validateAndRevokeRewardsByName,getClaimedRewardsForSkillByName) and rare-call administrative paths (reapplyStatRewardson config-version change,backfillUnlockedAbilities,reconcileAbilityState,cleanupOrphanedRewardsByName) intentionally NOT refactored - they walk only one skill's tree (per-skill ones) or run off the hot path (administrative ones); going through the cross-skill cache would add filter cost without a hot-path payoff.
v1.0.4 - May 2026
A new consumable scroll for resetting a single skill of your choice via a picker UI, plus a configurable rested XP system.
Updates
- New: Skill Reset Scroll. A second reset-scroll variant that lets you pick exactly one skill to reset instead of wiping everything. Hold to use, pick a skill from the menu (each shows how many rewards you've claimed), confirm, and only that skill's claimed rewards are unclaimed and refunded. XP and level are preserved exactly like the existing all-reset scroll. Closing the menu without confirming costs nothing — the scroll is consumed only on confirmation. Skills with zero claimed rewards are visibly disabled in the picker so you don't waste a scroll on them. The existing Skill Tree Reset Scroll (resets all skills on a 3-second hold) is unchanged and continues to work alongside this one.
- New: Rested XP. Skills now accrue a per-skill bonus XP pool while the player is logged out. The next time the player earns XP in a skill with a pool, that gain pays double (or whatever the admin sets) until the pool drains, then returns to normal. Admins choose the accrual model from a new dropdown on the admin page: Off (feature disabled), Flat XP/hr (a fixed amount of rested XP per hour offline, applied to every skill), % to next level/hr (scales with each skill's progression — a level 99 player at 100 XP from level-up gets a tiny pool, a level 5 player gets a meaningful pool), or Hybrid (both contributions added). Default is Flat at 1500 XP/hr per skill, capped at 50000 per pool, multiplier 2×, with a 168-hour (7-day) cap on offline time credited to keep long-vacation pools sane. Each skill card on the XP page now shows a small
Rested 50,000badge in cyan when the pool is non-zero. A chat notification on login summarizes how many skills accrued. Toggle the entire system off via the admin UI's "Rested XP > Mode > Off" or/mmoconfig restedxp off. - Fixed: Tracked Quests panel lingered when the quest system was disabled. Server owners who turned off quests still saw a
TRACKED QUESTSheading and the "Track quests from the Quests page to see them here." placeholder on the right side of the overview screen, advertising a feature players couldn't actually use. The panel now hides cleanly when quests are disabled, matching how the Tracked Achievements panel already disappears when achievements are off. - Fixed: ability and passive-ability rewards still appeared in the skill tree when the abilities feature was disabled. Server owners who turned off the hotbar abilities master toggle still saw ability unlock cards (both active hotbar abilities and triggered passives) listed on the skill tree, where claiming them produced no effect. Those cards are now hidden whenever abilities are disabled, and any tier whose only contents were ability unlocks is skipped entirely so no empty header rows are rendered.
- New: per-player bypass permissions for the Quests, Achievements, and Abilities master toggles. Three new permission nodes —
mmoskilltree.bypass.quests,mmoskilltree.bypass.achievements,mmoskilltree.bypass.abilities— let admins, QA staff, or selected testers use these features as if they were globally on, even when the server has the corresponding feature toggled off. Bypass holders see the full UI (tabs, nav buttons, tracked-item panels), and ability/passive cards remain visible and claimable in the skill tree for them too — so admins can still test ability builds while the master toggle is off for everyone else. Backend hooks (claims, ability casts, quest progress) fire normally for bypass holders. The nodes are strictly separate frommmoskilltree.admin— granting admin alone does not grant any bypass. With the global permissions toggle off (OP-only mode), only OP players bypass.
Technical Details
- New item asset
Mmo_Reset_Skill_Scroll.json(singular, vs the existing pluralMmo_Reset_Skills_Scroll) atsrc/main/resources/Server/Item/Items/Consumables/. Reuses the recipe-paper icon/model/texture trio of the all-reset scroll, dropped fromQuality: EpictoQuality: Rareto differentiate. Charge time is1.0s(vs3.0son the all-reset) — destructive confirmation now lives in the UI rather than the charge bar. - New interaction handler
MmoSkillResetSelectInteraction extends SimpleInstantInteraction(codec typemmo_skill_reset_select), registered inMMOSkillTreePlugin.registerInteractions()immediately afterMmoSkillAllResetInteraction.firstRun()only opens the picker page — does NOT consume the held item. Captures the held item ID viactx.getHeldItem().getItemId()and passes it to the page constructor so the page can later remove exactly that item ID from the hotbar at confirm time. - New page
pages/SingleSkillResetPage.java(extendsInteractiveCustomUIPage) +Common/UI/Custom/Pages/SingleSkillResetPage.ui. Modeled onAwardBoostPage's skill-grid pattern — reuses the existingPages/SkillButton.uiandPages/SkillButtonRow.uitemplates. 8 buttons per row, button label suffixed with the current claimed-reward count(N). Selected button highlights red (#3d1a1a/#ff8f8f); zero-count buttons render grayed (#1a2028/#4a5562) and the click handler treats them as no-ops. Inline#ConfirmPanelbelow the grid stays hidden until a non-zero skill is selected; on selection it shows a localized prompt + redConfirm Reset+ neutralCancel. 500ms anti-spam mirrors the AwardBoostPage pattern. - The reset itself delegates to existing
SkillTreeService.resetRewardsByName(store, ref, playerRef, skills, skillId)— no new service-layer code. That method already removes effects (which re-locks abilities viaRewardEffectRegistry.removeEffect()onABILITY_UNLOCKrewards), clearsclaimedRewards+pendingRewards, refunds points, and firesnotifyRewardsResetByName(using existingnotify.rewards_resetkey — no new notification key needed). After reset, the page callsplayer.getInventory().getHotbar().removeItemStack(new ItemStack(scrollItemId, 1))(matches the consume pattern inMmoSkillAllResetInteraction:102andXpTokenConsumeInteraction:156) and closes itself viasetPage(ref, store, Page.None). - New event-data inner class
SingleSkillResetPage.SingleSkillResetEventDatawithAction/Valuekeyed-codec fields (mirrorsAwardBoostPage.AwardBoostEventDatashape, simpler — no text fields). Action set isselect(with skill ID inValue),confirm,cancel,close. - New i18n keys in
EnglishDefaults.java(other 8 languages fall back to English per existing convention):ui.singlereset.title,.subtitle,.confirm_prompt(formatted with skill display name + reward count),.confirm,.cancel. Item.name/.descriptionlocalizations added to all 9Server/Languages/*/items.langfiles using ASCII-safe transliterations consistent with the existingMmo_Reset_Skills_Scrolltranslations. - No schema bumps. No save migration. No config file changes.
Technical: Rested XP
- Six new fields on
SkillConfig:restedXpMode(string:off|flat|percent|hybrid, defaultflat),restedXpPerHour(long, default 1500),restedXpPercentPerHour(double, default 5.0),restedXpMaxPool(long, default 50000),restedXpMultiplier(double, default 2.0),restedXpMaxOfflineHours(int, default 168). Wired through field declaration,DataDTO,load()(with defensive validation — bad mode strings fall back to current value, negative numbers ignored),save(),resetToDefaults(), and getter/setter pairs (mirrorsboostStackModefor the string enum,maxBoostMultiplierfor the numeric fields). Setter forrestedXpModenormalizes to lowercase + validates against the allowed set via privateisValidRestedXpMode(...). NoCONFIG_VERSIONbump — the load path is defensive on missing fields so existing configs upgrade transparently. - New service class
service/RestedXpService.javawithcreditOnConnect(SkillComponent, PlayerRef) -> int(returns skills-touched count). Always updatesskills.lastSeenMstonow(even inoffmode) so toggling modes mid-session doesn't double-credit. Bails on first-ever join (lastSeenMs == 0). CapselapsedMstorestedXpMaxOfflineHours * HOUR_MSbefore any per-hour math. Per-skill loop iteratesSkillRegistry.getInstance().allSkillIds(). Inflat/hybridmodes adds(long)(hours * flatPerHour)regardless of skill state; inpercent/hybridmodes readsskills.getXpByName(skillId)+skills.getLevelByName(skillId)+SkillComponent.getXpForLevel(currentLevel + 1)to compute per-skillxpToNextand adds(long)(hours * (percentPerHour / 100.0) * xpToNext). Skills already at cap are skipped viaskills.getRestedXpByName(skillId) >= capshort-circuit. - New fields on
SkillComponent:public ConcurrentHashMap(per-skill remaining pool, uppercase keys to match other per-skill maps) andrestedXpMap public long lastSeenMs = 0L(epoch ms of last disconnect). Initialized in the constructor alongside the other maps. Three new helper accessors —getRestedXpByName,setRestedXpByName(removes the entry on<= 0to keep the map clean),addRestedXpByName(skillId, delta, cap). Codec wiring: per-built-in-skillRestedXp Codec.LONGloop (matching the existingXp/FracXp/Rewards/Pendingper-skill loops at lines ~439-468), plus a singleCustomRestedXpCodec.STRINGentry usingserializeCustomLongMap/deserializeCustomLongMapfor community-defined skills, plus aLastSeenMsCodec.LONGentry mirroringLastCastItemRestoreMs.clone()deep-copiesrestedXpMapand propagateslastSeenMs. No save migration — old saves load with emptyrestedXpMapandlastSeenMs = 0, which the connect handler treats as a first-join (no credit, baseline established). - Splice in
SkillService.addXpByName(afterexactXpis computed at line 272, before the fractional accumulator): whenrestedXpMode != "off"and pool > 0, computebonus = min(pool, exactXp * (multiplier - 1.0)), add toexactXp, deductMath.ceil(bonus)from the pool. TheMath.ceildeduction prevents tiny fractional residue from never draining the last few XP. Pool clamped to>= 0defensively. Rested bonus is additive on the already-multiplied value — does NOT compound multiplicatively with skill-tree bonuses or active boosts (a 2× boost + 2× rested produces ~3× effective, not 4×). - Connect/disconnect hooks in
MMOSkillTreePlugin: connect handler at line 766 callsRestedXpService.creditOnConnect(skills, playerRef)after the language-default block; ifskillsTouched > 0sends anotify.rested_xp_earnedchat notification (cyan, formatted withrestedXpMultiplier). Disconnect handler at line 951 stampsskills.lastSeenMs = System.currentTimeMillis()inside the existingworld.executeblock — both the primaryRefpath and theHolderfallback path. - New "Rested XP" section on
AdminConfigPage(alongside the existing Boost Stacking Limits section):#RestedXpModeDropdown(off/flat/percent/hybrid, mirrors#CombatXpModeDropdownend-to-end via newbuildRestedXpModeDropdown(...)helper) + 5 numeric text fields (#RestedXpPerHourField,#RestedXpPercentPerHourField,#RestedXpMaxPoolField,#RestedXpMultiplierField,#RestedXpMaxOfflineHoursField) + shared#ApplyRestedXpBtn. The Apply action validates ranges (no negatives; percent ≤ 100; multiplier in [1.0, 10.0]; offline hours ≤ 8760) before writing. All ~9 touchpoints per numeric field implemented (declaration, constructor init, populate set, handleData routing, apply switch arm, reload reseed, resetDefaults reseed, EventData public field, EventData codec entry). Section is non-OP-gated (lives outside theif (isOp) { ... }block) — gameplay-tuning, not security-critical. - New
/mmoconfig restedxpchat command inMMOConfigCommand.handleRestedXp(...)— peer route to the same setter as the UI dropdown. No-args echoes current mode + per-mode values; bad input prints usage. Numeric tunings are intentionally UI-only (matching theapplyBoostLimitsposture) — the full-UI design covers all five knobs without bloating the command surface. - Rested badge on
ViewXpPage'sSkillCard: newrestedXpfield on the innerSkillCardDataclass, populated inbuildSkillCardfromskills.getRestedXpByName(skillId), rendered inpopulateSkillCardasRested {0}(formatted viaSkillDisplayUtils.formatNumber). Empty-text trick used instead of.Visibleper CLAUDE.md UI gotcha. NewLabel #CardRestedBadgeadded toSkillCard.uiin the header column, sharing a row with#CardXpText(XP text getsFlexWeight: 1, badge getsWidth: 110, HorizontalAlignment: End, cyan#4affffto differentiate from the yellow boost badge). - 11 new i18n keys in
EnglishDefaults.java:notify.rested_xp_earned,cmd.config.restedxp.mode_set,cmd.config.restedxp.usage,ui.admin.restedxp_header,ui.admin.restedxp_mode_label,ui.admin.restedxp_desc,ui.admin.rested_xp_mode.off/.flat/.percent/.hybrid,ui.skillcard.rested_badge. Other 8 languages fall back to English per existing convention.
Technical: UI Fixes
pages/ViewXpPage.populate(...): wrapped the previously-unconditionalpopulateTrackedQuestsPanel(...)call in aSkillConfig.getInstance().isQuestsEnabled()gate. When quests are disabled, the three side-panel elements (#TrackedQuestsTitle,#TrackedQuestsEmpty,#TrackedQuestsList) are setVisible=falseinstead of populated — mirrors the existingisAchievementsEnabled()block immediately above it. Initial-render path, so thecommandBuilder.set("…Visible", …)Hytale UI gotcha doesn't apply (onlysendUpdate-time visibility toggles are unsafe). No new config field; theenableQuestsflag was already wired throughSkillConfig.pages/SkillTreePage.isVisibleReward(...): extended the existing orphan-ABILITY_UNLOCKfilter to also returnfalsewhenActiveAbilitiesConfig.getInstance().isEnabled()isfalse. Since both active hotbar abilities and triggered passives share theABILITY_UNLOCKreward type and the singleenabledmaster toggle onActiveAbilitiesConfig, one early-return covers both. The existing tier-skip logic at the top of the render loop (choices.stream().noneMatch(SkillTreePage::isVisibleReward)) automatically removes any tier that's now empty post-filter — no separate handling for choice tiers, mixed-type tiers, or single-card ability tiers.renderedIndexFor(...)already routes claim-time UI updates through the same predicate, so the off-state is consistent across initial render andsendUpdate. No state mutation: pending and claimed sets are untouched, so re-enabling the feature restores all previously-pending or previously-claimed ability rewards exactly as they were.
Technical: Bypass Permissions
- Three new permission constants on
PermissionUtil:BYPASS_QUESTS_PERMISSION(mmoskilltree.bypass.quests),BYPASS_ACHIEVEMENTS_PERMISSION,BYPASS_ABILITIES_PERMISSION, with shared prefixBYPASS_PERMISSION_PREFIX = "mmoskilltree.bypass.". Three matching public checks (hasQuestsBypass/hasAchievementsBypass/hasAbilitiesBypass) route through a privatehasFeatureBypass(player, node)helper that mirrorshasAdminPermission's shape: whenSkillConfig.isSkillPermissionsEnabled() == false(OP-only mode) only OP bypasses; when permissions are enabled, only the specific node bypasses. Theziggfreed.alternate prefix is honored automatically via the existingcheckPermissionhelper.mmoskilltree.admindoes not imply any bypass — strictly separate, per design. - New
PermissionUtil.resolvePlayer(PlayerRef)helper resolves aPlayerRefto itsPlayercomponent viaplayerRef.getReference().getStore().getComponent(ref, Player.getComponentType()). Used by thePlayerRefoverloads of the new toggle accessors so service-layer event hooks (which operate onPlayerRef) get the same bypass treatment as UI pages. - New per-player toggle accessors:
SkillConfig#isQuestsEnabledFor(Player|PlayerRef),SkillConfig#isAchievementsEnabledFor(Player|PlayerRef),ActiveAbilitiesConfig#isEnabledFor(Player|PlayerRef). Each short-circuits on the global toggle (still always-on path) and falls through to the matchinghasXxxBypass(...)check. The zero-argisQuestsEnabled()/isAchievementsEnabled()/isEnabled()accessors are unchanged — they still answer "is the feature globally on?" and remain wired to theAdminConfigPagetoggle indicators (lines 317/318/339, 1136/1144/1168) so admins see the real config state, not their own bypass-flavored view. - Service-layer call-site swaps (3 + 5 + 2 = 10 sites):
QuestServiceonLevelReached/refreshReachLevelObjectives/processEvent;AchievementServiceonLevelReached/dispatch/revalidateReachLevelProgress/applyUnlocks/claimAchievementRewards;ActiveAbilityService.tryActivate;MMOSkillTreePluginHUD-init onPlayerReadyEvent. - UI-layer call-site swaps across
QuestPage,AchievementPage,AchievementSummaryPage,AchievementDetailPage,BoostPage,ViewXpPage,AbilityBindPage— every tab/nav-button/tracked-panel gate that readisQuestsEnabled()/isAchievementsEnabled()/ActiveAbilitiesConfig.isEnabled()now passes the localPlayer player(already resolved viastore.getComponent(ref, Player.getComponentType())near the top of each page'sbuild) into the matching*EnabledFor(player)accessor. pages/SkillTreePage.isVisibleReward(SkillReward, Player)andrenderedIndexFor(..., Player)gained aPlayerparameter so the existing v1.0.4 ABILITY_UNLOCK hide-when-disabled filter routes throughActiveAbilitiesConfig.isEnabledFor(player). Bypass holders see ability/passive cards (and any tier whose only contents were ability unlocks) on the skill tree just as if the global toggle were on; the orphan-ID filter is unchanged. All five call sites updated — three inbuild(tier-skip predicate, visible-count loop, card-render loop) and two inhandleDataEvent's post-claim sendUpdate path (renderedIndexForfor the clicked card's UI selector + the dim-unclaimed-cards loop).- No schema bumps. No save migration. No new localization keys (permission nodes have no UI strings of their own; the existing "system disabled" copy on
AbilityBindPageand the hidden tabs/panels on the other pages cover the visible behavior).PermissionUtilJavadoc grew a bulleted list of all known nodes for future discoverability since there's still no central permission manifest.
v1.0.3 - May 2026
Two follow-on hardening tweaks for the placed-block XP guard shipped in 1.0.2, plus a modder-friendly fallback for routing custom weapons to a combat skill.
Updates
- Modded weapons can declare their combat skill via an item tag. Previously, a new weapon shipped by a content mod would only earn XP for the right combat skill if its item ID happened to substring-match a key in
xp-maps.json— so admins had to add an entry for every new sword, axe, or staff. Weapons can now carry anMMO_Weapon_Typeraw tag whose values name a registered skill (e.g.["POLEARMS"]or["PoleArms", "Blunt"]). When the regular xp-map lookup misses, the first tag value that matches a registered skill wins — XP routes to that skill, weapon-gated damage rewards/lifesteal/crit fire under that skill, and the View XP buffs panel shows the correct skill name. Tag matching is case-insensitive. Existing weapons with xp-map entries are unaffected. The tag is read from the item'sgetRawTags()map at the moment you swing — no config reload needed when a mod adds a tagged weapon. - Creative-mode placements no longer poison the XP guard. When a player in creative mode placed a block, the guard remembered that placement and denied gathering XP to anyone who later broke it — including the same player after switching back to survival, and other players who came along to mine. Builder admins setting up an ore wall for survival miners were silently breaking those miners' XP. Placements made in creative mode are now skipped at the placement step, so survival players who break those blocks earn XP normally.
- Placed-block XP gating now defaults to server-wide strict mode. Previously the guard only denied XP when the breaker was the same player who placed the block — so a friend could place blocks for you to mine and you'd still get full XP. Out of the box, breaking any player-placed block now denies XP, regardless of who placed it. Admins who want the prior per-player behavior can flip
strictPlacedBlockChecktofalseinmods/mmoskilltree/skill-config.json. The block must still actually be flagged as placed — natural ores and trees are unaffected.
Technical Details
util/CombatWeaponUtil.java: newresolveCombatSkillFromItem(@Nullable ItemStack)overload + privateresolveFromRawTags(ItemStack)helper. The overload calls the existingresolveCombatSkillFromItemId(String)first, and only falls through to the rawTag path when the primary lookup returnsUNARMED. Tag values are uppercased and tested againstSkillRegistry.isKnownSkill(...)— first array entry that matches a registered skill (built-in or custom) wins; nohasTriggerfilter, so modders can deliberately route weapons to gathering or other non-combat skills. ExistingresolveCombatSkill(store, ref)was rerouted to call the new ItemStack-aware overload (it already had theItemStackin scope), so all seven downstream sites pick up the fallback automatically:CombatXpEventSystem,MobKillEventSystem,CombatDamageEventSystem(damage modifier filter +onDodgeArmedweapon gate),MMOSkillTreeAPI,CastConditionEvaluator, andViewXpPage. The legacyresolveCombatSkillFromItemId(String)stays unchanged for callers that genuinely only have a string. Routing-only —getWeaponXpValueis intentionally not patched, so kill XP for tag-routed weapons applies the mob base XP with no weapon-quality bonus (admin opt-in via xp-maps.json). Per-hit XP throughSkillService.processTriggerlikewise stays 0 for tag-routed weapons unless an item-ID entry is added. New imports:com.hypixel.hytale.assetstore.AssetExtraInfo,com.hypixel.hytale.server.core.asset.type.item.config.Item,com.hypixel.hytale.server.core.inventory.ItemStack.getData()null-safety mirrorsInternalContainerUtilTag.java:32.event/CombatXpEventSystem.resolveHeldWeaponSkill,event/MobKillEventSystem.handleKillXp,pages/ViewXpPage.populateBuffsBar: each swapped fromresolveCombatSkillFromItemId(itemId)toresolveCombatSkillFromItem(stack)(passing the liveItemStackthey already had in scope) so the rawTag fallback fires.event/PlaceBlockEventSystem.java: added aPlayer.getGameMode() == GameMode.Creativeearly-return guarding the twoPlacedBlockTrackercalls (trackPlacement+trackPlacedItem). Mirrors the pattern already used bySkillService.processTriggerfordisableXpInCreative. Downstream calls (SkillService.processTrigger,QuestService.onBlockPlace,AchievementService.onBlockPlace,StatisticsService.onBlockPlace) intentionally stay outside the gate — quests/achievements that count placements should still tick for builder progress, andSkillServicealready declines XP for creative-mode players internally.config/SkillConfig.java: newstrictPlacedBlockCheckboolean field (defaulttrue). Wired through field declaration,DataDTO,load(),save(),reloadDefaults(), andisStrictPlacedBlockCheck()/setStrictPlacedBlockCheck()getter/setter — all mirroring the existingtrackPlacedBlocksshape.CONFIG_VERSIONbumped from 11 to 12 so existing servers'skill-config.jsonis backed up and regenerated to surface the new key.service/PlacedBlockTracker.java: two new methods —wasPlacedByAnyPlayer(worldUuid, x, y, z)andremoveAnyPlayerTracking(...). Per-player methods (wasPlacedByPlayer/removeTracking) are left in the public API for the toggle-off path. SameisTrackPlacedBlocksshort-circuit applies.service/PlacedBlockTracker.java: secondaryMapindex added alongsideplayerPlacedBlocks(IndexEntry= private record carrying placer UUID + timestamp). Both lookup paths and both removal paths are now O(1) hash probes instead of scanning a player's set (per-player) or every player's set (strict).trackPlacementputs to both maps, evicting any stale entry from a previous placer's set if the position was re-placed.wasPlacedByPlayer/wasPlacedByAnyPlayerconsult the index only — expiration is checked againstIndexEntry.placedTime()and lazily evicted viaevictExpired(pos, entry)(CAS-style remove on the index, then drop from the placer's set).clearPlayer,cleanupExpired,load, andmigrateLegacyForPlayerall keep the index in sync; legacy hash-keyed entries are intentionally left out of the index until they migrate (their true placer UUID isn't known untilmigrateLegacyForPlayerruns). On-disk format is unchanged — the index is rebuilt fromplayerPlacedBlockson everyload().event/BreakBlockEventSystem.java: branches onSkillConfig.getInstance().isStrictPlacedBlockCheck()to call either the new server-wide pair or the existing per-player pair. No other callers needed — the toggle is intentionally scoped to block-break only;PickupItemEventSystem.consumePlacedItem(the placed-item harvest guard) stays per-player as today.- No save migration. Pre-1.0.3 placed-blocks.json entries continue to work unchanged — they were already keyed by player UUID after the 1.0.2 migration. No localization changes.
v1.0.2 - May 2026
A Settings menu hotfix and a new optional fishing-mod integration.
Updates
- Fixed: opening the Settings menu crashed players. The 1.0.1 cleanup of the Inventory Casting toggle removed the on-screen control but left a leftover hook that pointed at the deleted button. Opening Settings tried to wire that hook and crashed. The leftover hook is now gone — Settings opens cleanly again. No saves or settings affected; just open Settings normally after updating.
- Angler's Almanac integration. When Angler's Almanac is installed, completing the fishing minigame now awards FISHING XP based on how well you played: a perfect catch grants +300 XP, great +150, good +50, plus the per-fish XP from your
xp-maps.jsonFISHING map. First-time catches add a +25 New Discovery bonus and legendary catches add +500. All five payouts are server-tunable — the FISHING block ofmods/mmoskilltree/xp-maps.jsonships with_AA_Perfect/_AA_Great/_AA_Good/_AA_NewDiscovery/_AA_Legendarydefaults that you can edit (or set to-1to disable a payout). Quest objectives that count fish caught also tick. The integration auto-detects Angler's Almanac on startup and stays dormant if the mod is absent — no config required. If both Angler's Almanac and HyFishing are installed, HyFishing wins by default; flip the priority inmods/mmoskilltree/integrations.jsonif you want Angler's Almanac to drive XP instead.
Technical Details
pages/SettingsPage.java: removed the orphaneventBuilder.addEventBinding(... "#ToggleInventoryCasting" ...)call that survived the 1.0.1 inventory-casting feature removal. The bound element was deleted fromMMOSkillTreeSettingsPage.uiand the matchingtoggleInventoryCastingarm was removed fromhandleDataEventin 1.0.1, but the event binding itself was left behind. The Hytale UI engine fails when resolving a binding for a non-existent element ID, which crashed the page render. No schema bumps; no save migration; no localization changes.- New
integration/fishing/adapters/AnglersAlmanacAdapter.javalistens todev.rm20.anglersalmanac.IEvents.LootCaughtEventvia Hytale's globalEventRegistry(registerGlobal, resolved reflectively from the AA classloader so this still compiles without AA on the path). XP formula:performance(perfect/great/good) + xpMapLookup(getLootID()) + (isNewDiscovery ? bonus : 0) + (isLegendary ? bonus : 0), where every payout reads throughXpMapsConfig.getXpMapByName("FISHING")— the rating tiers + bonuses live under reserved keys_AA_Perfect/_AA_Great/_AA_Good/_AA_NewDiscovery/_AA_Legendary(defaults 300 / 150 / 50 / 25 / 500, seeded byFishingDefaults; set to -1 to disable). Underscore-prefixed keys deliberately fall outside the substring scan inlookupFishingXpand are read via directxpMap.get(key)in a newlookupBonusValue(key)helper. Awards viaSkillService.addXpByName(..., "FISHING", xp)on the world thread (event captured off-thread, hopped viaworld.execute) and callsQuestService.onCatchFish(...)for objective parity withHyFishingAdapter. Registered inIntegrationRegistry.resolve()with@IntegrationMeta(domain=FISHING, priority=1)afterHyFishingAdapterso HyFishing wins on a priority tie when both mods are installed; admin can flip viaintegrations.jsonenabled/priorityoverrides. Hytale'sEventRegistryhas no unregister API andIntegrationRegistry.resolveDeferredIfNeeded()runs a secondresolve()on first player connect — soshutdown()only flips avolatile boolean activeflag and intentionally leaves the cachedMethodhandles in place (the listener lambda capturesthisfrom the first instance, and nulling those fields would null-deref at fire time). Same posture asSimpleEnchantmentsAdapter. No schema bumps, no new localization keys.
v1.0.1
Casting rework, plus a sweep of admin and skill-tree fixes. The legacy keys-7/8/9 mode is gone — active abilities now fire from two parallel input paths (keyboard ability keys *and* matching cast items) so every player can pick whichever feels best without flipping a mode switch.
Updates
- Three MMO ability slots, six configurable inputs. Bind any unlocked ability to Slot 1, Slot 2, or Slot 3. Each slot independently picks one of six Hytale inputs: Ability 1 / 2 / 3 plus their + Crouch variants. Crouch is detected at the moment the key fires (player is holding Hytale's sneak), so it's a free modifier — no extra keybind needed. Defaults match the prior prerelease feel:
Slot 1 = Ability 2,Slot 2 = Ability 3,Slot 3 = Ability 2 + Crouch. - Per-slot input picker on the Abilities page. Each slot row has a new INPUT dropdown beneath the binding. Pick from the six Hytale inputs; if two slots would collide, the picker auto-swaps them so the three inputs always remain distinct.
- Cast items auto-given on first connect. Players receive *Cast Item 1 / 2 / 3* the first time they join after this update, so cast-item input works without anyone having to ask. Cast items always fire their own slot regardless of input mapping (mousewheel-friendly). Discarded one? The Abilities page has a "Restore Missing Items" button (15-minute cooldown).
- Hotbar 7/8/9 casting and the casting-mode toggle are gone. The old "hotbar swap" path that hijacked keys 7/8/9 caused chain-tracker corruption and a slot-revert dance. The three-way Asset / Hotbar / Items toggle and its Settings page entry are removed — there's no longer a mode to pick.
- Automatic migration for existing players. Bindings carried over from earlier 1.0 prereleases keep working: old hotbar slot 7 binding → Slot 1, slot 8 → Slot 2, slot 9 → Slot 3. The default input map keeps the keyboard feel identical to the prior 1.0.1 prerelease unless you remap.
- Command changes.
/mmoability bind <1|2|3>and/mmoability unbind <1|2|3>now take the slot number directly. New/mmoability input <1|2|3>swaps a slot's input from chat (auto-swaps any conflicting slot)./mmoability listnow showsSlot N (input): ability./mmoability moderemoved. /mmoawardxpacceptsalland category names./mmoawardxpawards XP to every skill;all /mmoawardxpworks forGATHERING,COMBAT,CRAFTING, orMISC. Amount is awarded once per resolved skill, matching/bulksetxpsemantics. Single-skill usage is unchanged.- Fixed: server log spam from XP/boost token items. Silenced the flood of
Unused key(s) ... FailsOnDamagewarnings emitted on every server start. The charging-interaction key was renamed by Hytale and 280 token/scroll item definitions were still using the old spelling. - Fixed: skill-tree picks resetting on every login. Claims for skill-tree nodes at off-pattern levels — the Daggers Lv 18 Shadowstep / Vanish choice, the Lv 33 Mark of Death / Smoke Bomb choice, and the Lv 100 capstones across all eleven combat trees — were being silently revoked at the start of each session. The login integrity check was identifying the wrong node for those claims, declaring the reward "orphaned", locking the ability, and clearing the claim. Players who picked one of these abilities had to re-claim after every reconnect. The matching skill-tree buff totals and "next reward at Lv X" indicator on the View XP page also missed the same off-pattern claims — both now read correctly. Existing testers will need to re-claim once after this update; nothing else is required.
Notes
- When a held item defines its own Ability 1 / 2 / 3 (the vanilla crossbow's reload binds to Ability 3, for example), pressing that key fires both the weapon's built-in ability and your bound MMO ability — they run in parallel rather than the held-item ability shadowing the MMO one. Remap that slot's input (or use its crouch variant) if you want the two kept separate.
- The cooldown HUD renders three pips (one per slot). Each pip's input badge mirrors the slot's currently-configured Hytale input (
A2/A3/A2c/ etc.), so the HUD reflects remaps live without a reconnect. The 4th pip slot stays reserved in the HUD layout so position overrides remain stable for a future expansion.
Technical Details
- New
service/ability/MmoSlot.javaenum (SLOT_1/2/3) — the canonical binding surface. Carriesserialize/parse/parseUserLabel/displayLabel/indexplusdefaultInputs()returning the canonical A2 / A3 / A2C mapping. Replaces all priorTriggerKey-keyed binding storage. service/ability/TriggerKey.java: extended to all six Hytale inputs (ABILITY_1/2/3plus their_CROUCHvariants). NewallInputs()returns the cycle order used by the Abilities page input button.parseUserLabelacceptsa1/ability1anda1c/shift_a1/crouch_a1(and the same family for A2/A3).shortLabel/displayLabelswitches widened. OldactiveTriggers()removed — the active surface is now per-player (SkillComponent.getMmoSlotInputs), not a static list.data/SkillComponent.java:abilityTriggers: Mapreplaced withmmoSlotBindings: Map; newmmoSlotInputs: Mapfield. New API:getAbilityForSlot,bindAbilityToSlot,unbindSlot,getInputForSlot,getSlotForInput,setInputForSlot(auto-swaps so the three inputs are always distinct). Codec addsMmoSlotBindings+MmoSlotInputsentries;HotbarAbilityBindingsbecomes legacy-read-only with a one-shot migration:H7→SLOT_1,H8→SLOT_2,H9→SLOT_3,A2→SLOT_1,A3→SLOT_2,A2C→SLOT_3.getMmoSlotInputs()lazy-populates with defaults if empty so saves predating the field still see the canonical mapping.castingModecodec entry retained as deprecated round-trip;getCastingMode()always returnsASSET_TRIGGERS. New persisted booleanabilityCastItemsBootstrapped(additive codec entry) gates first-connect auto-give.io/HotbarAbilityPacketFilter.java:toTriggerKeyswitch coversAbility1/Ability2/Ability3.maybeCrouchVariantresolves the crouch suffix for all three.handleAbilityTriggerresolvesTriggerKey→MmoSlotviaskills.getSlotForInput(...)then dispatches viagetAbilityForSlot. TheSwapFromarm andmode != ASSET_TRIGGERSgate are gone.AbilityInteractionInjectornow writesMMO_Trigger_Ability1alongside Ability2 / Ability3 so the keyboard A1 input surfaces through held items too.- New
Server/Item/RootInteractions/MMO_Trigger_Ability1.json— sibling to the existing 2/3 trigger root interactions; routes toMMO_Trigger_InnerwithRequireNewClick: true.Server/UnarmedInteractions/Empty.jsonextended to map Ability1 → MMO_Trigger_Ability1 alongside the prior two. interaction/MmoAbilityCastInteraction.java: replaced thegetTrigger() : TriggerKeyabstraction withgetMmoSlot() : MmoSlot. Dispatch usesskills.getAbilityForSlot(slot). Cast items always fire their own slot — they ignore the input map (mousewheel-friendly hotbar navigation works regardless of what the slot's keyboard input is set to).interaction/MmoAbilityCastSlot{1,2,3}Interaction.java:getMmoSlot()returnsSLOT_1/SLOT_2/SLOT_3respectively.service/ability/CastingMode.java: stays@Deprecated; enum values + parsers retained only so old codec values round-trip. No runtime branch reads the enum anymore.- Deleted:
event/HotbarAbilitySlotEventSystem.java(was DEBUG-only, never registered) andservice/ability/AbilitySlotRevertService.java(only callers were the SwapFrom arm + a disconnect-cleanup hook).event/AbilityHudTickingSystem.javano longer drains the revert queue.MMOSkillTreePlugindrops theAbilitySlotRevertServiceimport + disconnect cleanup call + obsolete "intentionally NOT registered" comment. service/ability/AbilityCastItemService.java: newbootstrapIfNeeded(player, skills)method gated bySkillComponent.abilityCastItemsBootstrapped. Hooked intoPlayerReadyEventfromMMOSkillTreePluginso net-new players (and existing players who haven't had it run yet) get their three cast items on first connect.pages/AbilityBindPage.java:pendingTrigger: TriggerKey→pendingSlot: MmoSlotthroughout. NewcycleInputaction handler callsskills.setInputForSlot(slot, nextInputAfter(current)). NewslotLabel/inputLabel/nextInputAfterhelpers;findBoundTrigger→findBoundSlot.populateCastingModeSection()collapsed to header + description + Restore Missing Items button (always enabled, 15-min CD on user-initiated calls).setModeandtoggleInventoryCastingaction arms removed;restoreCastItemslost its mode gate. Card status's "Bound to Slot N" usesslotLabel()directly. Dropped the unused legacy int-slot constructors and theparseInthelper.Common/UI/Custom/Pages/AbilitySlotRow.ui: row height bumped 110 → 138. Added#InputBadgelabel ("INPUT") +#InputDropdown($C.@DropdownBox) between the meta line and the CHANGE / UNBIND row. Dropdown entries are populated fromTriggerKey.allInputs()per render;ValueChangedfiressetInputwith the slot id + selectedTriggerKey.serialize()value.Common/UI/Custom/Pages/AbilityBindPage.ui: removed the#ModeAssetButton/#ModeHotbarButton/#ModeInventoryButtongroup, the#CastingModeLabel+#ToggleInventoryCastingButtongroup, and updated#CastingModeHeadertext to "CAST INPUTS".Common/UI/Custom/Pages/MMOSkillTreeSettingsPage.ui+pages/SettingsPage.java: removed the Inventory Casting@SettingCardand itstoggleInventoryCastinghandler.ui/hud/AbilityCooldownHud.java: iteratesMmoSlot.all(). Each pip readsskills.getInputForSlot(slot)and renders that input'sshortLabel()(e.g.A2c,A3,A1) so the HUD label tracks the player's input remap live. Always renders three pips. The 4th pip slot stays reserved.command/MmoAbilityCommand.java:bind/unbindtake a slot number (1|2|3); newinput <1|2|3>subcommand for chat-driven input remap;listrow format includes the configured input. Removed themodesubcommand and theCastingModeimport.command/AwardXpCommand.java: skill arg resolution now branches onALL/SkillCategory/ single-skill before falling through to the unknown-skill error.ALLusesSkillRegistry.allSkillIds(); category match usesSkillRegistry.allSkillIdsByCategory(name)(case-insensitive againstSkillCategory.values()). The resolved id list is iterated inside the existingworld.execute(...)block, callingSkillService.addXpByNameonce per id with the same amount. Error copy widened to mention the new tokens.i18n/lang/EnglishDefaults.java: new keysability.bind_page.input_badge,ability.bind_page.input.{a1,a1c,a2,a2c,a3,a3c},ability.input_changed,ability.input_invalid,cmd.ability.input.usage,cmd.ability.list.row_unbound.cmd.ability.list.row/row_lockedwidened to{slot} ({input}): {ability}.ability.bind_page.slots_headerretitled to "ABILITY SLOTS"; help text rewritten to describe the slot + input picker model.service/SkillTreeService.cleanupOrphanedRewardsByName: was iteratingclaimedRewards.entrySet()(keyed bynode.getTier(), the data-tier integer) and resolving each entry vianodes.get(tier)(list index). Standalone choice nodes added vianodes.add(new SkillTreeNode(dataTier, levelRequired, ...))after the canonicaladdTierblock sit at non-matching list positions — e.g. DAGGERS L18 shadowstep/vanish at data-tier 11 lands at list index 10, while L33 mark_of_death/smoke_bomb at data-tier 10 lands at list index 11. The cross-mappednodes.get(11)returned the L33 node,getChoiceById("shadowstep")returned null, the cleanup branded the claim "orphaned" → calledlockAbilityandremoveClaimedRewardByName. Thetier >= nodes.size()early-return also nuked L100 capstone claims (data-tier 20). Fix: build aMapkeyed bynode.getTier()and look up by data-tier; the "no matching tier in tree" branch (orphaned by node deletion) now usesnodesByTier.get(tier) == nullrather than the bogus list-size comparison. Same latent bug as the six sites already fixed earlier in the prerelease (sumFilteredRewards et al.) — this one survived in cleanupOrphanedRewardsByName, which runs fromvalidateAndRevokeRewardsByNameon everyPlayerReadyEventviavalidateAllRewards, hence the once-per-login reset.pages/ViewXpPage.getSkillBuffEntriesandgetRewardStatusByName: same shape — both walkedfor (int tier = 0; tier < nodes.size(); tier++)then calledskills.getClaimedRewardIdsByName(skillId, tier)/getClaimedCountByName(skillId, tier)withtier= list index. For trees with non-canonical node placement (any combat tree post-rework), buff totals double-counted or zeroed the wrong rows, and the "next reward at Lv X" indicator skipped non-canonical tiers. Both now iteratefor (SkillTreeNode node : nodes)and usenode.getTier()as the lookup key.findAbilityPlacementstill uses list-index iteration and stuffs that index intoAbilityPlacement.tier— left as-is because no caller reads thetierfield (onlyskillId()andlevelRequired()are consumed). The field is effectively cosmetic.
v1.0.0 - Active Abilities
1.0.0 ships active abilities for the first time. The plugin previously only granted passive stat rewards through skill trees. This 1.0 release adds 31 hotbar abilities plus a triggered-passive lattice across the combat trees, a flat-first damage rebalance, real-momentum dashes with i-frames, and a sweep of bug fixes.
Active Abilities
- Unlock through the skill tree — abilities live as
ABILITY_UNLOCKrewards on tier nodes, just like any other reward. The skill and level that unlocks an ability are defined by the tree, so admins can re-home an ability with a single config edit - Bind via
/mmoabilities— open the bind page, drag any unlocked ability onto hotbar slot 7, 8, or 9 - Press hotkey to cast — a cooldown pip appears on the HUD; your held item stays where it was (the slot snaps back automatically after a brief revert)
- Fully tunable — every numeric and asset-shaped value lives in
mods/mmoskilltree/abilities.json. Server owners can retune damage, cooldowns, radii, projectile models, sounds, animations, on-hit effects, and XP routing without a plugin rebuild
The 31-ability roster
Acrobatics
- Dodge (Lv 4) — roll in your movement direction (8-way WASD: forward / back / left / right / four diagonals — stationary defaults to backward). 100 ms i-frames during the slide, 3.5 s cooldown, 5 stamina. Free, low-stakes "get out" button every player picks up early
Magic — 9
- Arcane Missiles (Lv 8) — quick-tap single-target projectile salvo for early-game spellcasters
- Poison Dart (Lv 17) — fling a poisoned dart that hits for 2 and ticks 8 damage/sec for 10 s. Awards Magic XP regardless of held weapon
- Fireball vs Ice Bolt (Lv 22 — Pick 1 of 2) — burst (45 damage, 4-block AoE) vs control (28 damage, 2.5-block splash, 3 s Slow)
- Frost Nova vs Renewal (Lv 32 — Pick 1 of 2) — self-centered ice burst (28 damage, 4.5-block radius, 4 s Slow on every enemy hit) vs instant heal (20 HP) plus 5 s regen
- Flame Stream (Lv 43) — sustained 5-second flame torrent: 50 ticks of 4 damage at 1 mana / tick, pierces 3, scorches with
Burn. Channel breaks if you walk too far from the cast origin - Meteor (Lv 67) — mark a spot, wait 2.5 s, then a meteor lands dealing 70 damage in a 5-block radius (35 s cooldown). Headline late-game burst
Archery — 5
- Hunter's Mark (Lv 13) — tag a target; your next 4 hits deal +20% for 10 s
- Piercing Shot vs Scatter Shot (Lv 28 — Pick 1 of 2) — long-range single line (35 damage, 20 m, pierces 3) vs short-range buckshot (4 raycast pellets at 18 damage, 10 m, wide spray)
- Volley (Lv 42) — arrow rain at a target point: 4-block radius, 6 ticks of 10 damage over 3 s
- Paralyzing Dart (Lv 55) — arms your next ranged hit; on landing, the target is stunned for 1.5 s. 8 s armed window, 30 s cooldown. Costs mana (10) instead of stamina — the only archery ability that taps the mana stat, so bow players have a reason to invest in mana tier rewards
Swords — 3
- Power Strike (Lv 22) — empowered swing: 30 damage in a 2.5-block burst at swing time AND arms your next melee hit with a 2× multiplier for 5 s
- Leap Strike (Lv 32) — dash 8 blocks forward, damaging every enemy you pass through for 35 each
- Rallying Cry (Lv 47) — party buff: you AND every ally within 6 blocks arm a 1.5× next-hit for 8 s
Axes — 3
- Whirlwind (Lv 22) — 5-block radius spin, 22 damage to every enemy in reach
- Berserker Charge (Lv 32) — dash 6 blocks (gated to ≤ 50% HP), then arm your next melee hit with a 1.5× multiplier for 5 s
- Sunder (Lv 47) — brand a target for 6 s; your next 5 hits deal 1.25× (armor-shred drag-out tax)
Daggers — 4
- Shadowstep vs Vanish (Lv 18 — Pick 1 of 2) — blink 8 blocks forward (drops mob aggro in a 30-block radius) vs short-duration aggro suppression that mobs treat as IGNORE for the window. Both require Perfect Utils for the actual aggro mutation; without it the blink/cosmetic still works but mobs keep their lock
- Vampiric Strike (Lv 42) — dagger lifesteal hit (12 s cooldown), applies a
Bleedfor 8 over 4 ticks - Shadow Pounce (Lv 47) — dash 10 blocks forward, then arm your next hit with a 2.25× multiplier for 3 s
Defense — 3
- Shield Bash (Lv 22) — stuns the foe in front of you (within ~3 blocks) on cast for 1.5 s, AND arms your next melee hit with a flat +20 damage bonus for 4 s. 8 s cooldown, 5 stamina
- Shield Slam (Lv 38) — dash 6 blocks with a shield-first sweep, damaging enemies on the path and stunning each for 1.5 s
- Guardian's Call (Lv 47) — tank-flavored 1.4× next-hit aura on self + allies within 6 blocks for 10 s
Mining / Woodcutting / Harvesting / Fishing — 4
- Seismic Slam (Mining, Lv 32) — aim at the ground, wait 0.6 s, then a tremor erupts dealing 45 damage in a 4-block radius
- Lumberjack Leap (Woodcutting, Lv 32) — dash 7 blocks with an axe-leap sweep, 28 damage per enemy on the path
- Thorn Patch (Harvesting, Lv 32) — plant a toxic herb patch at your aimed point: 3-block radius, 6 ticks of 10 poison damage over 6 s
- Tide Caller (Fishing, Lv 32) — cast a turquoise tide pool that drowns and chills anything caught in it for 4 s (8 damage / sec); 16 s cooldown, requires a fishing rod
Choice tiers — Pick 1 of 2
Four "Choice — Pick 1" nodes sit in the skill tree, forcing a build-shaping decision between two opposite abilities at a single level threshold. Pick one and the other locks out for that skill until you reset:
- Daggers Lv 18 — Shadowstep vs Vanish (mobility flavor)
- Magic Lv 22 — Fireball vs Ice Bolt (burst vs control)
- Archery Lv 28 — Piercing Shot vs Scatter Shot (long-range line vs short-range cone)
- Magic Lv 32 — Frost Nova vs Renewal (control AoE vs sustain)
The skill-tree page sorts tiers by level requirement so each choice node renders right between adjacent stat tiers, with a "Choice — Pick 1" header above the two ability cards.
Triggered passives — combat trees + L100 capstones
On top of the 31 hotbar abilities, every combat tree (Swords, Daggers, Polearms, Staves, Axes, Blunt, Unarmed, Archery, Magic, Artillery, Defense) grants:
- One triggered passive at low / mid level on each tree — fires automatically on
on_hit_dealt,on_kill, oron_finisherevents. Examples: *Coup de Grâce* (Daggers Lv 11) lands a 2× execute swing when you hit a target ≤ 25% HP; *Cleaver* (Axes Lv 25) detonates a 4-block ground burst on every combo finisher; *Pyromaniac* (Magic Lv 57) buffs your next Fireball hit - Two Lv 100 capstones (Pick 1 of 2) per combat tree — augment specific abilities, apply real status effects to the victim, or drop damage zones at the kill site instead of generic % multipliers. Examples: *Cataclysm* (Staves) makes meteor leave a 4-second burning crater; *Multishot* (Archery) gives every Scatter Shot pellet its own bleed; *Stagger* (Blunt) stuns for 0.75 s on every hit
Plus Wind Dancer (Acrobatics Lv 67) — a passive that auto-arms a 2.5× next-hit damage buff for 4 s with a chained 1.4× follow-up when you successfully dodge an incoming hit during the perfect-dodge window. No hotbar slot, no cast — pure dodge-and-counter feel.
Damage rebalance — flat-first, % gated to endgame
A ground-up rework of how damage stacks. Percent-damage rewards multiplied with each other and with weapon-tree bonuses, and paired with other mods the chain hit one-shot territory long before defenses could kick in. Now percent damage is a rare endgame multiplier; flat damage is the bread-and-butter at every other tier.
- Low and mid tiers (Lv 5–65) no longer give % damage. Every "Damage +X%" reward in the early/mid tree has been replaced with a flat "+X Damage" reward. Flat damage adds after all multipliers and the ability-stack cap, so it doesn't compound — your Tier 0 reward is exactly as strong at Lv 100 as it was at Lv 5
- % damage is endgame-only. Tiers 8 and 9 (Lv 80, Lv 100) keep their
STAT_DAMAGE,LIFESTEAL, andCOMBO_DAMAGEpercent rewards, with values roughly halved - Combo finishers and lifesteal got the same treatment — flat at low tiers, percent only at L80+
- Two new unique-passive choice tiers per combat tree at off-grid levels (Lv 11–29 lower, Lv 52–78 upper). Each is a "Pick 1 of 2" with hand-crafted passives: execute-style flat damage that scales when the target is below a HP threshold, full-HP openers that reward fresh engagements, ability-filtered specialists ("+15 Power Strike Damage", "+20 Fireball Damage"), and low-HP rage rewards
A maxed-out Lv 100 build sees roughly 12% lower peak burst vs. the old percent-stacking max. Mid-game (~Lv 40) drops more sharply because flat doesn't compound — by design.
Dashes are real dashes (momentum + i-frames)
The five DASH abilities — Leap Strike, Berserker Charge, Shield Slam, Shadow Pounce, Lumberjack Leap — write a one-shot velocity impulse that physics carries through the world, slowing on collisions and decaying with air resistance. Dashes also grant i-frames — a brief invulnerability window during the slide. Hits that connect mid-dodge are cancelled outright, and any incoming knockback is stripped. Defaults: 200 ms for most dashes, 300 ms on Shield Slam (longest, tank-flavored), 150 ms on Shadow Pounce (briefer, assassin timing). Server owners can tune every value (velocityHorizontal, iframesDurationMs) per ability from mods/mmoskilltree/abilities.json.
Dodge Counterattack — Acrobatics + Defense passive
Time a dodge so a hit lands inside the i-frame window and your next swing hits harder. Souls-style parry-then-execute, built into the skill tree as a passive you unlock and scale per-skill.
- Generalist "+25% Dodge Counterattack" options at Acrobatics Tier 4 (Lv 30), Acrobatics Tier 6 (Lv 50), and Defense Tier 4 (Lv 30). A fully-invested generalist hits 1.75× damage on every successful dodge counter
- Specialist tiers that only apply to a specific ability — *Shadow Counterstrike* (Acrobatics Tier 8, Lv 80, +50% extra when you dodge with Shadow Pounce) and *Bulwark Counterstrike* (Defense Tier 7, Lv 65, +50% extra on Shield Slam dodges). A Shadow Pounce specialist with full investment caps at 2.25×; a Shield Slam tank tops out at 2.0×
The dodge timing window (50 ms by default) and how long the counterattack stays armed (750 ms) are server-tunable in mods/mmoskilltree/abilities.json.
Active vs Passive labeling
Every ABILITY_UNLOCK reward in the skill tree wears a clear [ACTIVE] or [PASSIVE] tag so you can tell at a glance which abilities go on your hotbar vs. which trigger on their own. Active abilities bind to slots 7/8/9 and cast on hotkey. Passive abilities don't appear on the bind page, can't be assigned to a slot, and trigger automatically when their condition is met.
If a future patch removes an ability from the codebase but leaves the skill-tree reward in place, the orphan reward chip is hidden from the page entirely instead of showing a broken-looking placeholder. Tier "Pick N of M" headers reflect the real visible reward count.
Mark hierarchy — two flavors, two roles
The two marks (Hunter's Mark, Sunder) each occupy a clear corner:
- Hunter's Mark — ranged tracker (1.20× / 10 s / 4 hits — lower per-hit, longer window, more total budget)
- Sunder — armor-shred drag-out tax (1.25× / 6 s / 5 hits)
Held-weapon gating
Many abilities require the appropriate weapon category — Power Strike + Leap Strike + Rallying Cry need a sword, Whirlwind + Sunder hold an axe, Shield Bash + Shield Slam + Guardian's Call need a shield, Vampiric Strike + Shadow Pounce need daggers, Hunter's Mark + Piercing Shot + Volley need a bow. Hot-swap to the wrong weapon mid-fight and the cast aborts cleanly — no cost, no cooldown.
Two ways to cast — keyboard or inventory item
Hotkey casting on slots 7/8/9 is the default. Players who prefer mousewheel-friendly hotbar navigation can opt into inventory-item casting on the abilities page (or in /settings): toggle "Cast Abilities via Inventory Item" on, and three new items appear in your inventory — one per ability slot. Use an item to cast the ability bound to that slot. The items are reusable (don't get consumed), and while the mode is on, hotbar slots 7-9 work as ordinary hotbar slots. If you ever lose an item, click "Restore Missing Items" on the abilities page (15-minute cooldown).
UI
- Cooldown HUD — pip-style cooldown bar shows your bound abilities and their remaining cooldowns. Each pip displays the ability's icon (the same one shown on the bind page) with its hotbar key above and a seconds counter below — hover to see the ability name. A subtle blue fill behind the icon drains as the cooldown ticks down. Configurable from the Admin Config menu (vertical anchor / horizontal align / X-Y offsets)
- MultipleHUD compatibility — if the MultipleHUD mod (Buuz135) is installed, the ability HUD coexists with other HUD-using mods (Coordinates HUD, Block Information, Equipment Plus HUD) instead of replacing them. Auto-detected at startup; falls back to the standard slot if not installed
- Bind page filtering — search box (filters by name or flavor text), sort buttons (Name / Level / Skill), skill filter dropdown, category chips (Combat / Gathering / Crafting / Misc), Hide Locked toggle, empty-state messages
- Distinct icons for every ability — each ability shows its own thematically picked Hytale item icon on the bind page
- Reward icons across all menus — every reward that grants an item shows its actual item icon next to the reward text in the skill tree, item rewards, achievements, and quest menus. Stat-shaped rewards (like "+5 Max Health") get a representative icon synthesized from the reward type. Locked rewards appear greyed out with a "Lv N" badge so you know which level milestone unlocks each one. Native Hytale item tooltips on hover
Configuration
- Fully tunable abilities.json — Damage, cooldowns, radii, projectile models, sounds, damage causes, animations, on-hit effects, XP routing, and nested behaviors all live in the config
- Configurable damage cap — server admins can tune the pre-defense ability multiplier ceiling via
mods/mmoskilltree/skill-config.json(maxPreDefenseMultiplier, default 4.0). Raise it to allow bigger combo numbers, lower it for tighter PvE balance reward-icons.jsonoverride config — server admins can remap any reward icon. Three precedence layers: per-skill overrides (highest), per-combat-target overrides, and per-reward-type defaults. Set a value to"__DISABLED__"to suppress an icon entirely
Ability localization
Active ability names, flavor text, and the dynamic damage/radius/duration summary are translated into the player's chosen language. Numeric values come straight from the ability's config, so balance changes propagate to every language without re-translation.
- Ability names — all default abilities resolve their display name through localization
- Flavor descriptions — the hand-written intro line on the bind-page tooltip is per-language
- Dynamic summaries — the auto-generated sentence describing each ability's effect (e.g. "Launches a projectile dealing 40 damage in a 4-block radius.") is built from per-archetype templates that translators can re-order to fit each language's natural sentence flow
Ground AoE visuals
Each ground AoE has its own bespoke effects so they read as distinct beats in combat:
- Meteor — telegraph ring during the 2.5-second drop, with a falling fireball timed so its visual impact lands on the same frame as the damage tick, then a cataclysmic fire burst on hit
- Volley — overhead arrow streaks plus 4 cosmetic arrows per tick that freefall from sky-height onto random points in the zone
- Seismic Slam — dust telegraph during the windup, stone-impact windup sound, and a heavy earth-stomp on landing
- Thorn Patch — a foliage rustle on cast, sprouting plant burst, and small poison drips per tick
Cast / impact / tick visuals are separate beats — a ground AoE can play one particle on cast (telegraph), a different particle when damage first lands (impact), and a third per tick. Per-tick sound effects are configurable.
Bug fixes
/mmoawardxpworks without a trailing world argument — the admin XP-award command no longer requires--worldto be passed. Run/mmoawardxpfrom console, in-game, or from a quest-reward command and it just works. The command picks up the world from the player sender, or from the first available online player when run from console. Old callers that still pass a world name continue to parse cleanly- Custom Skills admin page opens again — the Custom Skills page (and its wizard) crashed silently on open with an internal array index error after the acrobatics movement triggers were added in 0.16.1. Both pages now render correctly from every entry point (admin dashboard button and help guide)
- Quest claim button refreshes after claiming a repeatable quest — used to stay visible after claim, making it look like rewards weren't dispensed. Rewards were always being granted; only the row visuals were stuck. The page refreshes on a successful claim so the quest moves out of the active list and the row reflects its new state (cooldown, or available again if there's no cooldown)
v0.16.1
Skills
- Acrobatics no longer requires hurting yourself to level up - The skill used to award XP only when you took fall damage and lived, which meant the fastest way to train it was to deliberately jump off cliffs. Acrobatics now also gains XP passively as you play:
- Sprint - Earn Acrobatics XP for every ~10 metres you sprint on the ground (default 10 XP per interval). Swim-sprinting is handled separately by the swim track so the two don't double-count
- Swim - Earn Acrobatics XP for every ~10 metres you swim through water (default 15 XP per interval - swimming is slower so the per-metre rate is higher)
- Safe Landings - Landings from 5+ blocks now award 1 XP per 1% of fall-damage reduction you've invested in. Partial reduction = partial payout; 50% reduction = 50 XP, 100% = 100 XP. If your reduction is partial, the existing Fall XP still fires too - this is the "stuck the landing" reward that scales with how much you've actually invested in fall reduction. Players with zero reduction earn nothing extra; sub-threshold hops (under 5 blocks) reward nothing either. The per-1% value is the
SafeLandingentry in the Acrobatics XP map, so server owners can rebalance it freely - Dangerous Fall Survived - Fires on landings from 5+ blocks that your 100%+ fall-damage reduction fully nullifies. Awards 150 XP per landing - a premium over the standard Fall trigger, since it requires a full investment in fall-damage reduction. Stacks with Safe Landing on the same landing
- Fall damage XP rebalanced - Surviving a damaging fall now awards 50 XP (down from 100) since it's the easiest trigger to farm. Taking fall damage still trains Acrobatics - the new Safe Landing and Dangerous Fall Survived rewards are the higher-tier payouts for players who've leveled their fall-damage reduction to full
- Fall damage reduction cap removed - Fall damage reduction used to silently cap at 90%, which meant Safe Landing XP could never actually trigger. Reduction now stacks uncapped across skill choices - once your combined fall reduction hits 100%, you take zero fall damage and start earning Safe Landing XP instead. (Regular defense/physical damage reduction is unchanged and still caps at 90%)
- Updated description - Acrobatics is now described as "Run, swim, and stick your landings to gain experience" in the skill tree and registry
Achievements
- Sprint and swim distance achievement chains - Two new five-tier chains for the Acrobatics movement triggers introduced this version:
- Sprint - Jogger (1 km) → Runner (10 km) → Marathoner (50 km) → Pathfinder (250 km) → Globetrotter (1,000 km)
- Swim - Wader (500 m) → Swimmer (5 km) → Diver (25 km) → Mariner (100 km) → Leviathan (500 km)
- Each tier awards Acrobatics XP scaling with the distance milestone; the top tier of each chain also drops a 2× Acrobatics boost token for 60 minutes
- Movement Master meta achievement - Unlocks automatically when both Globetrotter and Leviathan are completed. Awards +250,000 Acrobatics XP and a 2.5× Acrobatics boost token for 90 minutes, plus 500 achievement points and a server-wide announcement
- Both chains live under a new
movementsubcategory in the Misc category alongside the existing fall damage and survival chains
Statistics
- Sprint and swim distance now tracked as lifetime totals - Two new stats (
sprint_distance_meters,swim_distance_meters) accumulate while you run on the ground or swim. They show up in the Statistics panel alongside playtime, mob kills, and the rest, so you can see how far you've actually travelled across a save
Technical: Sprint/Swim Tracking
- New
StatisticsComponent.KEY_SPRINT_DISTANCE_METERS/KEY_SWIM_DISTANCE_METERSconstants. The component'sTotalscodec already serializes any string key, so no codec or schema bump is required StatisticsService.accumulateSprintDistance(...)/accumulateSwimDistance(...)mirror theaccumulatePlaytimepattern - fractional per-tick meters are batched into per-playerConcurrentHashMapaccumulators (sprintAccum,swimAccum) and only whole meters are committed toStatisticsComponent. The methods return the integer meters committed this call so the caller can dispatch achievement progress with the matching delta.clearPlayer()drops both accumulators on disconnect alongside the existing playtime accumulator- Two new
ObjectiveTypeenum values:SPRINT_DISTANCE,SWIM_DISTANCE.AchievementConfig.parseAchievement()reads triggerType viaObjectiveType.fromString()so JSON overrides pick the new values up automatically - no achievement-config schema bump AchievementService.onSprintDistance(...)/onSwimDistance(...)mirroronFallDamage-hydrateIfNeededthen off-threaddispatch(...), empty target so default chain definitions matchMovementXpTickingSystemsprint and swim branches now callStatisticsService.accumulateSprint/SwimDistancefirst and dispatchAchievementService.onSprint/SwimDistancewith the committed integer-meter delta. Stat writes and achievement dispatch happen independently of the XP threshold cycle, but the existing permission early-return path (PermissionUtil.canGainSkillXpByName == false) still skips both - revoked permissions don't pollute leaderboards- Three new default chain definitions added to
AchievementDefaults.addMiscChains():sprint_t1..t5(chainIdsprint),swim_t1..t5(chainIdswim), and themovement_mastermeta withmetaChildren = ["sprint_t5", "swim_t5"]. SortOrder slots: sprint 30-34, swim 40-44, meta 50 (avoids the existingxp_total20-24 range)
Technical: Movement XP System
- New
MovementXpTickingSystem extends EntityTickingSystemqueriesMovementStatesComponentandVelocityeach tick to drive sprint/swim distance accumulators and detect onGround transitions. Per-player accumulator state lives in aConcurrentHashMapand is cleared on disconnect viaMovementXpTickingSystem.clearPlayer(uuid)(wired into the existingPlayerDisconnectEventhandler alongsideComboStateService.clearPlayer) - Four new values added to
TriggerType:MOVEMENT_SPRINT,MOVEMENT_SWIM,MOVEMENT_SAFE_LANDING,MOVEMENT_DANGEROUS_FALL_SURVIVED. Identifiers"Sprint","Swim","SafeLanding","DangerousFallSurvived"match new entries inAcrobaticsDefaults(default XP: 10 / 15 / 1 per 1% reduction / 150). The base"Fall"identifier drops from 75 → 50 XP to match the new bonus tiers - New
SkillService.processTrigger(..., double scale)overload that multiplies the looked-up XP-map value before awarding.MovementXpTickingSystemuses it forMOVEMENT_SAFE_LANDING- passes the integer fall-damage-reduction percent so the XP map'sSafeLandingentry stores the per-1% value (1 by default → 50 XP at 50%, 100 XP at 100%). All other call sites unchanged: the no-arg overload delegates withscale = 1.0 SkillType.ACROBATICSandSkillRegistry.BUILTIN_SKILL_DATAnow list all five triggers (originalFALL_DAMAGEplus the four new movement triggers)SafeLanding/DangerousFallSurvivedvsFALL_DAMAGEmutual exclusion: the ticking system only fires the two zero-damage triggers whenSkillTreeService.getFallDamageReduction(skills) >= 1.0. Below 100% reduction,CombatXpEventSystem.handleFallDamage()continues to fireFALL_DAMAGEas before - never both.DangerousFallSurvivedis un-thresholded (any fall withprevFallDistance > 0);SafeLandingretains itsacrobaticsSafeLandingThresholdBlocksgate for the tall-fall bonus, so both fire on a landing that clears the SafeLanding thresholdMAX_FALL_REDUCTION(= 0.9) constant removed fromCombatDamageEventSystem.applyFallDamageReduction()now clamps only at 1.0 (prevents negative damage / healing on landing).MAX_DEFENSE_REDUCTIONis untouched - regular physical damage reduction still caps at 90%- Three new
SkillConfigfields:acrobaticsSprintThresholdMeters(10.0),acrobaticsSwimThresholdMeters(10.0),acrobaticsSafeLandingThresholdBlocks(5.0). All have public getters/setters and are persisted via the existingConfigDataserialization - Admin Settings page gains three new rows (
#AcrobaticsSprintRow,#AcrobaticsSwimRow,#AcrobaticsSafeLandingRow) under#NotifBatchRow- each with a Label + TextField + Save button. Wiring coversAdminConfigPagefields, constructor init, render,ValueChangedhandlers, three new apply actions (applyAcrobaticsSprint/applyAcrobaticsSwim/applyAcrobaticsSafeLanding), reload + reloadDefaults sync,AdminConfigEventDatafields + codec bindings, andUIUtil.bindTextField+addEventBindingfor the three Save buttons. Three newui.admin.acrobatics_*_labelkeys inEnglishDefaults(other languages fall back to English, same pattern asui.admin.notif_batch_label) SkillConfig.CONFIG_VERSIONbumped from 11 → 12. Existingskill-config.jsonfiles will be backed up and regenerated with the new thresholds on first load
Technical: Notes and Caveats
- Movement-speed reward type deferred - Native Hytale movement speed lives in the asset-level
MovementConfig, not inDefaultEntityStatTypes, so a "movement speed" reward effect can't be a one-line stat-map bolt-on. Candidate mechanisms (ApplyForceInteraction, customEntityEffectwithapplyConditions, per-playerMovementConfigoverride) will be spiked in a follow-up - Sprint-XP anti-cheat - The sprint accumulator trusts the client's
MovementStates.sprintingflag. Exploitation isn't economical at default rates, but a later update can layer inClientMovementpacket filtering if needed
Bug Fixes
- Achievement search bar now works - Typing a letter in the achievement list's search field no longer instantly clears what you just typed. The field now properly captures keystrokes and filters the list when you click the Search button (or press Enter), matching the behavior of the Quest page search
- Statistics panel no longer shows empty drill buttons - Stats that don't have a drill-down detail page (e.g. Damage Dealt, Deaths, Playtime) no longer show an empty ">" button column on the right. Only stats with a breakdown view (mob kills, blocks broken, fish caught, skill XP) display the drill button
Technical: UI Fixes
AchievementPagesearch field now uses the@SearchInputdynamic value resolution pattern already established inQuestPage:ValueChangedfires a lightweightsearchInputaction that only updatesthis.searchTextlocally (so the text field keeps focus across keystrokes); only the Search button'sActivatingevent reopens the page with the new filter applied. Also corrected#SearchField.Text→#SearchField.Value(TextField's property isValue) and unfinalizedsearchTextso the in-place update is legalStatCard.ui#DrillBtnnow defaults toVisible: false;AchievementPage.appendStatSectionflips it on only when a drill key is supplied. Prevents the 22-px button slot from reserving layout space for stats that aren't drillable
v0.16.0
Skills
- Removed five skills: Taming, Cooking, Smithing, Repair, Alchemy - These skills have been cut from the built-in roster. The plugin now ships with 20 built-in skills: five Gathering (Mining, Woodcutting, Excavation, Harvesting, Fishing), twelve Combat (Swords, Daggers, Polearms, Staves, Axes, Blunt, Archery, Magic, Artillery, Unarmed, Defense, Acrobatics), and three Crafting/Misc (Crafting, Enchanting, Building). Existing player data for the removed skills is preserved in saves but will no longer receive XP or display in the UI. Remove any references to these skills from your skill-tree, item-requirement, quest, and achievement override configs before upgrading
Achievements
Chained Tiered Achievements
- Chained achievement tiers - Related achievement tiers (First Blood → Warrior Initiate → Seasoned Fighter → Legendary Hunter → Warlord, and similar chains for mining, woodcutting, fishing, harvesting, crafting, building, elite kills, and mob-specific kill tracks) now group into named chains. The main list only shows the current active tier in each chain - once you unlock Tier 1, Tier 2 takes its place on your list. Pinning a specific future tier keeps it visible; searches still find any tier. Massively cuts down on list clutter once you've played for a while
- Unlocks Next section on detail page - Click into any tiered achievement's details and you'll see a new "Unlocks Next" card that previews the next tier's name and requirement. The side panel's Related section now also shows every tier of the chain with checkmarks, so the full progression path is one glance away
- Four new trigger types -
COMPLETE_QUEST(withDAILY/WEEKLY/REPEATABLE/NORMALqualifier),TAKE_FALL_DAMAGE(cumulative fall damage taken),PLAYER_DEATH(death counter),GAIN_XP(per-skill or total). All four support the same config shape as existing triggers -targetstring,qualifier,requiredAmount- and plug into the off-thread achievement worker with zero tick-time cost - Seven new default chains - Complete quests (1 / 10 / 50 / 250 / 1,000), dailies (5 / 25 / 100 / 365 / 1,000), weeklies (1 / 10 / 52 / 104 / 250), repeatables (10 / 100 / 500 / 2,500 / 10,000), take fall damage (100 / 1K / 10K / 50K / 250K HP), die (1 / 25 / 100 / 500 / 2,000 times), earn XP (10K / 100K / 1M / 10M / 100M). Over 35 new default achievements spread across a new Quests subcategory and a new Survival/Progression split of Misc
- Quest completion dispatches to achievements -
QuestServicenow fires an achievement hook when a quest completes (auto-claim or manual claim). Daily/weekly/repeatable classification is derived from the quest'srepeatable+cooldownSeconds(≥6d → weekly, ≥20h → daily, else repeatable) so existing quest configs just work - no schema migration - Fall damage achievements fire post-mitigation - The existing
applyFallDamageReductioninCombatDamageEventSystemnow publishes the reduced (final) amount to the achievement system, so fall-damage tracking is fair across players with and without fall-reduction skill tree nodes - Player death event system - New
PlayerDeathEventSystem(sibling of the mob-kill system, but inverse filter) dispatchesPLAYER_DEATHwhen a player dies. Uses Hytale'sDeathSystems.OnDeathSystem- same base class as the mob kill handler
- New Achievements system - A WoW-style long-term progression layer that complements quests. Achievements track silently in the background as you play - kill mobs, break blocks, craft items, reach skill levels, catch fish, and more - and unlock with no "accept" step. Unlocks grant XP, commands (like
/giveitem drops), and achievement points that themselves unlock point-milestone rewards at 100 / 500 / 1,500 / 5,000 points. Open the page with/mmoachievementsor via the new Achievements tab on/xp - ~190 default achievements across 5 categories - Combat, Gathering, Crafting, Leveling, and Exploration. ~60 static activity achievements cover kill progression chains (First Blood → Warlord), mob-specific targets, cumulative damage milestones, elite tier kills (Rare Hunter / Elite Slayer / Legendary Slayer / Against All Odds feat), multi-criteria cross-skill combos (Renaissance Warrior, Ranger's Test, Deadeye Trial, Battle Tested, Iron Fortress), gathering chains for Mining/Woodcutting/Fishing/Harvesting, crafting tiers, building milestones, and server-first race achievements. An additional 130+ leveling achievements are auto-generated at startup from the active skill registry - every registered skill, including custom ones, automatically gets a five-tier track: Novice (10), Apprentice (25), Veteran (50), Expert (75), Master (100). Master of All likewise builds its criteria list dynamically so it always requires level 100 in every registered skill. Every default is customisable via JSON overrides in
mods/mmoskilltree/achievements/ - Per-achievement broadcast command - Each achievement can specify an
announcementCommandthat runs on unlock (with{player}and{achievement}placeholders). Defaults useeventtitle @a {"text":"..."}for the difficult ones so the whole server sees major milestones. Silent by default - keep quiet achievements quiet - Hidden achievements - Set
hidden: trueon an achievement and it won't appear in the list until unlocked. Perfect for secret goals and lore rewards - Point milestones - Earn enough points and a milestone reward triggers: 300 pts (bronze - 3 Defense XP Shards), 1,500 pts (silver - +25K All XP, 1.5× All XP for 1h), 5,000 pts (gold - +100K All XP, 2× All XP for 2h), 12,000 pts (platinum - +500K All XP, 2.5× All XP for 3h), 22,000 pts (diamond - +2M All XP, 3× All XP for 4h). Thresholds scale to the ~27,000-point pool across the 238 default achievements, so each tier represents a meaningful progression checkpoint. Each milestone claims once per player; override the thresholds and rewards in
mods/mmoskilltree/achievements/point-milestones.json
Manual Reward Claiming
- Two reward lists per achievement and milestone - Every achievement and every point milestone now has two reward lists. Auto-claim rewards fire instantly the moment you unlock - XP, boost tokens, broadcast commands, anything that doesn't need an inventory slot. Manual-claim rewards wait for you to press the new yellow CLAIM REWARDS button on the achievement details page (or CLAIM on a milestone card). All default item drops have been moved to manual so nothing is silently lost when your bags are full
- Inventory-space check before granting items - Pressing CLAIM bundles every
/givein the manual reward list and runs them through the same combined inventory-space check the skill-tree rewards use. If even one slot is missing you'll see a "Not enough inventory space to claim!" toast and the rewards stay pending until you make room. No more lost loot from a packed inventory at unlock time - Reward state visible per row - The achievement details page now tags each reward inline: "Auto" (already granted on unlock), "Pending" (yellow - waiting for the CLAIM button), "Claimed" (green - already taken), or "Locked" (grey - achievement still in progress). Glance once, know what's owed and what's done
- Milestone status flips to READY - Milestone cards now show three states instead of two -
progress / thresholdwhile you're working on it, READY in yellow with the CLAIM button when you've earned it, CLAIMED in green once you've taken the rewards
Quests Can Now Require Achievements
- Achievement-gated quests - Quests can require specific achievements to be unlocked before becoming visible/acceptable. Use the new
requiredAchievements: ["ach_id"]shortcut on any quest, or nestachievementsinside a structuredprerequisitesblock for AND/OR logic alongside quest and permission prereqs. Locked quests display the requirement ("Unlock achievement: Master Miner") on their row
WoW-Style Depth
- Meta-achievements - A meta achievement (set
metaChildren: [...]on the definition) unlocks automatically once every listed child is unlocked. Cascades through meta-of-metas so your server's capstone rewards can be chained. Load-time validation warns and disables metas with missing, disabled, or cyclic references - bad config won't crash the server - Multi-criteria achievements - A single achievement can now require multiple conditions to be met - e.g. "Renaissance Warrior" (defeat 100 in melee AND 50 Skeletons with ranged AND reach Defense 25). Each criterion tracks its own progress independently; the achievement unlocks only when ALL are met. Describe each criterion with its own
displayTextso the UI can render each bar with a custom label - Server-First achievements - Set
serverFirst: trueand only the first player on the server to complete it gets the unlock. A persistentserver-first-claims.jsonrecords the claimant's UUID, username, and timestamp. Losers see a "Server First missed" notification; winners get the gold SERVER FIRST badge on their row. A single synchronizedtryClaim()deterministically picks one winner in a race - Feats of Strength - One-of-a-kind accomplishments that don't count toward your point total, don't drive milestone thresholds, and stay hidden until unlocked (appear in a separate "Feats of Strength" section at the bottom of the page, WoW style). Use these for event trophies, alpha-tester badges, or unrepeatable world events
- Pinning - Pin up to 5 achievements to your tracker. Click the pin button on any row to toggle. Pinned in-progress achievements surface in the Tracked side panel on
/xpand rise to the top of the achievement list. Your 5-slot cap is enforced; over-cap pins show a toast instead of silently replacing - Nested subcategories - Categories can be split into subcategories (Combat → Melee / Ranged / Bosses; Gathering → Mining / Woodcutting / Fishing / Harvesting). A second row of tabs appears when a main category has subs; pick one to narrow your browse further
- Search bar - Type any fragment of an achievement's name, description, or criterion text to filter the list. Case-insensitive; also searches child names within meta achievements. Hidden achievements stay hidden in search results - no spoilers
- Point totals ignore feats of strength - Your displayed achievement points + the thresholds that drive milestone rewards cleanly skip feats. A rare event reward feels special without skewing the steady long-term progression curve
Performance
- Achievement progress runs off the world thread - Every event (block break, mob kill, craft, etc.) already fires on the world thread with the quest system; achievements extend that hook with a single-thread worker that updates an in-memory progress cache off the tick, then hops back to the world thread only to persist and dispatch rewards. Means a busy server with many achievements and many players pays almost nothing in tick time for achievement checks
- Periodic flush + disconnect flush - Progress is flushed from the in-memory cache into the player's
AchievementComponentevery 30 seconds and on disconnect, so progress survives restarts even if players stay connected for long sessions
Admin Controls
- Global master toggle for achievements - The admin settings page (
/mmoconfig→ Settings) now has an Enable Achievements switch, matching the existing Enable Quests and Enable Item Rewards toggles. Flipping it off instantly freezes the whole achievement subsystem - no progress tracking, no unlocks, no claim button - and the Achievements tab disappears from the cross-page navigation. Flip back on and events resume immediately without a restart. Useful for servers running a focused season where only quests should drive progression, for debugging a runaway unlock, or for staged rollouts
Default Achievement Overhaul
- Per-skill leveling milestones now chained - Every skill's five level milestones (10 / 25 / 50 / 75 / 100) are now a single visible chain - unlock the Novice and Apprentice takes its place on the list. Pin a specific tier to keep it visible. Tier 1-4 grants flat skill XP (250 / 1,500 / 10,000 / 50,000 XP calibrated to the OSRS curve); Tier 5 (level 100) grants both a hefty 250,000 XP payout and a celebratory 2.5× boost for 2 hours
- Rebuilt Server First set - Removed the two vague "any skill" server-first achievements and replaced them with a focused set: Crown of the Realm (first to reach level 50 in every skill), Sovereign of Skills (first to 75 in every skill), Master of All (first to 100 in every skill - hidden), plus one server-first for each individual skill at level 100 ("First Mining Master", "First Swords Master", etc.). 23 server-firsts total, driving the long-game race across all 20 active skills
- Six new ore-mining chains - Copper, Iron, Silver, Gold, Mithril, and Adamantite each get a four-tier chain (Novice / Adept / Master / Legendary at 200 / 1,000 / 5,000 / 25,000 blocks mined). Replaces the standalone
iron_rushandgold_feverachievements and - as a side benefit - fixes a latent bug where ore variants in different biomes (basalt, shale) didn't count toward progress - Legendary tier extensions - Three gathering chains now reach a new top tier: Legendary Lumberjack (100,000 wood), Legendary Angler (25,000 fish), Legendary Harvester (50,000 crops). Each grants flat XP and a celebratory boost
- Two new meta-achievements - Legendary Gatherer (requires Legendary Miner + Lumberjack + Angler + Harvester) and Legendary Warrior (requires Warlord + Force of Nature + Legendary Slayer). Each awards 1,000,000 All XP and a 3× All Skills boost for 4 hours on claim
- Reward mix rebalanced toward flat XP - Most mid-tier achievements now grant flat skill XP rather than temporary boost tokens. Boosts are reserved for the top tier of each chain, meta achievements, server-firsts, and the Silver / Gold / Platinum / Diamond point milestones - and every boost-bearing reward *also* grants flat XP, so claiming a top-tier reward always gives you something permanent. Mid-tier boosts converted include Legendary Hunter, Apex Predator, Skeleton Vanquisher, Elite Slayer, the cross-skill feats (Renaissance Warrior, Ranger's Test, Deadeye Trial, Battle Tested, Iron Fortress), Ore Prospector, Forest Runner, Seasoned Angler, Field Worker, Craftsman, Master Artisan, Grand Artisan, Architect, and every quest-completion and survival chain tier below the cap
Technical: Achievement Master Toggle
- New
SkillConfig.enableAchievementsfield (defaulttrue) withisAchievementsEnabled()/setAchievementsEnabled()accessors, persisted toskill-config.jsonalongsideenableQuests. No schema version bump - missing field defaults totrue AchievementService.dispatch()short-circuits when the flag is off, covering every event hook that funnels through it (onEntityKill,onBlockBreak,onBlockPlace,onCraftItem,onPickupItem,onDealDamage,onCatchFish,onQuestComplete,onFallDamage,onPlayerDeath,onGainXp).onLevelReachedgets its own guard since it bypassesdispatch.applyUnlocks()andclaimAchievementRewards()also guard defensivelyAdminConfigPageadds a#ToggleAchievementsbutton +#AchievementsLabelrow (newui.admin.achievements_labelkey in all 9 language files)ViewXpPage,BoostPage,QuestPagehide#TabAchievementsand gate the"achievements"click action when disabled - matching the existing quest gating pattern
Statistics Tracking
- Persistent per-player statistics - Every player now accrues lifetime counters across 11 categories: mobs defeated, blocks broken, blocks placed, items crafted, items picked up, damage dealt, damage taken, deaths, fish caught, XP earned, and seconds played. Per-item maps (which mob types, which blocks, which skills) track breakdown for richer reporting. Updates inline on the world thread via
ConcurrentHashMap.merge- zero tick-time cost - Playtime tracking - Accumulated via the regen ticking system every frame; whole seconds commit to the component as they add up. Restart-safe, offline-safe
Achievement UI/UX Polish
- New Achievement Overview landing page -
/mmoachievementsopens to a summary/overview view first. Shows your most recent unlocks with dates, achievements you're nearest to completing, a category-completion grid (e.g. Combat 12/34 with a mini progress bar), and your progress toward the next point milestone. The full filterable list is one click away via Browse All - Unlock dates on achievement rows - Every unlocked achievement now shows the date it was earned (ISO format,
YYYY-MM-DD). Historical unlocks from before this iteration show--since no timestamp was recorded at the time; new unlocks from now on get a real date - Reward previews on achievement rows - Each achievement row now shows a compact one-line preview of its rewards ("Rewards: Defense XP Shard, 2x All XP 1h, ..."), truncated at ~80 characters. No more mystery about what you'll get until after you unlock
- New Details view - Click "Details >" on any achievement row to open a full-detail page with the big header, long description, every criterion with its own progress bar, the complete reward list, and clickable meta children. The page also surfaces which meta achievements this one is part of, plus related achievements in the same category/subcategory. Admins additionally see the on-unlock announcement command as a diagnostic
- Filter-preserving back navigation - Opening a Details page from a filtered list and clicking Back returns you to the exact filters you had set (category, subcategory, search, sort, status). Opening from the Summary lands back on the Summary
UI/UX - Wider Menu Pages With Page-Specific Side Panel
- Three-column menu layout - The four main menu pages (
/xp,/mmoachievements,/boosts,/mmoquests) widen from 1200 to 1570 pixels and gain a new right-side panel whose contents are unique to each page. The left sidebar stays as cross-page navigation, the centre panel keeps its existing list/content, and the new side panel shows "at a glance" context you shouldn't have to click away to find - Achievements page side panel - Statistics - Lifetime stats render permanently alongside the achievement list. Five sections (Combat / Gathering / Crafting / Leveling / Playtime) show your counters for mob kills, blocks broken/placed, items crafted, damage dealt/taken, deaths, fish caught, XP earned, and total playtime. Top-3 breakdowns ("mushroom x128, pork x64, …") render underneath counters for mobs, blocks, fish, crafted items, and per-skill XP
- XP page side panel - Tracked Achievements - Up to 5 pinned in-progress achievements render as compact rows with live progress bars. Click any row to open the achievements page scrolled to that entry. When you've pinned nothing, an empty-state prompt nudges you to pin. Fully unlocked pins don't clutter the tracker - they drop out so only your current work shows
- Boosts page side panel - Active Boosts - The active-boost list sits in the side panel, always visible while you browse tokens and reusable boosts in the main content. The tokens list has twice the vertical space - fewer scrolls to find what you want to redeem
- Quests page side panel - Tracked Quest detail - The first active quest's title, description, objectives (with live
x/yprogress), and reward summary render large in the side panel. Makes "what am I doing right now?" a passive glance instead of a scroll - Pin button uses ASCII-safe glyphs - Star glyphs (
★/☆) aren't in Hytale's UI font. The pin button uses+/-with per-state background colours (gold fill when pinned, slate-grey when unpinned) so the state is obvious at a glance - Per-criterion rows now render inline - Multi-criteria achievements (e.g. Renaissance Warrior) show one row per criterion beneath the aggregate bar - each with its own
x/yprogress text and a "met" tick. Meta achievements render the list of child achievements as clickable chips that open the child's page
Quest Tracking + Menu Navigation Parity
- Track up to 5 quests - Active quests now have a track button in their row header. Tracked quests show in a new "Tracked Quests" section on the main XP page's right-side panel, just below the tracked-achievements list, with a progress bar showing completed / total objectives. Five tracked-quest slots, enforced the same way achievement pinning is, with a localized toast when you hit the cap. Tracked quests auto-hide when they complete or are abandoned
- Click through to the quest - Clicking a tracked-quest row on the XP page jumps straight to the Quests page
- Navigation parity across the four menu pages - All four menu pages expose the same full navigation set (Overview, skill categories, Leaderboard, Boosts, Quests, Achievements, Settings, Admin) - no page is missing tabs the others have
- New Achievements tab on Boosts and Quests pages - The left-panel navigation on Boosts and Quests now exposes a direct Achievements tab so you're always one click away from the achievement list
Admin Tools
- New
/mmoconfig achievementsubcommand - Seven operations:award(force-unlocks with server-first handling),[--force] revoke(clears state + all criterion progress + cascades to meta parents),reset,[id|all] progress,[criterionIndex] reload(hot-reload config + server-first claims + re-hydrate online players),list [category|all], andpin/unpin. SharedapplySingleUnlockinternals keep the event path and admin path consistent
Public API
- New
MMOSkillTreeAPI.addAchievementListener(Consumer- External plugins can subscribe to achievement unlocks. The event carries the PlayerRef, achievement ID, points awarded, and running total. Listeners run on the world tick thread. Paired with) removeAchievementListener,fireAchievementEvent, and new gettersisAchievementUnlocked,getAchievementProgress,getAchievementPoints,getAchievementComponent. Guild plugins or leaderboards can plug in without touching plugin internals - New
MMOSkillTreeAPImethods -pinAchievement,unpinAchievement,getPinnedAchievements,getStatistics,getStatTotal,getServerFirstClaim. External plugins can read the full picture - pin state, stat totals, claim ownership - without touching plugin internals
Technical: Achievement System Architecture
- New package
com.ziggfreed.mmoskilltree.achievementwithAchievementDefinition(immutable builder, reusesQuestReward+QuestRewardExecutor),AchievementComponent(ECS component with threeConcurrentHashMaps - progress, states, claimedMilestones - codec-persisted viaBuilderCodec), andPointMilestoneReward - New
AchievementConfigsingleton mirrorsQuestConfig's loader pattern: regenerates_defaults/default-achievements.json+_defaults/default-point-milestones.jsonon every server start, loads user files frommods/mmoskilltree/achievements/*.jsonwhen present, builds an inverted indexMapfor O(1) event lookup. Reuses> ObjectiveTypeenum andQuestObjective.MatchMode(EXACT / CONTAINS / PREFIX) - no new trigger vocabulary needed - New
AchievementServiceruns a single-threadScheduledExecutorService(MMOSkillTreeAPI-Achievement-Worker, daemon). Event hooks capture (UUID, worldUuid, event args) as locals on the world thread and submit to the executor. Off-thread path walks the inverted index, atomically increments theConcurrentHashMapprogress cache, and on newly-crossed thresholds calls> world.execute(...)on the capturedWorldto apply rewards + firePlayerAchievementEventon the tick thread. Cache is hydrated fromAchievementComponentonPlayerConnectEventand flushed onPlayerDisconnectEvent, plus a 30s periodic world-thread flush tick - One-line hook added after every existing
QuestService.onX(...)call inMobKillEventSystem,BreakBlockEventSystem,CombatXpEventSystem,CraftRecipeEventSystem,PickupItemEventSystem,PlaceBlockEventSystem, andSkillService.handleLevelUpByName- zero new event-registration cost, shares the same trigger points as quests PrerequisiteGroupextended with anachievements: Listfield alongside the existingquests/permissionslists. New constructor overload keeps binary compatibility; existing two-arg and three-arg constructors default achievements to an empty list.getAllAchievements()added for flat lookup.QuestService.evaluatePrerequisites()now resolves an optionalAchievementComponentfrom thePlayerparameter and applies AND/OR semantics to achievement membership the same way quest-completion is evaluated todayQuestConfig.parsePrerequisiteGroupreads anachievementsJSON key inside a structured prereq object. Top-levelrequiredAchievementsshortcut merges into the root prereq group's achievement list, same pattern as flat-array prerequisites.QuestDefaults.prerequisiteGroupToJsonserializes the field when non-empty;questToJsonemits structured prereqs when achievements, groups, or permissions are presentMMOSkillTreeAPIadds aCopyOnWriteArrayListmirror of the existing XP listener registry plus four read getters.> apiGradle subproject gainsPlayerAchievementEvent(record-shaped class withplayerRef,achievementId,pointsAwarded,totalPoints) alongsidePlayerGainXpEvent- New UI:
pages/AchievementPage.javaextendsInteractiveCustomUIPage; four new.uitemplates underCommon/UI/Custom/Pages/(AchievementPage.ui,AchievementRow.ui,AchievementCategoryTab.ui,PointMilestoneCard.ui) follow the styling conventions ofQuestPage.ui/QuestRow.ui.ViewXpPagegets a new#TabAchievementsbutton that routes toAchievementPage. NewAchievementsCommand(aliasesmmoach,achievements) registered alongside other player commands - All 9 language default files (English, Spanish, French, German, Italian, Portuguese, Hungarian, Turkish, Russian) receive 22 new keys covering the Achievements page UI, unlock/claim notifications, 5 category labels, and the
quest.requires_achievementrequirement-line string MMOSkillTreePlugin.setup()addsAchievementConfig.load(), registersAchievementComponentcodec, startsAchievementService.getInstance().start(), registersAchievementsCommand, hydrates the cache onPlayerConnectEvent(viaholder.ensureAndGetComponent(AchievementComponent.TYPE)), flushes onPlayerDisconnectEvent(world-thread hop), and callsAchievementService.getInstance().shutdown()in the shutdown hook
Technical: Meta / Multi-Criteria / Server-First
- New
AchievementCriterionclass (triggerType,target,matchMode,qualifier,requiredAmount,displayText,index).AchievementDefinitionhascriteria: List,metaChildren: List,serverFirst,featOfStrength,subcategory. Absentcriteria, a 1-element list is synthesized from the top-level trigger fields;metaBuilder(id)bypasses trigger parsing for metas.isHidden()returns true for feats regardless of the explicithiddenflag - Progress keys use the composite
"format across the codec.# " AchievementComponent.criterionKey(id, idx),getCriterionProgress,setCriterionProgress,clearAllProgress(id)helpers paired with the new scheme. NewpinnedAchievements: ConcurrentHashMap(id → pin-time epoch) field + pin/unpin/getPinnedIds/MAX_PINNED=5 helpers appended to the codec viaBuilderCodec.append(...). Achievement IDs now reserve#in addition to|,=,: AchievementConfiggains JSON round-trip forcriteria,metaChildren,serverFirst,featOfStrength,subcategoryplus root-levelcategoryOrder,subcategoryOrder. Meta validation runs after load: missing/disabled child detection disables the parent (one log line), three-color DFS cycle detection disables both participants. Built childId → parentIds index (getMetaParentsForChild) drives parent re-evaluation. Inverted trigger index now carriescriterionIndexfor per-criterion dispatchAchievementService.checkTriggers/checkLevelReachedupdate the criterion-keyed progress cell, then callisAllCriteriaMet(achId, progress)before unlocking.applyUnlocksbranches intoapplySingleUnlock(comp, skills, playerRef, username, def, skipServerFirstCheck, runRewards)- shared by event path and admin path - and cascades to meta parents using a breadth-first queue. Server-first races resolve viaServerFirstClaimStore.tryClaim(synchronized + file-backed).computePointsTotaland the running total inapplyUnlocksboth filter out feats. Newrevoke(comp, def, playerId)cascades to parent metas- New
service/ServerFirstClaimStore.javasingleton - world-thread-onlytryClaim, JSON atmods/mmoskilltree/achievements/server-first-claims.json. Record:{achievementId, claimantUuid, claimantUsername, unlockedAt}. Loaded on setup, hot-reloaded by adminreload
Technical: Statistics Component
- New
statistics/StatisticsComponent.javawith 8 pipe-delimitedConcurrentHashMapfields (totals plus 7 per-category breakdowns). Canonical keys are class constants (KEY_MOB_KILLS, …,KEY_PLAYTIME_SECONDS). Codec mirrorsAchievementComponent's serialization helpers - New
service/StatisticsService.javasingleton - thin wrappers (onMobKill,onBlockBreak,onCraftItem, …) called inline from the existing event systems. No off-thread worker, no flush cycle. Playtime uses a transient dt accumulator keyed by player UUID; whole seconds commit to the component as they accumulate, so variable-dt ticks don't drift - One-line hook added in
MobKillEventSystem,BreakBlockEventSystem,PlaceBlockEventSystem,CraftRecipeEventSystem,PickupItemEventSystem,CombatXpEventSystem,CombatDamageEventSystem,SkillService.addXpByName, andRegenTickingSystem(playtime). Each hook is conditional on the component existing - missing component → early return, no NPE.PlayerConnectEventensures it exists viaholder.ensureAndGetComponent(StatisticsComponent.TYPE)
Technical: Achievement UI/UX Polish
AchievementComponentgains a fifth codec fieldunlockTimestamps: ConcurrentHashMap(achievementId → epoch millis). Schema extension is append-only perBuilderCodec.append(...); missing field on read deserializes to an empty map so pre-0.16.0 saves load cleanly.AchievementService.applySingleUnlockcallsputIfAbsent(id, System.currentTimeMillis())after flipping state to CLAIMED;revoke(...)and the server-first-lost branch both.remove(id);/mmoconfig achievement reset allalso clears the map.getUnlockedAt/hasUnlockTimestamphelpers- New
pages/AchievementDetailPage.java+Pages/AchievementDetailPage.ui- full-page detail view (three-column layout matching the other menu pages; left panel is cross-menu nav, centre is header + meta row + description + criteria + meta children + rewards, right panel is "Part of" meta parents plus "Related" siblings in the same category/subcategory). Constructor carries the source filter tuple (srcCategory,srcSubcategory,srcSearch,srcSort,srcStatus,srcKind) so the Back button rebuilds the exact caller. Rewards render via the existingQuestObjectiveRow.uitemplate for precedent parity withQuestPage - New
pages/AchievementSummaryPage.java+Pages/AchievementSummaryPage.ui- the new landing page. Header exposes unlocked/points/next-milestone with progress-to-next; body has RECENT UNLOCKS (top 5 byunlockTimestampsdesc via newAchievementRecentUnlockRow.uipartial), NEAREST TO COMPLETE (top 5 by completion ratio desc, excludes feats/hidden/lost-server-firsts, reusesAchievementRow.ui), CATEGORIES grid (newAchievementCategorySummaryCard.uipartial with mini ProgressBar), and a Browse All button that opens the fullAchievementPagelist view. Right panel reuses the statistics panel via a newly-staticAchievementPage.populateStatisticsPanel AchievementRow.uihas#UnlockDate(right-aligned in the header row),#RewardsSummary(below the progress row), and a#DetailsBtnbutton (Details >, bottom of the row). Pin button is in the header row - click isolation from the Details button.AchievementPage.populateRowfills the new fields;handleDataEventgets a new"details"case that opensAchievementDetailPagecarrying the current filter tuple. New helpersformatUnlockDate(long, SkillComponent)(ISOYYYY-MM-DDviaSimpleDateFormat, dash fallback for missing timestamps) andbuildRewardsSummary(List(comma-joined display names, 80-char truncation) are static so the summary page can reuse them, SkillComponent) - Every bare
new AchievementPage(playerRef)call (fromAchievementsCommand,BoostPageachievements tab,QuestPageachievements tab,ViewXpPageachievements tab,StatDetailPageBack button) constructsnew AchievementSummaryPage(playerRef)- the summary is the entry point. Filter-specific constructors (new AchievementPage(ref, category, ...)) are direct deep links into the list view and remain as-is - 19 new localization keys added to all 9 language default files (English, German, French, Spanish, Italian, Portuguese, Hungarian, Turkish, Russian). Keys cover the Details button, Back button, Browse All / Browse > buttons, Rewards label/header, Unlocked date label, unknown-date
--marker, announcement label, detail-page not-found / meta-parents / related / no-related strings, and the summary page title / recent / nearest / categories headers plus the two empty-state strings and the next-milestone label
Technical: UI & Localization
AchievementPage.javaconstructor is(playerRef, filterCategory, filterSubcategory, searchText, highlightAchievementId). Two-arg constructor preserved as a shim for call sites that don't need filters.AchievementEventDatacodec hasSubcategory,SearchInput,AchIdkeys. Renders a second-level subcategory tab row when the selected category has subs, a search bar with Search/Clear buttons, a pin button per row that toggles pin state (reopens the page so sort-order applies), a gold SERVER FIRST badge + claim-ownership line on server-first rows, and a separate unlocked-feats section at the bottom. Multi-criteria rows render an aggregate "N/M criteria" progress bar; single-criterion achievements use the normal barAchievementRow.uiincludes#PinBtn,#ServerFirstBadge,#ServerFirstClaimed.AchievementPage.uiincludes#SubcategoryTabswith#FilterSubAll+#DynamicSubcategories, a#SearchFilterRowwith$C.@TextField #SearchField+#SearchBtn+#ClearSearchBtn, and a#FeatsHeader+#FeatsListsection- All 9 language default files received ~35 new keys for the sub-view UI, criteria aggregate, server-first badge + lost/won notifications, feats header, view toggles, stats labels, and pin cap toast. Non-English files get English verbatim with
// TODO: localizecomments per plugin convention
Technical: Quest Tracking + Nav Parity
QuestComponentgainstrackedQuests: ConcurrentHashMap(questId → pin timestamp) withMAX_TRACKED=5, helpersisTracked/track/untrack/getTrackedCount/getTrackedIds, codec entry"TrackedQuests"via the existingserializeLongMap/deserializeLongMaphelpers. Codec defaults missing field to empty on read, so existing save data migrates cleanly without a version bumpQuestServiceadds statictrack(comp, id),untrack(comp, id),getTracked(comp)wrappers with existence validation againstQuestConfigQuestRow.uiadds#TrackBtn(+#TrackBtnSpacer) in the header row;QuestPage.populateQuestRowshows the button only for ACTIVE quests, toggles its glyph (+/-) and gold-vs-slate style via newapplyTrackStyle. Handler routes"trackquest"/"untrackquest"throughQuestService.track/untrack, sends a localized toast viasendTrackToast, then reopens the page (event bindings can't be rebound viasendUpdate)- New
TrackedQuestRow.uitemplate (twin ofTrackedAchievementRow.ui) powers the new#TrackedQuestsListonViewXpPage.ViewXpPage.populateTrackedQuestsPaneliteratesQuestService.getTracked(comp), filters to still-active quests, caps at 5, computes aggregate objective progress from the deserializedPlayerQuestDataprogress blob.ViewXpEventDatacodec adds aQuestIdfield; new"opentrackedquest"action opensQuestPage AchievementPage.ui/AchievementPage.javainclude the full left-panel tab set (Gathering/Combat/Crafting/Misc/Leaderboard/Quests/Settings/Admin) with matchingbindTabEventsbindings and acategory/leaderboard/settings/adminhandler branch - routes skill-category clicks toViewXpPagethe same wayBoostPage/QuestPagedo. Feature-gated tabs (Leaderboard/Quests/Admin) follow the existingVisible: falsedefault + conditionalcommandBuilder.set(...Visible, true)patternBoostPage.ui/BoostPage.javaandQuestPage.ui/QuestPage.javaeach add#TabAchievements+ theachievementsaction handler that opensAchievementPage- Six new localization keys across all 9 language files:
ui.viewxp.tracked_quests_title,ui.viewxp.tracked_quests_empty,ui.quests.track_cap_reached,notify.quest.tracked,notify.quest.untracked,ui.settings.tab. Non-English files tagged// TODO: localizeper convention
Technical: Menu Layout & UI Polish
- All four menu
.uifiles (AchievementPage.ui,ViewXpPage.ui,BoostPage.ui,QuestPage.ui) useAnchor: (Width: 1570, Height: 1000). Each has aGroup #SidePanel { Anchor: (Width: 320); Background: "../Common/ContainerPanelPatch.png"; }as the third column of#Content.#RightPanelis the middle content column - IDs aren't shared across pages so no conflicts - New UI templates:
StatCard.ui,StatCategorySection.ui,TrackedAchievementRow.ui,AchievementCriterionRow.ui,AchievementMetaChildChip.ui. All use ASCII-safe text glyphs and existing$C.@Common.uistyles AchievementPage.javaaddspopulateStatisticsPanel(cb, skills, store, ref)- reads fromMMOSkillTreeAPI.getStatistics(...)and falls back to zeros when the component is null (pre-first-connect player). UsestopFromMap(perMobKills, 3)/perBlockBroken/perSkillXphelpers to surface top-N breakdowns underneath each counter. Also addsapplyPinStyle(cb, selector, pinned)to swap button backgrounds at runtimeAchievementPage.populateRowappendsAchievementCriterionRow.uiper criterion whencriteria.size() > 1 && !def.isMeta(), and appendsAchievementMetaChildChip.uiper listed child whendef.isMeta(). Meta chip clicks fire a newopenchildaction carryingChildIdthat reopens the page withhighlightAchievementId=childId(hidden-and-locked children still respect their own hidden flag and don't chip)AchievementEventDatacodec adds aChildIdstring fieldViewXpPage.javaaddspopulateTrackedPanel(cb, eb, skills, store, ref)- readsAchievementService.getPinned(comp), filters unlocked IDs, caps to 5, aggregates progress the same wayAchievementPagedoes.ViewXpEventDatacodec adds anAchIdstring field; newopentrackedaction opensAchievementPagewith the achievement ID as highlight. NewTrackedAchievementRow.uitemplateQuestPage.javaaddspopulateTrackedQuestPanel(cb, activeQuests, quests, skills)- takes the first entry from the existingactiveQuestslist, re-uses existingQuestObjectiveRow.uifor per-objective rendering inside#TrackedObjectives, and writes reward names into#TrackedRewards. Empty-state fallback showsui.quests.no_active_quest. Side panel IDs (#TrackedQuestTitle,#TrackedQuestDesc,#TrackedObjectives, etc.) are distinct from the existing#ActiveQuestListin the main content so no selector collisionsBoostPage.uiputs#ActiveHeader+#ActiveBoostsListin#SidePanel. Same IDs, same populate code - no Java change needed. The main-content tokens list height is 340- Nine new localization keys across all 9 language files:
ui.achievements.stats_title,ui.achievements.stats.playtime_hours,ui.achievements.stats.deaths,ui.viewxp.tracked_title,ui.viewxp.tracked_empty,ui.quests.active_title,ui.quests.no_active_quest,ui.quests.rewards_header. Non-English files tagged// TODO: localizeper convention
Technical: Chained Achievements
AchievementDefinitiongains@Nullable String chainId+int tier(1-based, 0 = not chained).AchievementConfig.parseAchievementreadschainId+tierfrom JSON;achievementToJsonemits them only when set. A newvalidateAndIndexChains()pass builds aMapkeyed on chainId, sorted by tier ascending. Duplicate tiers within one chain reject the chain (logged + individual members still work); non-contiguous tiers warn but still index. Exposed via> AchievementConfig.getChain(chainId),getChainForAchievement(id),getAllChains()AchievementPage.getFilteredAchievementsnow post-processes the filtered list throughcollapseChains(...): chained entries are bucketed bychainId, and for each chain only the active tier (lowest-tier withisUnlocked=false, or highest tier if all unlocked) is emitted. Pinned tiers always bypass collapse; any non-empty search string short-circuits collapse entirely so users can still find specific tiers by nameAchievementDetailPageaddspopulateUnlocksNext(middle column, right after Rewards) andpopulateChainTiers(side panel's RELATED column) which both consultAchievementConfig.getChain(def.getChainId()).AchievementDetailPage.uigains#UnlocksNextHeader/#UnlocksNextListand#ChainTiersHeader/#ChainTiersListcontainers- New
ObjectiveTypevalues:COMPLETE_QUEST,TAKE_FALL_DAMAGE,PLAYER_DEATH,GAIN_XP. Picked up automatically by the existingAchievementConfig.buildIndex()inverted-index builder - no trigger-specific dispatch code added AchievementServiceadds four hook methods (onQuestComplete,onFallDamage,onPlayerDeath,onGainXp) that submit to the existing off-thread worker viadispatch(...). No new threading infrastructureQuestService.checkQuestCompletionandQuestService.claimQuestboth callAchievementService.getInstance().onQuestComplete(...)after reward execution. New helperQuestService.classifyQuestType(QuestDefinition)returns"NORMAL"/"DAILY"/"WEEKLY"/"REPEATABLE"based onisRepeatable()+getCooldownSeconds()(≥6d / ≥20h thresholds).qualifierMatchesinAchievementServicehandles the equality check against the achievement's qualifierCombatDamageEventSystemcallsonFallDamageinside the existingDamageUtil.isCause(damage, "FALL")branch, passing the post-reduction amount- New
PlayerDeathEventSystemextendsDeathSystems.OnDeathSystemwith a Player-component filter (mirror ofMobKillEventSystem's filter line 69 but inverted). Registered inMMOSkillTreePlugin.javaalongside the other event systems SkillService.addXpByNamecallsAchievementService.getInstance().onGainXp(store, ref, playerRef, skillId, adjustedAmount)right afterStatisticsService.onXpEarned. Fires for both total-XP chains (empty-target triggers) and per-skill chains (explicittarget=skillId) in one dispatch - the existing empty-target short-circuit inmatches()handles both cases
Technical: Manual Reward Claim Schema
AchievementDefinitionandPointMilestoneRewardreplace their singlerewardslist withautoClaimRewards+manualClaimRewards. Builder + JSON parser updated to match;getAllRewards()convenience concatenates both for previews. No legacy fallback - 0.16.0 is in development so the schema change is breaking by designAchievementComponentswapsclaimedMilestones: ConcurrentHashMapformilestoneStates: ConcurrentHashMap(valuesSTATE_UNLOCKED/STATE_CLAIMED). Codec key renamed fromClaimedMilestonestoMilestoneStates; new helpersisMilestoneUnlocked,isMilestoneClaimed,setMilestoneState. The previously-unusedSTATE_UNLOCKEDconstant on achievements is now actually written -isUnlocked()(UNLOCKED or CLAIMED) andisClaimed()(CLAIMED only) are the two-state lifecycle gateAchievementService.applySingleUnlockruns auto-claim rewards immediately and writesSTATE_UNLOCKEDwhen the manual list is non-empty (otherwiseSTATE_CLAIMED). Milestone unlock path mirrors the same split. NewClaimResultenum (SUCCESS,ALREADY_CLAIMED,NOT_UNLOCKED,INVENTORY_FULL,NO_REWARDS) returned by two new world-thread public methods:claimAchievementRewards(playerRef, store, ref, id)andclaimMilestoneRewards(playerRef, store, ref, threshold). Both gate onSTATE_UNLOCKED, run command rewards throughCommandRewardService.hasInventorySpaceForCommandsfor a combinedcanAddItemStackscheck, then transition toSTATE_CLAIMED- New static helpers in
CommandRewardService:parseGiveItemStacksFromCommands(Listand) hasInventorySpaceForCommands(Player, List. Both delegate to the existing source-agnostic) GiveCommandParser.parseGiveCommand(...)so the achievement claim path reuses the skill-reward inventory-check pipeline without touchingCommandRewardEntry AchievementDetailPage.populateRewardsrenders the auto + manual lists separately with state markers (Auto/Pending/Claimed/Locked tags).#ClaimRewardsBtnlives inAchievementDetailPage.uiwith anchor-height 0 by default; the page sets it to 32 only whenunlocked && !claimed && !manualRewards.isEmpty()and bindsAction: claim.PointMilestoneCard.uifollows the same pattern with#MilestoneClaimBtnand aclaimMilestoneaction carrying theThresholdstring. BothAchievementPageandAchievementSummaryPageevent-data codecs gain aThresholdstring field plus aclaimMilestoneswitch case routing toclaimMilestoneRewards- New localization keys (English/German/French; other languages fall back to English):
ui.achievements.claim_button,ui.achievements.milestone_claim_button,ui.achievements.reward_auto,ui.achievements.reward_pending,ui.achievements.reward_claimed,ui.achievements.reward_locked,ui.achievements.inventory_full,ui.achievements.claim_success,achievement.point_milestone_unlocked.ui.achievements.milestone_availablerepurposed from "Claim!" to "Ready" MMOConfigCommand.resetAchievementupdated for the field rename -comp.claimedMilestones.clear()→comp.milestoneStates.clear().AchievementConfigmeta-disable rebuild path uses both new reward methods
Bug Fixes
- Fixed false "total level X item rewards available" notification spam - The reward-unlock notification was firing on nearly every level-up once a player crossed any configured total-level threshold, even when every reward at that tier was already claimed or when the configured rewards were not visible in the Rewards page (e.g.
/effectcommands with no display name). Clicking through to/xp > Rewardsshowed nothing to claim. The notification now matches the Rewards UI exactly: it fires only when there is at least one unclaimed reward that the UI would actually render as a claimable card. The same fix applies to per-skill level-up reward notifications
Technical: Reward Notification Filter
SkillService.checkAndNotifyItemRewardsByNamerewritten to iterate rewards per-index and delegate claim checks toCommandRewardService.isRewardClaimed(which already handles both the new"claim-ID format and the pre-0.7.3 legacy_ _ " "fallback). Previous implementation called_ " skills.hasClaimedCommandReward(getLevelRewardId(...))directly, which only ever matched legacy IDs - modern claims write per-index IDs, so the notification's claimed-check was alwaysfalseand fired forever- Added a private
isDisplayable(CommandRewardEntry)predicate inSkillServicethat mirrorsItemRewardsPage.hasDisplayInfo- a reward is displayable if it has alocalizationKey,displayName, or its command contains/give. Notifications now skip rewards the UI would hide, so "rewards available" never points at an empty tier
v0.15.0
Bug Fixes
- Fixed XP gains silently stopping after server restart - Players on permission setups where OP or a wildcard node grants every permission would stop receiving XP after a restart - every block break, mob kill, or other XP trigger silently awarded 0 XP while quest progress continued to work. The player-specific level cap lookup introduced in 0.14.3 was collapsing to level 1 whenever the permission provider answered
truefor broad matches, which made the max-XP threshold equal 0 and the "already at max level" branch fire on every award. No config change needed. Servers withenableSkillPermissionsturned off also benefit: the cap lookup no longer runs when skill permissions are disabled
Technical: Level Cap Scan Fix
PermissionUtil.getEffectiveLevelCapnow short-circuits todefaultCapwhenSkillConfig.isSkillPermissionsEnabled()is false - previously it probed the permission provider every call, leaving the function reachable on servers that had opted out of skill permissions entirelyPermissionUtil.scanForCapnow iteratesupperBound → 1instead of1 → upperBoundand returns the first match. A wildcard-granting provider now yieldsdefaultCap(benign); narrow per-level grants still produce the explicitly-granted cap. Remains O(defaultCap) in the worst case but short-circuits on the first iteration for wildcard providers
Configuration
- Decimal XP values - XP map entries in
xp-maps.jsonandmob-kill-xp.jsonnow accept fractional values (e.g.0.5,2.5,1.25). Useful for fine-tuning XP curves where integer-only values felt too coarse - you can now set a block to give half XP per break or dial a mob to an exact decimal. The admin UI pages for XP Overrides and Mob Kill XP now parse decimal input, and so does/mmoconfig. Integer values still display cleanly without a.0suffix. Existing integer-only configs load as-is - no migration needed
Technical: Decimal XP Plumbing
⚠️ Breaking change for plugins that depend on MMO Skill Tree's public surface. XP-carrying signatures changed fromlongtodouble. If you build another plugin against this jar, you will need to recompile. Specifically:SkillService.addXpByName(..., long)is now(..., double);XpMapsConfig.getXpMapByName / getOverridesByName / getDefaultValueByName / setXpByNameandMobKillXpConfig.getXpForMob / getEffectiveMap / getOverrides / getDefaults / getDefaultValue / setXpnow return/acceptDouble/double;TriggerResult.xpAwardedisdouble;CombatWeaponUtil.getWeaponXpValuereturnsdouble.DISABLED_VALUEis now adoubleconstant (-1.0). Java auto-widenslong→doubleat callsites, so source compatibility is retained for most arithmetic, but binary compatibility is not - recompile against this version.
XpMapsConfigandMobKillXpConfigchanged fromMaptoMapfor XP values (includingDISABLED_VALUEsentinel). NoSCHEMA_VERSIONbump - Gson deserializes existing integer JSON intoDoubletransparentlySkillService.addXpByNamesignature changed fromlong amounttodouble amount. The existing fractional-accumulator logic (exactXp = amount * totalMultiplier) is unchanged; fractional XP naturally accumulates via the per-skill remainder tracker rather than being truncated at the callsiteSkillDefaults.getDefaults(skillId)now returnsMap. The 28 per-skill xpdefaults source files underconfig/xpdefaults/remainMap- promotion happens at the aggregator boundary to avoid mass-editing every default file.MobKillXpDefaultskept asMap;MobKillXpConfigconverts at the boundary via adefaultsAsDouble()helperTriggerResult.xpAwardedtype widened todouble.CombatWeaponUtil.getWeaponXpValuenow returnsdouble. Event systems (MobKillEventSystem,CombatXpEventSystem) and integration adapters (HyFishingAdapter,SimpleEnchantmentsAdapter) updated to carrydoublethrough their XP pipelines- UI pages
XpOverridesPage,MobKillXpPage, andCustomSkillWizardPageswitched toDouble.parseDoubleandMap. Both pages gained aformatXphelper that renders integer-valued doubles without a trailing.0so existing entries look unchanged
Skill Tree Rewards
- Combo finisher damage bonus - New
COMBO_DAMAGEreward type grants a % damage bonus on the final hit of a combo chain. Every melee weapon skill (Swords, Daggers, Polearms, Staves, Axes, Blunt, Unarmed) ships with four combo damage reward options built into its default tree, unlocking at levels 15, 30, 50, and 80. Stacking every tier grants +89% bonus damage on combo finisher hits. Because combo damage only procs on every 2nd hit (Axes), 3rd hit (Swords), or 4th hit (other melee), tier values are higher than flat damage rewards at the same milestone: 10% / 18% / 26% / 35%. Per-skill targeting also means server owners can author additional combo rewards on any combat skill's tree with independent bonuses - Per-skill combo pacing baked into defaults - Axes finish faster than everything else so heavier weapons feel punchy: Axes finish on every 2nd hit, Swords on every 3rd, and every other combat skill (Daggers, Polearms, Staves, Blunt, Archery, Unarmed, custom skills) finishes on every 4th. Combo window is 3 seconds across the board. Server owners can override any of these per-skill via
skillComboWindowMs/skillComboFinisherHitCountmaps inskill-tree.json - Combo % on the Skill Overview page - The horizontal buffs bar on
/getxpnow displays your current combo bonus for the equipped weapon's skill alongside DMG, CRIT, STEAL, BLK, and FALL - so you can see at a glance how much extra damage your next combo finisher will land - Combo finisher notification - A pink "COMBO +N" notification now pops when a combo finisher triggers, showing the exact bonus damage added. Previously the bonus was applied silently, which made the mechanic easy to miss
Player Settings
- Fine-grained combat notification toggles - The Settings page now has five individual toggles for each combat notification type: critical hits, lifesteal, defense blocks, fall damage reduction, and combo damage. The existing Combat Effect Notifications toggle still works as a master override - when off, every combat notification is suppressed. Players who like seeing crits but find lifesteal spam noisy can now silence just that channel
Technical: Combo Finisher Detection
- New
COMBO_DAMAGEreward type inRewardTypeenum with display name "Combo Damage %" for localization ComboStateServicesingleton tracks (attacker, target, hitCount, lastHitTime, skillId) per player and returns true when hitCount % finisherHitCount == 0. Auto-resets on target-swap, time-window timeout, skill-swap, or player disconnect- Defaults live in
SkillTreeDefaults.getComboFinisherHitCountDefault()/getComboWindowMsDefault(): AXES=2, SWORDS=3, all others=4; window 3000ms for all skills. No global combo field lives inSkillConfig- everything is resolved per-skill throughSkillTreeConfig SkillTreeConfigadds per-skill override mapsskillComboWindowMsandskillComboFinisherHitCount, plus resolver methodsgetComboWindowMsForSkill(skillId)/getComboFinisherHitCountForSkill(skillId)that prefer the override and fall back toSkillTreeDefaults.ComboStateServicereads through these resolvers on every hitCombatDamageEventSystem.applyAttackerEffects()signature expanded to includeRef. Finisher detection callstargetRef ComboStateService.recordHitAndCheckFinisher(attackerId, targetId, skillId)using hashCode-based synthetic UUID for target identity. On finisher:modifiedDamage *= (1 + SkillTreeService.getComboFinisherBonusForSkill(skills, skillId))SkillTreeService.getComboFinisherBonusForSkill(SkillComponent, String skillId)filters and sums COMBO_DAMAGE rewards by skill ID, mirroringgetDamageBonusForSkillRewardEffectRegistryregistersComboFinisherEffect(extendsPassiveEffect, no-op on claim/remove)- Player-disconnect cleanup via
MMOSkillTreePlugin.PlayerDisconnectEventhandler callsComboStateService.getInstance().clearPlayer(playerId)alongside notification throttle cleanup. Per-player combo state is ephemeral and not persisted - All 9 language defaults (English through Russian) include
reward.combo_damagedisplay names and descriptions (e.g. "Deal {0}% extra damage on combo finisher hits") - 28 new
SkillRewardentries added toSkillTreeDefaults.java- 4 per skill (T2/T4/T6/T8 at levels 15/30/50/80) × 7 melee skills (SWORDS, DAGGERS, POLEARMS, STAVES, AXES, BLUNT, UNARMED). Reward IDs, values 0.10 / 0.18 / 0.26 / 0.35, all targeting the skill's_combo_1..4 CombatTarget. NoSCHEMA_VERSIONbump - override-basedSkillTreeConfigauto-merges new defaults, preserving existingskill-tree.jsonoverrides. Non-melee skills (ARCHERY, MAGIC, ARTILLERY, DEFENSE) intentionally omitted - combo finisher pacing is defined only for melee weapons
Technical: Combo UX and Notification Toggles
ViewXpPage.populateBuffsBar()now also queriesSkillTreeService.getComboFinisherBonusForSkill(skills, equippedSkill.getSkillId())when the equipped weapon is combat-classified, and writes the result into the new#BuffCombolabel (color#ff7fcf). The label is fixed-width (95px) in the horizontal buffs bar alongside the existing six stat labelsCombatDamageEventSystem.applyAttackerEffects()combo block now captures the bonus delta before applying it (bonusDamage = modifiedDamage * comboBonus; modifiedDamage += bonusDamage) so the displayed notification value is the exact increment added, not the post-multiply total. NewnotifyComboFinisher()method mirrors the existingnotifyLifesteal/notifyCriticalHitpattern, throttled via newNotificationThrottle.COMBO_DAMAGEconstant and localized vianotify.combo_damage- Five new per-type toggle fields added to
SkillComponent:showCritNotifications,showLifestealNotifications,showDefenseNotifications,showFallNotifications,showComboNotifications(all defaulttrue). Each has a paired getter/setter,KeyedCodecentry (keysShowCritNotificationsetc.), constructor default, andclone()copy. Additive-only codec change - existing player saves without these keys decode to the defaults, so no migration needed CombatDamageEventSystemnotification gates now combine the mastergetShowCombatEffects()with each type's individual toggle. Master off still suppresses every combat notification; master on plus type toggle off suppresses only that channelSettingsPageadds five new@SettingCardsub-cards under the master Combat Effects card (indented 30px via Padding override to visually convey the hierarchy). Each has a toggle button bound to one of the new actions (toggleCrit,toggleLifesteal,toggleDefense,toggleFall,toggleCombo) handled inhandleDataEvent()- All 9 language defaults receive 12 new keys:
ui.viewxp.buff_combo_format,notify.combo_damage, plusui.settings.{crit,lifesteal,defense,fall,combo}_notifand their_descvariants. The existingui.settings.combat_effects_desctext updated to describe the master-toggle-over-sub-toggles relationship
v0.14.4 - April 2026
Skill Tree Rewards
- Combo finisher damage bonus - New
COMBO_DAMAGEreward type grants a % damage bonus on the final hit of a combo chain. Server owners can author these rewards on any combat skill's tree to give high-level players a tangible mechanical payoff for maintaining hit chains. Per-skill targeting means Swords Combo and Archery Combo can have independent bonuses - Per-skill combo pacing baked into defaults - Finisher hit counts differ by combat skill so each weapon class feels distinct: Swords finish on every 3rd hit, Axes on every 2nd, and everything else (Archery, Unarmed, custom skills) on every 4th. Combo window is 2 seconds across the board. Server owners can override any of these per-skill via
skillComboWindowMs/skillComboFinisherHitCountmaps inskill-tree.json
Technical: Combo Finisher Detection
- New
COMBO_DAMAGEreward type inRewardTypeenum with display name "Combo Damage %" for localization ComboStateServicesingleton tracks (attacker, target, hitCount, lastHitTime, skillId) per player and returns true when hitCount % finisherHitCount == 0. Auto-resets on target-swap, time-window timeout, skill-swap, or player disconnect- Defaults live in
SkillTreeDefaults.getComboFinisherHitCountDefault()/getComboWindowMsDefault(): SWORDS=3, AXES=2, all others=4; window 2000ms for all skills. No global combo field lives inSkillConfig— everything is resolved per-skill throughSkillTreeConfig SkillTreeConfigadds per-skill override mapsskillComboWindowMsandskillComboFinisherHitCount, plus resolver methodsgetComboWindowMsForSkill(skillId)/getComboFinisherHitCountForSkill(skillId)that prefer the override and fall back toSkillTreeDefaults.ComboStateServicereads through these resolvers on every hitCombatDamageEventSystem.applyAttackerEffects()signature expanded to includeRef. Finisher detection callstargetRef ComboStateService.recordHitAndCheckFinisher(attackerId, targetId, skillId)using hashCode-based synthetic UUID for target identity. On finisher:modifiedDamage *= (1 + SkillTreeService.getComboFinisherBonusForSkill(skills, skillId))SkillTreeService.getComboFinisherBonusForSkill(SkillComponent, String skillId)filters and sums COMBO_DAMAGE rewards by skill ID, mirroringgetDamageBonusForSkillRewardEffectRegistryregistersComboFinisherEffect(extendsPassiveEffect, no-op on claim/remove)- Player-disconnect cleanup via
MMOSkillTreePlugin.PlayerDisconnectEventhandler callsComboStateService.getInstance().clearPlayer(playerId)alongside notification throttle cleanup. Per-player combo state is ephemeral and not persisted - All 9 language defaults (English through Russian) include
reward.combo_damagedisplay names and descriptions (e.g. "Deal {0}% extra damage on combo finisher hits")
v0.14.3 - April 2026
Permissions
- Player-specific level caps via permission node - Server owners can now restrict an individual player's max level per skill (or globally) by granting a permission node. Useful for tiered access (free vs VIP, trial accounts capped at low levels). Permissions can only lower the cap configured in custom-skills.json — they never raise it. Per-skill nodes override the
allwildcard so admins can carve exceptions out of a global restriction. Cap-locked players still gain XP up to their cap, then silently stop, exactly like hitting the configured max /mmoconfig resetpermcache- New admin subcommand to clear the cached permission lookups after granting or revoking nodes in your permissions plugin
Bug Fixes
- Fixed elite mobs becoming invulnerable after taking multiple hits - Elite mobs were spawning with a health boost modifier, then immediately calling
stats.maximizeStatValue()in thehealToFull()method. This corrupted the entity's damage state, preventing the health decrease calculation from applying correctly on subsequent hits. The fix is to remove the heal-to-full behavior entirely and let the health modifier work normally. Elites now take damage as expected and die when their HP reaches zero
Technical: Level Cap Permission
- New
mmoskilltree.levelcap.and. mmoskilltree.levelcap.all.permission nodes (skill ID lowercase, level integer). Honored alongside the legacyziggfreed.prefix PermissionUtil.getEffectiveLevelCap(Player, String username, String skillId, int defaultCap)resolves the cap: per-skill node first, thenall, falling back todefaultCap. Returnsmin(permissionCap, defaultCap)so an over-permissive node never raises the configured ceiling- Resolution scans
1..defaultMaxcallingplayer.hasPermission(node)per level (Hytale's permission API has no enumeration), caches the result per username + scope key in aConcurrentHashMap. Cache cleared onPlayerDisconnectEventand via/mmoconfig resetpermcache SkillService.addXpByNamenow pullsPlayerfrom the store and callsgetEffectiveLevelCap(player, playerRef.getUsername(), skillId, configMax)instead of using the config cap directly. Silent-clamp branch is unchanged- No
CONFIG_VERSIONbump — feature is permission-driven only, no new config fields
Technical: Elite Aura and Invulnerability Fix
- Aura visuals: Elite mobs now display a persistent cosmetic
EntityEffectapplied viaEffectControllerComponent.addInfiniteEffect()inEliteMobsService.tryMakeElite(). Effect IDs are resolved per-tier fromEliteMobsConfig.tierEffects(defaults:MMO_Elite_Rare/MMO_Elite_Epic/MMO_Elite_Legendary). Three EntityEffect assets ship in the plugin jar underServer/Entity/Effects/MMO/, each using a vanilla Hytale particle system (Drop_Rare,Drop_Epic,Drop_Legendary) plus anEntityBottomTintcolor. Aura has no stat modifiers, damage calculator, or invulnerability flag - Invulnerability fix: Root cause was
healToFull()callingstats.maximizeStatValue()on spawn, which corrupts damage calculations for the entity's lifetime. Fixed by removing the method entirely. Also removedhealToFullOnEliteRollconfig field/UI/localization - Component simplification: Merged
EliteSpawnProcessedComponentintoEliteMobComponentusing aNONE_TIERsentinel. Reduces archetype mutations from 2-per-elite to 1 during spawn, lowering ECS iteration hazard - Death cleanup: Nameplate is never applied, so
MobKillEventSystemcleanup remains simple. Hytale'sDeathSystems$ClearEntityEffectshandles aura removal automatically on death
v0.14.2 - April 2026
Quest System
- Quest completion can now reset other quests - A quest can declare a
resetsOnCompletelist of other quest IDs. When the triggering quest is claimed, each target quest is fully reset (state, progress, cooldown) and becomes re-acceptable immediately. Useful for "weekly master" style quests that reset a group of dailies, or any quest chain where finishing one should re-open earlier content. Resets fire after rewards execute. For auto-claim quests they fire on completion; for manual-claim quests they fire when the player actually claims. Parked COMPLETED_UNCLAIMED quests do not reset their targets until claimed. Chains do not cascade — targets are reset, not re-completed
Bug Fixes
- Fixed
/mmoboost clearand/mmoboost statusfailing for player names containing underscores - Player names likejohny_boywere being split on the underscore, so the command looked up "johny" instead of the full name and reported "Player not found". The clear and status subcommands now treat the args value as a single player name and preserve underscores. Multi-arg subcommands (give,global) are unaffected and continue to accept|or_as delimiters - Fixed Command Rewards admin page overflowing when a skill has many level tiers - The level tier selector on the Command Rewards admin page laid out all tier buttons on a single non-wrapping row, so skills with many configured tiers pushed buttons off-screen past the Add Tier input. Tier buttons now wrap to multiple rows and the input/Add Tier controls sit on their own row above
- Fixed elite mob death cleanup throwing console errors and leaving nameplates on corpses - When an elite mob died, the plugin tried to clean up its elite data and nameplate at the wrong point in the tick cycle, logging
Elite death cleanup error: Store is currently processing!and aborting the cleanup. The result was lingering elite nameplates floating on corpses and persistent elite data on dead entities. Cleanup is now deferred to a safe point so elite corpses reset properly and the console spam is gone - Fixed leveling formula reverting to Hybrid after server restart - Switching the leveling formula to Legacy in the Admin Config page updated it live, but the choice was never written to disk. On restart the formula fell back to the default (Hybrid), which made high-level XP requirements balloon unexpectedly. The formula selection now persists across restarts
Plugin API
- Listen for XP gain from another mod - Other mods can now register a listener that fires every time a player gains XP in any skill. The event includes the player, skill ID, base and final XP amounts (so you can see how much the skill tree bonus and active boosts contributed), before/after XP and level values, and a level-up flag. Useful for analytics mods, custom notification overlays, achievement systems, or any cross-mod reward chain that wants to react to progression
Technical: Quest Resets
- New
resetsOnComplete: Listfield onQuestDefinition(quest JSON top-level array of quest IDs). Parsed inQuestConfig.parseQuest(). Post-loadvalidateResetsOnComplete()logs warnings for unknown quest IDs and self-references; runtime filters both defensively QuestService.applyResetsOnComplete()fires fromclaimQuest()and from the auto-claim branch ofcheckQuestCompletion()— after rewards execute, before the completion log line. CallsQuestComponent.resetQuest(targetId)for each valid target. Single INFO log line per triggering quest listing all reset targets
Technical: PlayerGainXpEvent
- New
com.ziggfreed.mmoskilltree.api.events.PlayerGainXpEvent(in themmoskilltree-apimodule, bundled into the plugin jar). Plain POJO carryingplayerRef,skillId,baseAmount,finalAmount,xpBefore,xpAfter,levelBefore,levelAfter, plusisLevelUp() - New listener registry on
MMOSkillTreeAPI:addXpListener(Consumer/) removeXpListener(...). InternalfireXpEvent(PlayerGainXpEvent)called fromSkillService.addXpByNameat the end of the award path and on the first-time-XP branch, after XP commit and milestone/level-up side effects. Backed byCopyOnWriteArrayList; per-listener exceptions are caught and logged so one misbehaving subscriber cannot break the XP pipeline. Listeners run synchronously on the world tick thread — safe to touch Store/Ref withoutworld.execute - Not routed through Hytale's
EventBus—PluginBaseexposes onlygetEventRegistry()(subscribe-only), so custom events published between plugins currently have no first-class Hytale path. The listener registry is the cross-plugin contract - Subscriber snippet:
`java
import com.ziggfreed.mmoskilltree.api.MMOSkillTreeAPI;
MMOSkillTreeAPI.addXpListener(event -> {
if (event.isLevelUp()) {
// react to level-up in event.getSkillId() for event.getPlayerRef()
}
});
`
- The api module now has a
compileOnlydependency onHytaleServer.jar(forPlayerRef). Nothing is bundled — consumers already have Hytale on their classpath
Technical: Elite Death Cleanup
MobKillEventSystem.cleanupEliteComponentswas callingstore.removeComponent(...)directly from inside anOnDeathSystem, which Hytale's ECS forbids — structural mutations on the live store mid-system raiseStore is currently processing!. The three removals (EliteMobComponent.TYPE,EliteSpawnProcessedComponent.TYPE,Nameplate.getComponentType()) now go throughcommandBuffer.removeComponent(...)so they're deferred until after the current system pass. Thestore.getComponent(...)existence check stays on the store (reads are allowed)
Technical: Leveling Formula Persistence
SkillConfig.ConfigDatanever declareduseHybridFormula, so Gson wroteskill-config.jsonwithout the field andload()had nothing to rehydrate.setUseHybridFormula(false)updated the in-memory flag and calledsave(), but the next restart constructed a freshSkillConfigwith the defaultuseHybridFormula = true. Added the field toConfigData(defaulting totrueso existing configs keep current behavior on first load), wired it into bothsave()andload(). NoCONFIG_VERSIONbump — the change is additive and legacy files stay readable
v0.14.1 - March 2026
Default Quest Updates
- New "Monster Slayer" daily quest - Slay 30 hostile mobs for +3,500 Acrobatics XP and +3,500 Defense XP. Repeatable every 24 hours with auto-accept
Bug Fixes
- Fixed quest XP rewards not being awarded - Quest rewards that grant skill XP (e.g., daily quest bonus XP) were silently failing because the internal command used the wrong format and couldn't run from console. XP rewards now use a dedicated
/mmoawardxpcommand that works correctly from both console and quest reward systems - Fixed placed block tracking resetting on server restart - The anti-exploit system that prevents XP from breaking player-placed blocks was purely in-memory and lost all data on server restart. Placed block positions are now saved to disk on shutdown and restored on startup, so players can't exploit restarts to re-mine their own blocks for XP
- Placed block tracking is now per-world - Block positions now include the world UUID, so a block placed at coordinates (10, 64, 20) in one world won't incorrectly block XP for the same coordinates in a different world
Technical Details
- New
AwardXpCommand(/mmoawardxp) extendsAbstractAsyncWorldCommandfor console-compatible XP awards viaSkillService.addXpByName QuestRewardExecutor.executeXpReward()now dispatches/mmoawardxpinstead of/setmmoxp(which is player-only and had mismatched argument order)PlacedBlockTrackernow persists block data tomods/mmoskilltree/placed-blocks.jsonvia Gson on shutdown, with load on startup. Expired blocks are pruned on both load and save. Placed items are not persisted (short expiry)BlockPositionrecord now includesworldUuidfield. AlltrackPlacement/wasPlacedByPlayer/removeTrackingcalls passplayerRef.getWorldUuid()
v0.14.0 - March 2026
Mob Drop Commands
- Command-based mob drops - Configure commands that execute when specific mobs are killed. Each mob pattern supports multiple independent drop pools with their own chance, delay, and notification. Perfect for custom loot tables, currency rewards, particle effects, or any server-side action triggered by mob kills
- Drop pools - Each mob pattern supports multiple drop pools that roll independently on kill. Configure different drops at different rarities — e.g., a common 20% bone drop alongside a rare 5% legendary weapon drop, all under the same "Skeleton" pattern
- Elite mob tier filtering - Each drop pool can require a specific elite mob tier (Rare, Elite, Legendary, or custom tiers). Normal mobs only trigger pools with no tier requirement, while elite mobs trigger both tier-specific and any-tier pools. Combine with the elite mobs system for rarity-scaled loot tables
- Configurable drop notifications - Each pool can show a notification to the player with customizable color, icon, and localized primary/secondary text. Notifications use the player's language setting
- Multiple commands with delay - Each pool supports multiple commands executed sequentially with a configurable delay (in ticks) between them. Single-command pools execute immediately with no delay overhead
- Placeholders - Commands support
{player}(killer's username),{mob}(mob type, e.g., "Skeleton_Fighter"), and{tier}(elite tier name, e.g., "Rare") placeholders - Admin Dashboard page - New "Mob Drop Cmds" page accessible from the Admin Dashboard. Flat list of all pools across all patterns with tier, chance, delay, and notification status. Add pools, remove individual pools, disable/enable entire patterns
- Built-in defaults - Ships with starter drops for each elite tier (5% base, 25% Rare, 50% Elite, 100% Legendary) awarding essences in increasing amounts. Customize or disable from the Admin Dashboard or config file
- Override-based config - Uses the same override system as other configs. Only your customizations are saved in
mods/mmoskilltree/mob-drop-commands.json. Longest substring pattern matching (same as mob kill XP). Reference file at_reference/defaults-mob-drop-commands.json
Elite Mob Tiers Admin Page
- Elite Tiers configuration page - New dedicated admin page for managing elite mob tiers, accessible from the Admin Dashboard's Elite Mobs section via the "Elite Tiers" button
- Tier CRUD - Add, edit, and remove elite tiers through the UI. Each tier has a name, selection weight, damage taken multiplier, damage dealt multiplier, and health multiplier. Editing an existing tier name updates it in place
- Settings panel - Configure roll chance, NPC-only restriction, player involvement requirement, heal-on-roll behavior, and debug messages — all with instant toggle feedback
- Reset to defaults - One-click reset reverts all tier customizations to built-in defaults (Rare/Elite/Legendary)
Technical Details
MobDropEntrydata class holdsList+pools boolean disabled. EachMobDropPoolhas commands, delay, chance, requiredTier, andMobDropNotification(enabled, color, icon, primaryKey, secondaryKey)MobDropCommandsDefaultsprovides a fallback""pattern with 4 pools: base (5% Life Essence), Rare (25%), Elite (50% Fire+Life), Legendary (100% Void+Fire+Life). Overrides merge with defaults (pools concatenated), not replaceMobDropCommandsConfigextendsAbstractOverrideConfig— schema version 1, loads defaults fromMobDropCommandsDefaults,ConcurrentHashMapstorage, longest substring match viagetDropForMob().rebuildEffectiveMap()merges default pools + override pools. Pool management viaaddPool()andremovePool()MobKillEventSystem.onComponentAdded(): readsEliteMobComponent.tierNameBEFOREcleanupEliteComponents()removes it (fixes existing bug where quest system couldn't see elite qualifier). PasseseliteTierto bothhandleMobDropCommands()andQuestService.onEntityKill()handleMobDropCommands(): iterates all pools, checksrequiredTiermatch (empty = any, non-empty = must matcheliteTiercase-insensitively), rolls chance per pool, sends notification per pool, dispatches commands per poolsendDropNotification(): falls back tonotify.mob_drop/notify.mob_drop.hintlocalization keys when notification params are emptyexecutePoolCommands(): single command = immediate execution; multiple commands = daemon thread withThread.sleep(delay * 50L)between commands (same pattern asQuestRewardExecutor)CommandExecutor.processMobDropPlaceholders(): replaces{player},{mob}, and{tier}placeholdersMobDropCommandsPageuses$C.@Container/@SectionCard/@SettingRowpattern. 12-field codec with flat pool list +MobDropRow.uirow template showing Default/Custom status per pool- New
EliteMobTiersPageextendsInteractiveCustomUIPagewith 8-field codec. Uses$C.@Container/@SectionCard/@SettingRowpattern.EliteTierRow.uirow template. Toggle settings usesendUpdate()for instant UI feedback AdminConfigPage: new "Mob Drop Cmds" button + "Elite Tiers" button in Elite Mobs sectionMMOSkillTreePlugin.setup(): loadsMobDropCommandsConfigfrommods/mmoskilltree/mob-drop-commands.jsonMMOConfigCommand.handleReload(): includesMobDropCommandsConfig.getInstance().load(false)in config reload- Config version 11 (skill-config.json unchanged), new override config
mob-drop-commands.jsonat schema version 1 - 34 new localization keys added to all 9 language files
v0.13.4 - March 2026
Performance
- Notification batching for combat, XP, and luck - Combat effect notifications (critical hits, defense blocks, lifesteal heals, fall damage reduction), XP gain notifications, and luck bonus notifications are now batched per player. During rapid combat or gathering, values accumulate over a configurable window and display as a combined total (e.g., "-12.5 blocked" instead of five separate "-2.5 blocked" notifications, or "Lucky! (+5)" instead of five separate luck popups). Reduces client-side lag during intense fights and gathering sessions while preserving accurate feedback. Critical hits show a hit count when multiple crits occur in the same window (e.g., "Critical Hit! x1.5 x3")
- Configurable notification batch window - The notification batch window (default 1000ms) is now configurable via the Admin Dashboard under General Settings. Adjustable from 100ms to 5000ms to tune notification frequency for your server's needs
Default Quest Updates
- New "Essence Run" daily quest - Turn in 100 Life Essence for +500 XP in Mining, Woodcutting, Fishing, Unarmed, and Defense. Repeatable every 24 hours with auto-accept
- Fixed Outlander Chief reference - The "Champion of Orbis" boss objective now targets
Outlander_Marauderinstead of the non-existentOutlander_Chief
Technical Details
NotificationThrottleutility class added — per-player, per-notification-type accumulator with config-driven batch window (SkillConfig.notificationBatchMs, default 1000ms, range 100-5000ms). Values accumulate silently during the window and flush as a combined total when it elapses. First notification in each window sends immediatelyCombatDamageEventSystem: all four notification methods (notifyCriticalHit,notifyDefenseBlock,notifyLifesteal,notifyFallReduction) now useNotificationThrottle.accumulate()to batch values. Defense/lifesteal/fall sum damage amounts; crits count occurrencesSkillService.addXpByName(): XP gain notification batches accumulated XP viaNotificationThrottle.accumulate()(alongside existingshouldShowXpGainthreshold check)LuckUtil.notifyLuckBonusByName(): luck bonus notifications now batch viaNotificationThrottle.accumulate()withLUCK_BONUStype, accumulating item counts during the batch windowMMOSkillTreePlugin:PlayerDisconnectEventhandler callsNotificationThrottle.removePlayer()to clean up accumulator stateSkillConfig: newnotificationBatchMsfield (default 1000, clamped 100-5000) with getter/setter, ConfigData serialization, and reloadDefaults supportAdminConfigPage: new "Notification Batch Window (ms)" setting row with text field and save button in General Settings sectionQuestDefaults:champion_of_orbisboss target changed fromOutlander_ChieftoOutlander_MarauderQuestDefaults: newdaily_essence_runquest — TURN_IN objective with EXACT match onIngredient_Life_Essence, 100 required, 24h cooldown, rewards +500 XP each in Mining, Woodcutting, Fishing, Unarmed, and Defense
v0.13.3 - NPC Quests & Turn In
NPC Quest System
- NPC-bound quests - Quests can now be tied to specific NPCs via the
npcViewIdfield. NPC quests are hidden from the main quest log until accepted — players discover them by interacting with NPCs - NPC Quest UI - A dedicated two-panel quest page for NPC interactions. The left panel lists available quests with status indicators, the right panel shows quest details including dialogue text, objectives, rewards, and action buttons (Accept, Turn In, Claim, Abandon)
- NPC accept gating - Quests with
npcRequiredToAcceptcan only be accepted through the NPC quest page, not from the main quest log. Once accepted, NPC quests appear in the normal quest log for tracking - Quest dialogue text - Three new markdown fields on quests (
incompleteMarkdown,activeMarkdown,completeMarkdown) display contextual NPC dialogue based on quest state. Per-objective dialogue is also supported viadetailMarkdownon objectives. Text supports#headers,##subheaders,bold, and*italic*formatting /mmoquestuicommand - Admin/console command to open the NPC quest page for any player. Usage:/mmoquestui
Turn In Objective Type
- TURN_IN objectives - New quest objective type that removes items from the player's inventory. Players click the "Turn In" button in the quest UI to submit items. Supports partial turn-in — if you have 7 of 10 required items, you can turn in what you have and come back for the rest
- Turn In button in quest log - When the current objective is a TURN_IN type, a "Turn In" button appears alongside the Abandon button in the main quest page
- Item matching - TURN_IN objectives use the same match modes as other objectives (EXACT, CONTAINS, PREFIX) for flexible item ID matching
Technical Details
ObjectiveType.TURN_INadded — not event-driven, so skipped inQuestConfig.buildIndex()inverted indexQuestDefinition: new fieldsnpcViewId(String),npcDisplayName(String),npcRequiredToAccept(boolean),incompleteMarkdown/activeMarkdown/completeMarkdown(String). All nullable/optional for backward compatibilityQuestObjective: newdetailMarkdownfield with 9-param constructor overload; existing constructors chain with nullQuestConfig: parses new fields,getQuestsByNpcId(String)returns sorted enabled quests filtered by NPC IDQuestDefaults.questToJson(): serializes new NPC, markdown, and detailMarkdown fields (only when non-null/non-default)QuestService.attemptTurnIn(): binary search oncanRemoveItemStacks()to find max removable amount for partial turn-in, thenremoveItemStack()to consume items. Returns amount turned in (0 = insufficient)QuestService.isQuestVisible(): NPC-gated quests (non-nullnpcViewId) hidden from QuestPage when NOT_STARTEDQuestService.isQuestVisibleForNpc(): variant without NPC-gate filter for NpcQuestPage, still respects hidden/prereq/level checksQuestService.canAcceptQuest(): new 5-param overload withboolean fromNpc; blocks non-NPC acceptance whennpcRequiredToAcceptis trueNpcQuestPageextendsInteractiveCustomUIPage— two-panel layout, handles selectQuest/accept/turnIn/claim/abandon/close actions, resolves markdown content by quest stateNpcQuestUICommandextendsAbstractAsyncCommand— admin/console command, usesPlayerLookupUtil.findByUsername()+world.execute()to open NpcQuestPageQuestMarkdownRenderer.render(): splits markdown by double-newline into blocks, appends Label elements viaappendInline()with appropriate font sizes and styles for headers, subheaders, and body text. Stripsboldand*italic*markers (Hytale labels don't support mixed inline styles)QuestPage: Turn In button inpopulateQuestRow()ACTIVE case,findActiveTurnInObjective()helper,turnInaction handler with notification messages,objectiveIdfield added toQuestEventDatacodec- New
NpcQuestPage.uiandNpcQuestRow.uiUI templates;QuestRow.uiupdated with#TurnInBtnand#TurnInSpacer - 6 new localization keys added to all 9 language files:
ui.npc_quest.title,ui.npc_quest.no_quests,ui.npc_quest.quest_count,ui.quests.btn_turn_in,ui.npc_quest.turn_in_fail,notify.quest_turn_in
v0.13.2 - February 2026
SimpleEnchantments Integration
- Enchanting XP from enchanting items - When SimpleEnchantments is installed, enchanting an item awards ENCHANTING skill XP automatically. XP scales with enchantment level — higher-level enchantments give more XP
- Configurable XP values - Enchanting XP values are looked up from the ENCHANTING xp-map config, matching against enchantment names. Falls back to 20 XP per enchantment level if no config match
- Enchanting skill tree - ENCHANTING now has a full 10-tier skill tree with mana-focused rewards. Enchanters unlock large mana pools alongside health, defense, and health regen bonuses
- Auto-detected integration - SimpleEnchantments is detected and connected at startup with no configuration needed. Uses the same adapter framework as HyFishing, HyCitizens, and Perfect Parries
Direct Page Commands
/mmoquestscommand - Opens the Quest page directly. Aliases:/mmoquestlog,/mmoql. No more navigating through multiple UI pages to check your quests/mmoleaderboardcommand - Opens the Leaderboard page directly. Aliases:/mmolb,/mmorankings. Quick access to skill rankings without going through the skill overview/mmoboostscommand - Opens the Boosts page directly. Alias:/mmoxpboosts. Check active boosts and activate tokens without navigating through the skill overview
Permission Prerequisites
- Permissions in prerequisite groups - Quest prerequisites can now include permission checks alongside quest completions in AND/OR logic. Add a
"permissions"array to any prerequisite group to require players to hold specific permissions before accepting the quest. Works at any nesting level, so you can combine quest completions and permissions freely (e.g., complete quest_a AND have rank.vip, or complete quest_b OR have server.bypass) - Quest builder updated - The docs site quest builder now supports adding permissions to prerequisite groups with a dedicated input field and removable chips
Performance
- Fixed TPS drop with many active XP boosts - The XP multiplier calculation (called on every XP award) no longer deserializes personal boosts twice or runs global boost cleanup on every call. Servers with many active boosts should see noticeably smoother tick rates
Localization
- Skill tree reward cards fully localized - Reward display names and descriptions on skill tree cards now use the player's language instead of always showing English. Both the title (e.g., "+3% Mining XP") and the description (e.g., "Gain 3% more XP from Mining") are generated from localization templates. Custom display names and descriptions set by server admins in overrides are preserved as-is
- Item reward descriptions localizable - Command reward entries now support a
descriptionKeyfield for localized descriptions. The item rewards page resolvesdescriptionKeyvia the player's language first, falling back to the literaldescriptionfield - Empty skill tree tiers hidden - Skill tree tiers with no reward choices are now skipped in the UI instead of showing empty rows. The item rewards page already handled this
Bug Fixes
- Fixed fish awarding combat and kill XP when HyFishing is installed - Fish entities with model IDs not in the hardcoded fish pattern list (e.g., custom fish from HyFishing) were treated as normal combat mobs, awarding weapon XP on hit and kill XP on death. Fish detection now also checks FISHING xp-map config keys, so any fish type configured for fishing XP is correctly recognized and blocked from combat XP
- Fixed elite mob nameplates and components persisting after death - Elite mob nameplate labels, elite data, and the spawn-processed marker are now removed when the mob dies, preventing floating nameplates on corpses
- Added target dummy compatibility - Training dummies from mods like Better Training Dummy no longer award combat XP. The Mannequin entity is now in the default mob kill XP list with 0 XP, so players can practice combat without inflating their skill levels. More broadly, any mob configured with 0 XP in
mob-kill-xp.jsonnow blocks both per-hit and kill XP - Fixed Settings page conflict with other mods - Renamed the Settings page UI file to avoid conflicts with other mods that also register a
SettingsPage.ui
Technical Details
MobKillXpDefaults: AddedMannequin→ 0 XP. Mannequin is the modelAssetId used by Hytale's NPC mannequin entity, which training dummy mods (e.g., Better Training Dummy) use as their base entityCombatXpEventSystem: Mobs with explicit 0 XP in mob-kill-xp config now skip all per-hit combat XP (damage-based, per-hit, and questDEAL_DAMAGEtracking). Previously only kill XP was blocked byhandleKillXp()'smobBaseXp <= 0checkFishMobPatterns.isFishMob()now checks FISHING xp-map config keys (bidirectional substring match) as a fallback after the hardcoded pattern set. BothCombatXpEventSystemandMobKillEventSystemcall this method, so the fix propagates to both combat hit XP and kill XP blockingXpBoostService.getTotalMultiplierByName()optimized: early exit when no boosts active, throttled global cleanup (every 5s instead of every call), single-pass personal boost processing (deserialize once → remove expired → accumulate multiplier) instead of separatecleanupExpiredPersonalBoosts()+getPersonalBoosts()callsgetActiveBoostsForPlayer()andgetActivePersonalBoostCount()also converted to single-pass patterns to avoid double deserializationMobKillEventSystem.cleanupEliteComponents()callsstore.removeComponent()forEliteMobComponent,EliteSpawnProcessedComponent, andNameplateon non-player death. Runs before XP/quest processing so the elite qualifier is still read firstSkillTreeDefaults.getDefaults()returns empty list for ENCHANTING whenIntegrationDomain.ENCHANTINGhas no active adapter (same gate pattern as HyCitizens quest defaults)- New
QuestPageCommand(/mmoquests, aliasesmmoquestlog,mmoql),LeaderboardCommand(/mmoleaderboard, aliasesmmolb,mmorankings), andBoostPageCommand(/mmoboosts, aliasmmoxpboosts) — simpleAbstractPlayerCommandimplementations that open their respective pages - New
ENCHANTINGintegration domain withEnchantingIntegrationinterface,NoOpEnchantingIntegrationfallback, andSimpleEnchantmentsAdapterreflection-based adapter SimpleEnchantmentsAdapterregisters event bus handlers forItemEnchantedEventandEnchantmentActivatedEvent, awardsENCHANTINGXP on the world thread- XP formula:
lookupEnchantingXp(enchantmentName) * enchantmentLevel, with 20 XP default per level - Extracted shared reflection helpers into
ReflectionUtil—resolvePluginInstance,tryInvoke,tryLoadFirst,findSingleParamMethod,findFirstMethod,invoke,createEventProxynow shared across all adapters HyFishingAdapterandHyCitizensAdapterupdated to useReflectionUtilstatic imports (removed ~270 lines of duplicated reflection code)- Added
getEnchantingDefaults()toSkillTreeDefaults— 10 tiers with mana-focused rewards (up to +30 mana at tier 9), XP bonuses, health, defense, and health regen PrerequisiteGroup— newpermissionsfield (4-arg constructor, backward-compatible 3-arg delegates with empty list). AddedgetPermissions(),getAllPermissions()recursive flattener, updatedisEmpty()to check permissionsQuestService.evaluatePrerequisites()— new overload accepting@Nullable Player. AND mode requires all permissions held, OR mode requires at least one.canAcceptQuest()andisQuestVisible()now pass Player for permission evaluation. Permission-only prereq groups work even without a QuestComponentQuestConfig.parsePrerequisiteGroup()reads"permissions"JSON array;QuestDefaults.prerequisiteGroupToJson()serializes it (omitted when empty)QuestPage.buildPrereqGroupText()renders permission entries usingui.quests.req_permissionlocalization key (added to all 9 languages)- Docs site quest builder:
PrereqGrouptype includespermissions: string[],PrereqGroupEditorhas permission input + removable chips,buildPrereqGroupJson/parsePrereqGrouphandle the field - Added
reward.desc.*localization keys (25 per language) to all 9 language files for reward description templates. Templates use{0}for the formatted value and{1}for skill name (BONUS_XP, LUCK). Combat-targeted rewards appendreward.desc.with_targetsuffix Messages.getRewardDisplayTextByName()refactored: comparesreward.getDisplayName()against English-generated text (case-insensitive) instead of only the enum default. SkillTreeDefaults display names like "+3 Max Health" now correctly match and get localized instead of being treated as custom overrides- New
Messages.getRewardDescription(skills, reward, skillId)generates localized descriptions fromreward.desc.*templates. Falls back to raw description if it doesn't match the English template or enum default (preserving custom admin overrides) - New private helpers:
generateRewardDisplayText(langCode, reward, skillId),generateRewardDesc(langCode, reward, skillId),getRewardFormattedValueForLang(langCode, reward),getDescriptionValue(reward)(absolute value, no prefix/suffix) SkillTreePage.build()now callsMessages.getRewardDescription()instead ofreward.getDescription()for card renderingSkillTreePage.build()skips tiers with empty choices list, trackingdisplayedTierIndexseparately from data tier index. Event data includes bothTier(data) andRow(display) for correct claim handler UI selector mappingCommandRewardEntry: newdescriptionKeyfield withgetDescriptionKey(),hasDescriptionKey()accessors and Builder support. 8-arg constructor chain preserved for backward compatibilityCommandRewardsConfig.RewardData: serializes/deserializesdescriptionKeyalongside existing fieldsItemRewardsPage: newresolveDescription()method (mirrorsresolveDisplayName()) — triesdescriptionKeyviaMessages.get()first, falls back to literaldescription. All three parse methods updatedItemRewardsPage.resolveDisplayName()andresolveDescription()now compare the literal value against the English resolution of the localization key (case-insensitive). If they match, the player's localized version is used instead of the English literal. Custom overrides that differ from English are preserved as-is- Renamed
SettingsPage.ui→MMOSkillTreeSettingsPage.uito avoid UI file name conflicts with other mods. Java reference inSettingsPage.javaupdated
v0.13.1
Elite Mobs Admin Section
- Dedicated Elite Mobs section in Admin Dashboard - Elite mob settings now have their own section in the Admin Dashboard instead of being grouped under General Settings. The section includes the existing enable/disable toggle plus a new nameplate toggle
- Elite Mob Nameplates toggle - New admin toggle to show or hide nameplate labels (e.g. "Rare Trork", "Legendary Bear") above elite mobs. Disabled by default - enable it when you want players to see elite rarity at a glance
HyCitizens NPC Integration
- TALK_TO_NPC quest objectives now work automatically - When HyCitizens is installed, interacting with an NPC triggers TALK_TO_NPC quest objectives. Quest authors can use
"type": "TALK_TO_NPC"with the citizen ID as the target and it will progress automatically - Auto-detected integration - HyCitizens is detected and connected at startup with no configuration needed. Uses the same adapter framework as HyFishing and Perfect Parries
- Default "Daily Rounds" quest - A new daily quest auto-generates when HyCitizens is detected: talk to any NPC for a Defense XP Shard. Uses empty target to match any NPC, with a 24-hour cooldown
Technical Details
- Added
showNameplatesfield toEliteMobsConfig/EliteMobsDefaults(override-based, default: false) EliteMobsService.tryMakeElite()now gatesapplyEliteNameplate()behindconfig.isShowNameplates()- New
#EliteMobsSectioninAdminConfigPage.uiwith#ToggleEliteMobsand#ToggleEliteNameplatestoggle rows - Added localization keys
ui.admin.elite_mobs,ui.admin.elite_mobs_label,ui.admin.elite_nameplates_labelto all 9 languages - New
CITIZENSintegration domain withCitizensIntegrationinterface,NoOpCitizensIntegrationfallback, andHyCitizensAdapterreflection-based adapter HyCitizensAdaptersubscribes toCitizenInteractEventvia dynamic proxy onCitizensManager.addCitizenInteractListener(), dispatches toQuestService.onTalkToNpc()on the world thread- Added
QuestService.onTalkToNpc()- routes NPC interactions toTALK_TO_NPCobjectives (null CommandBuffer, same pattern asonCatchFish) - Removed
TALK_TO_NPC"no event system" warning fromQuestConfigobjective parser QuestDefaults.getDefaults()conditionally addsdaily_npc_greetingquest whenIntegrationDomain.CITIZENSis active;MMOSkillTreePluginregenerates quest defaults after integration resolution to pick it up
v0.13.0 - Elite Hostile Mobs
Elite Hostile Mobs
- Tiered elite mob system - Hostile mobs can now randomly become elite when they spawn into the world. Elite mobs have boosted max health, deal more damage, and take less damage. Three default tiers ship out of the box: Rare (70% chance, 1.5x HP/damage), Elite (25% chance, 2.5x HP, 2x damage), and Legendary (5% chance, 5x HP, 3x damage)
- On-spawn elite rolling - Elite status is determined the moment a mob first appears, not during combat. This prevents archetype changes mid-fight that could cause AI or damage bugs
- Data-driven tier config - All elite tier definitions, roll chance, and behavior are configured in
mods/mmoskilltree/elite-mobs.json. Add custom tiers, adjust multipliers, or change the 10% base roll chance without touching code - Override-based config - Elite mobs config now uses the same override pattern as other configs. Only your customizations are saved; defaults auto-update with mod versions. A reference file at
mods/mmoskilltree/_reference/defaults-elite-mobs.jsonshows all default values - Mob ID filtering - Control which mobs can become elite using allow/deny lists with substring pattern matching. Deny specific mobs (e.g., training dummies), restrict elites to certain mob types, or assign specific tiers to individual mobs via tier overrides
- Projectile and prop safety - Non-living entities (projectiles, props, triggers) can never become elite, preventing accidental stat scaling on arrows and environmental objects
- Admin toggle - Enable or disable the entire elite mob system from the Admin Dashboard under General Settings. Disabled by default — flip it on when you're ready
Technical Details
- New
EliteMobComponentECS component (mmoskilltree:elitemob) withBuilderCodecpersistence fortierName,incomingDamageMult,outgoingDamageMult,maxHealthMult - New
EliteSpawnProcessedComponentmarker component (mmoskilltree:elite_spawn_processed) — ensures each entity is only processed once for elite rolling EliteSpawnRollTickingSystemextendsEntityTickingSystem— runs every tick, skips players (PlayerRef), skips non-living entities (EntityStatMapnull check), marks entities withEliteSpawnProcessedComponent, then callstryMakeElite()onceEliteMobDamageSystemnow handles damage scaling only — elite rolling removed from damage pipeline to avoid archetype changes insideFilterDamageGroupEliteMobsService: idempotenttryMakeElite()with weighted tier selection,StaticModifier(MULTIPLICATIVE)HP modifier keyed"mmoskilltree_elite_hp", optional heal-to-full viamaximizeStatValue(). EarlyEntityStatMapnull guard prevents non-living entities from being rolled eliteEliteMobsConfigconverted from standalone GSON config toAbstractOverrideConfigwith nullableOverrideDatafields,EliteMobsDefaultsstatic defaults class, and auto-generated reference file- Mob ID resolution reuses existing
EntityIdentifierUtil.getMobId()(ModelComponent modelAssetId extraction) - Admin toggle added to
AdminConfigPageUI and Java (General Settings section)
v0.12.3 - February 2026
Elite Hostile Mobs
- Tiered elite mob system - Hostile mobs can now randomly become elite when a player engages them in combat. Elite mobs have boosted max health, deal more damage, and take less damage. Three default tiers ship out of the box: Elite (70% chance, 1.5x HP/damage), Rare (25% chance, 2.5x HP, 2x damage), and Legendary (5% chance, 5x HP, 3x damage)
- Data-driven tier config - All elite tier definitions, roll chance, and behavior are configured in
mods/mmoskilltree/elite-mobs.json. Add custom tiers, adjust multipliers, or change the 10% base roll chance without touching code - Mob ID filtering - Control which mobs can become elite using allow/deny lists with substring pattern matching. Deny specific mobs (e.g., training dummies), restrict elites to certain mob types, or assign specific tiers to individual mobs via tier overrides
- Admin toggle - Enable or disable the entire elite mob system from the Admin Dashboard under General Settings. Disabled by default — flip it on when you're ready
Technical Details
- New
EliteMobComponentECS component (mmoskilltree:elitemob) withBuilderCodecpersistence fortierName,incomingDamageMult,outgoingDamageMult,maxHealthMult EliteMobDamageSystemextendsDamageEventSystemonFilterDamageGroup— rolls elite on first player combat encounter, then scales damage both directionsEliteMobsService: idempotenttryMakeElite()with weighted tier selection,StaticModifier(MULTIPLICATIVE)HP modifier keyed"mmoskilltree_elite_hp", optional heal-to-full viamaximizeStatValue()EliteMobsConfig: standalone GSON config atmods/mmoskilltree/elite-mobs.jsonwithsetEnabled()auto-save- Mob ID resolution reuses existing
EntityIdentifierUtil.getMobId()(ModelComponent modelAssetId extraction) - Player detection via
Player.getComponentType()— only rolls elites when a player is involved in combat - Admin toggle added to
AdminConfigPageUI and Java (General Settings section)
v0.12.2 - February 2026
Custom Skill Wizard
- Command Rewards step replaces Item Requirements in the wizard - Step 3 of the Custom Skill Wizard now lets you configure command rewards (commands that run when the skill levels up) instead of item level requirements. Add rewards with a level trigger, command, and optional display name. Commands support
{player},{level},{skill}, and{totalLevel}placeholders
Quest Expansion
- Branching prerequisites (AND/OR logic) - Quest prerequisites can now use AND/OR grouping for branching storylines. Instead of requiring all prerequisite quests to be completed, you can create branching paths where players only need to complete one of several quests to proceed. Supports nested groups for complex quest chains like "Complete the intro quest AND (choose the warrior path OR the mage path)"
- Mixed sequential and parallel objectives - Quests can now have objectives that must be completed in stages while allowing some objectives within the same stage to be done in any order. Assign an
ordervalue to each objective — objectives with the same order value can be completed in parallel, while higher-order objectives unlock only after all lower-order ones are done. Backward compatible: quests using"sequential": truecontinue to work as before - Structured reward types - Quest rewards now support XP grants, boost tokens, and currency alongside commands. XP rewards grant skill XP directly, boost token rewards activate timed XP multipliers, and currency rewards grant gold. Each reward type has a display name shown in the quest UI. Existing command-based rewards continue to work unchanged
- Quest tags and variables - Quests can now include tags for filtering and metadata variables for custom key-value data. Tags are displayed as chips in the quest UI and can be used for organization. Variables store arbitrary data for future conditional logic and docs site filtering
Localization
- Skill card buff labels and stats bar fully localized - The short buff labels on skill cards (HP, DMG, CRIT, etc.), the stats bar in the side panel (XP, DMG, BLK, CRIT, STEAL, FALL), and the tier progress text are now localized in all 9 languages instead of being hardcoded in English
Config Presets (Premium)
- Quests now included in preset export/import - The config preset system now supports exporting and importing quest files alongside other configs. When exporting, all user quest files from
mods/mmoskilltree/quests/are merged into the preset. When importing, quest data is written to apreset-quests.jsonfile in the quests directory. Existing user quest files are backed up before import
Technical Details
- New
PrerequisiteGroupdata class with recursive AND/OR evaluation (mode,quests,groupsfields) QuestConfig: prerequisite field now accepts both string array (legacy AND-all) and object format with nested groupsQuestService.canAcceptQuest()/isQuestVisible(): recursiveevaluatePrerequisites()replaces flat list check- Per-objective
orderfield parsed inQuestConfig;QuestService.isObjectiveActive()checks order groups instead of strict sequential index - Backward compat:
"sequential": trueauto-assigns incrementing order values to objectives - New
QuestRewardTypeenum:COMMAND,XP,BOOST_TOKEN,CURRENCY QuestConfigparses rewardtypefield with per-type validation; rewards withouttypedefault toCOMMANDQuestServicereward executor dispatches by type: XP usesSkillService.addXp(), BOOST_TOKEN usesXpBoostService, CURRENCY uses command fallbackQuestPage: displays OR prerequisites, order-grouped objectives, structured reward icons, and tag chipsQuestDefaults: added example branching quest chain, mixed-order quest, and structured reward quest- Quest tags/variables parsed and stored in quest definition; tags displayed in
QuestPageUI - New localization keys for OR prerequisites, order labels, reward types, and tags across all 9 language files
- Added
QUESTStoExportableConfigenum with file stem"quests" ConfigPresetService: new methodsexportQuestsAsJson(),collectQuestFiles(),writeQuestPresetFile(),backupQuestFiles(),backupQuestFilesRecursive()for directory-based quest export/import- Quest export merges all user
.jsonfiles fromquests/(skipping_defaults/andbackups/) into a single JSON object - Quest import writes to
quests/preset-quests.json; existing user quest files are backed up to timestamped backup directory reloadAffectedConfigs()now callsQuestConfig.getInstance().load()when the"quests"config key is presentViewXpPage.formatRewardShort(): now takesSkillComponentand usesMessages.get()withreward.short.*keys instead of hardcodedString.format()(15 reward types)ViewXpPage.populateBuffsBar(): 6 stat bar labels useui.viewxp.buff_*_formatlocalization keys- Tier progress text (
"Tier X/Y") usesui.viewxp.tier_progresslocalization key - 22 new localization keys (
reward.short.*,ui.viewxp.buff_*_format,ui.viewxp.tier_progress) across all 9 language files
Docs Site: Gallery Improvements
- Public/Private visibility toggle - Configs, presets, and quests uploaded to the community galleries now default to Private. Users can toggle visibility with a single click on the Eye/EyeOff badge in the top-right corner of each card on the "My" tab. Only items that are both admin-approved and set to Public appear in community listings
- Extracting quests from presets - Extracting a preset that contains quests now saves them to the Quest Gallery instead of the Config Gallery
Technical Details: Docs Site
- Added
visibilityfield ("public" | "private", default"private") toConfigPreset,ConfigTemplate, andQuestTemplatePrisma models - Community GET routes filter on
{ active: true, visibility: "public" } - Mine GET routes include
visibilityin select; new PATCH handlers toggle visibility with ownership validation - POST routes accept optional
visibilityfield, defaulting to"private" - UI cards show 3-state badge: Pending (Clock, yellow), Public (Eye, green, clickable), Private (EyeOff, gray, clickable)
v0.12.1 - February 2026
Boost Activation Command
- Run a custom command when any XP boost is activated - Server owners can now configure a command that runs automatically whenever a boost is activated (permission boosts, admin boosts, and token activations). Use this to send Discord webhooks, log to a database, or broadcast custom messages
- Configurable scope filter - Choose whether the command fires for all boosts, global boosts only, or personal boosts only via a dropdown on the Admin Config page
- Placeholders - The command template supports
{player},{multiplier},{target},{duration}, and{scope}placeholders that are replaced with the actual boost values at activation time - Admin UI controls - Toggle, command template field, scope dropdown, and placeholder reference all available on the Admin Configuration page under General Settings
Admin UI
- Admin Config page reorganized into sections - The admin configuration page is now split into 4 clearly labeled section cards: General Settings (toggles, dropdowns, formula), Command Integration (milestone announcements and boost activation command), Leveling Parameters, and Boost Stacking Limits. Previously all settings were in a single long section
- Admin Config page no longer scrolls to top on changes - Toggling settings, changing dropdowns, and saving values on the admin config page now update in-place without resetting your scroll position
Bug Fixes
- Fixed hatchets (axes) not working with woodcutting level requirements and tool requirements - Hatchet item IDs were configured as
Tool_Axe_instead of the correctTool_Hatchet_, causing woodcutting level requirements and log tool type requirements to have no effect. Also removed Steel, Silver, and Gold hatchet entries that don't exist in Hytale
Technical Details
- Fixed
ItemRequirementsDefaults: renamedTool_Axe_*→Tool_Hatchet_*for woodcutting requirements, removed non-existent Steel/Silver/Gold variants - Fixed
ActionRequirementsDefaults: renamedTool_Axe_→Tool_Hatchet_for all log tool requirements (15 entries + fallback) - Updated hint text in
ActionRequirementsPage.javaandActionRequirementsPage.ui SkillConfig: 3 new fields (enableBoostActivationCommand,boostActivationCommand,boostActivationCommandScope)- New
CommandExecutor.processBoostPlaceholders()method for boost-specific placeholder substitution XpBoostService.executeBoostActivationCommand()helper called in all 3 activation paths (activateBoost,activateAdminBoost,activateToken)AdminConfigPage.ui: split single#SettingsSectioninto 4 section cards (#SettingsSection,#CommandSection,#LevelingSection,#BoostLimitsSection)AdminConfigPage.java: converted 18 toggle/dropdown/apply actions fromrefreshPage()→sendUpdate()to preserve scroll position; only reload/reset actions still use full page rebuild- 1 new localization key (
ui.admin.command_integration) across all 9 language files - 7 new localization keys for boost activation command across all 9 language files
v0.12.0 - February 2026
Skill Icons
- Skill icons on XP page - Each skill card on the Skill Overview page now displays an item icon beside the skill name representing the skill (pickaxe for Mining, longsword for Swords, bow for Archery, etc.)
- Skill icons in notifications - XP gain and level-up notifications now include a skill icon next to the message text
- Custom skill icon field - Custom skills can now specify an icon path in the admin Custom Skills page and the creation wizard. Icons are used for notifications; skill card icons require a matching entry in SkillCard.ui
Admin UI
- Dropdowns replace button selectors across admin pages - Combat XP Mode, Default Language, Leveling Formula, and Boost Stack Mode on the admin dashboard now use native Hytale dropdowns instead of rows of toggle buttons. The reward type field in the Custom Skill Wizard and Skill Tree Overrides page, and the category selector in the wizard, also use dropdowns. All selections are a single click from a clean list instead of scanning multiple buttons
- Custom Skills page: Edit button opens wizard - The separate "Edit" and "Wiz" buttons on each custom skill row have been merged into a single "Edit" button that opens the full wizard in edit mode
- Unimplemented reward types hidden from dropdowns - Reward types without a working effect (Speed, Knockback Resistance, Haste, Fortune, Vein Miner, Effect Duration, Cooldown Reduction) are no longer shown in the reward type dropdown on admin pages
Quests
- Multi-objective quests now show completion order hint - Quests with two or more objectives now display a label above the objective list indicating whether they must be completed in order ("Complete in order:") or can be done in any sequence ("Complete in any order:"). Localized in all 9 languages
Custom Skills Parity
- Custom skills fully integrated in navigation - Custom skills now appear in the prev/next arrows on Skill Tree and Item Rewards pages, letting players browse through all their skills seamlessly instead of being excluded from navigation
- Total Rewards tab available for custom skills - The "Total" tab on the Item Rewards page now works for custom skills, showing aggregated rewards across all skills
- Custom skills treated identically everywhere - Custom skills now participate in all the same systems as built-in skills with no behavioral differences: combat stat breakdowns, boost multipliers, quest objectives, leaderboards, and admin overrides all use the same unified code path
Bug Fixes
- Fixed players gaining XP in disabled skills - Disabling a skill via the Custom Skills admin page would hide it from the UI and leaderboards, but players could still gain XP from gameplay actions (mining, combat, kills, etc.). Disabled skills now correctly block all XP gains from triggers, combat damage, mob kills, and fishing
- Fixed skill and tier buttons not wrapping in admin pages - When a category had many skills or a skill had many tiers, buttons would overflow horizontally instead of wrapping to the next row in the Skill Tree Overrides and Command Rewards pages. Buttons now auto-wrap to fit the available space
- Fixed players farming fishing XP by catching and releasing the same fish - When using HyFishing, players could repeatedly catch and release the same fish to farm unlimited Fishing XP. The same fish now only awards XP once
- Fixed fish mob kills awarding combat XP when a fishing mod is installed - When HyFishing was active, killing fish mobs would award combat weapon XP (e.g., Swords XP) instead of being blocked entirely. Fish mob kills now award no XP when a fishing integration is active, since fishing XP comes exclusively from rod catches
- Fixed skill permissions not enforced for combat kills, damage-based XP, and fishing - When
enableSkillPermissionswas on, players without a skill permission (e.g.,mmoskilltree.skill.swords) could still gain XP through damage-based combat, mob kills, fish kills, and the HyFishing integration. All XP award paths now respect skill permissions - Fixed
/questcommand missing permission check - The/questcommand (accept, claim, abandon, status) did not checkmmoskilltree.command.questwhen command permissions were enabled, allowing all players to use it regardless of permissions
Technical: Unified Skill ID System (API Migration)
The SkillType enum is now deprecated. All internal code uses string-based skill IDs ("MINING", "SWORDS", "MAGIC", etc.) instead of enum values. Custom skills are now first-class citizens everywhere — no more dual-map patterns or branching on built-in vs custom.
For plugin developers using MMOSkillTreeAPI:
All existing SkillType-based methods still work but are deprecated. Migrate to the string-based equivalents:
| Deprecated Method | Replacement |
|---|---|
| getXp(store, ref, SkillType.MINING) | getXp(store, ref, "MINING") |
| getLevel(store, ref, SkillType.MINING) | getLevel(store, ref, "MINING") |
| addXp(store, ref, SkillType.MINING, amount) | addXp(store, ref, "MINING", amount) |
| removeXp(store, ref, SkillType.MINING, amount) | removeXp(store, ref, "MINING", amount) |
| setXp(store, ref, SkillType.MINING, amount) | setXp(store, ref, "MINING", amount) |
| getLevelProgress(store, ref, SkillType.MINING) | getLevelProgress(store, ref, "MINING") |
| getAllXp(store, ref) → Map | getAllXpByName(store, ref) → Map |
| getAllLevels(store, ref) → Map | getAllLevelsByName(store, ref) → Map |
The string-based methods work for both built-in skills and custom skills. getAllXpByName and getAllLevelsByName include custom skills; the deprecated SkillType versions only return built-in skills.
No player data migration required — the on-disk codec format is unchanged.
Technical Details
- Skill system unified to string IDs — Replaced all
EnumMap+Mapdual-map patterns with singleMapmaps acrossSkillComponent(4 map pairs unified),XpMapsConfig,SkillTreeConfig,CommandRewardsConfig, and all service/page classes.SkillTypeenum is deprecated; onlyMMOSkillTreeAPI.javaimports it. 53 files migrated fromSkillTypeto string-based skill IDs - Extracted standalone
TriggerTypeandSkillCategoryenums fromSkillTypeinner classes. AddedSkillRegistry.BUILTIN_SKILL_NAMES(23-entryList),BUILTIN_SKILL_DATA(Maprecord with displayName/category/description/iconPath/triggerTypes),isBuiltinSkill(String),isKnownSkill(String), and category/trigger/display lookups - Converted
SkillDefaults,SkillTreeDefaults,CommandRewardsDefaultsfromswitch(SkillType)dispatch toMaplookups - Unified
CombatSkillResultto singleString skillIdfield (wasSkillType builtinSkill+String customSkillId). RemovedisCustom()method. UnifiedSkillTreeService.getCombatStatBreakdown()— no longer branches on custom vs built-in - Unified
CombatTargettargeting:MELEE_SKILLS/RANGED_SKILLSchanged fromEnumSettoSet.appliesToByName(String)is now the single code path (removed deprecatedappliesTo(SkillType)). AddedisMeleeSkill(String)/isRangedSkill(String)static helpers - Removed
SkillReward.appliesToCombatSkill(SkillType)—appliesToCombatSkillByName(String)now delegates toCombatTarget.appliesToByName()instead ofSkillType.fromString()dispatch - Removed
RewardEffectContext.skillTypefield — onlyString skillIdfield remains - Removed deprecated
SkillType-taking methods from:SkillService(mergedaddXp/addCustomXp→addXpByName),SkillTreeService(~13 methods),CommandRewardService,XpBoostService(getTotalMultiplier),QuestService(onLevelReached),LeaderboardDataStore,SkillDisplayUtils(8 methods),LuckUtil(3 methods),PermissionUtil,Messages(6 methods) - Added
String iconPathfield toSkillTypeenum (23 built-in defaults) andCustomSkillclass (nullable).SkillRegistry.getIconPath(skillId)provides unified lookup. Icon paths use game content format (Icons/ItemsGenerated/...) for notifications and bundled PNGs inPages/Icons/for UI cards - Added
#CardIconRowtoSkillCard.uiwith 25 hardcoded iconGroupelements (#IconMINING,#IconSWORDS, etc.), each withBackground: "Icons/filename.png"andVisible: false. Java toggles visibility by skill ID viaSKILL_ICON_IDSallowlist. Card layout redesigned: icon (48x48) sits to the left of name/level/progress in a horizontal header row instead of its own centered row, reducing card height from 248 to 218 - Updated
SkillServicenotification calls to use 3-argsendNotification(handler, msg, iconPath)and 4-argsendNotification(handler, msg, subtitle, iconPath)overloads for XP gain, level-up, and first-skill notifications - Added
iconPathfield toCustomSkillsConfig.SkillDatafor JSON persistence, and icon path text fields toCustomSkillsPageform andCustomSkillWizardPageStep 1 - Added
#ObjectivesHintlabel toQuestRow.ui(hidden by default).QuestPage.populateQuestRow()shows it for quests with 2+ objectives, selecting betweenui.quests.objectives_sequentialandui.quests.objectives_any_orderbased onquest.isSequential(). Added localization keys to all 9 language defaults - Added
CustomSkillsConfig.isBuiltinDisabled()check toSkillService.processTrigger()(skips disabled skills during pattern matching),addXpByName(),addXp(), andaddCustomXp()(early return for disabled skills). Admin commands (/setmmoxp,/bulksetxp) are unaffected since they useSkillComponent.setXpByName()directly - Changed
#SkillSelectorand#TierButtonsfromLayoutMode: LefttoLayoutMode: LeftCenterWrapinSkillTreeOverridesPage.uiandCommandRewardsPage.uiso buttons wrap instead of overflowing. Removed separate spacer groups from tier button generation in favor ofRight/Bottomanchor margins - Replaced text fields and toggle buttons with
$C.@DropdownBoxacross 3 pages: reward type inCustomSkillWizardPageandSkillTreeOverridesPage, category in wizard Step 1, and Combat XP Mode / Default Language / Formula / Stack Mode inAdminConfigPage. Entries populated viaDropdownEntryInfo+commandBuilder.set(".Entries", list). Events useValueChangedbindings with@DropdownValuekey prefix for UI value resolution (e.g.,EventData.of("Action", "combatXpMode").append("@DropdownValue", "#CombatXpModeDropdown.Value")) - Removed
buildLanguageButtons(),updateCombatXpModeButtons(), formula/stack-mode button styling blocks, andLANG_BUTTON_TEMPLATEfromAdminConfigPage.java. AddeddropdownValuefield toAdminConfigEventDatacodec - Merged Edit + Wizard buttons in
CustomSkillsPage: removed#WizardBtnfromCustomSkillRow.ui, changed#EditBtnaction towizardEdit, removed deadeditSkillhandler - Added
NoOpEffectfilter toUIUtil.populateRewardTypes()— skips anyRewardTypewhose registered handler isNoOpEffect - Added
seenInstanceIdstracking toHyFishingAdapter— a globalConcurrentHashMap.newKeySet()storing fish instance IDs fromFishCaughtEvent.getFishInstanceId(). Duplicate IDs are skipped before XP lookup. Reflection-cached with graceful fallback if the method doesn't exist on older HyFishing versions. Bounded at 10,000 entries (clear-and-re-add on overflow); not persisted since fish entities don't survive restarts - Added
PermissionUtil.canGainSkillXpByName()checks to 4 directSkillService.addXpByName()call sites that bypassedprocessTrigger():CombatXpEventSystem.handleDamageBasedXp(),MobKillEventSystem.handleKillXp(),MobKillEventSystem.handleFishKillXp(), andHyFishingAdapter.awardFishingXp() - Added
PermissionUtil.hasCommandPermission(player, "quest")check toPlayerQuestCommand.execute() - Restructured fish mob handling in
MobKillEventSystem: fish mobs nowreturnearly regardless of fishing integration status — either awarding Fishing XP (no integration) or nothing (integration active). Previously the no-integration branch returned but the integration-active branch fell through to combat XP. AddedisFishMobWithFishingIntegration()guard toCombatXpEventSystemto block per-hit combat XP on fish mobs when a fishing integration is active - Consolidated UI event binding boilerplate into
UIUtil: addedbindButton()(simple action) andbindButton()(action + key-value param) forActivatingbindings. MigratedAwardBoostPage(28 bindings →UIUtil.bindButton()/bindTextField()) andLeaderboardPage(8 bindings →UIUtil.bindButton()), removing directCustomUIEventBindingType/EventDataimports from both
v0.11.2
Skill Capes
- Magic mastery cape - Reaching level 100 in Magic now awards a mastery cape with +30 Mana, +8% Signature damage, and light armor stats - matching the Staves cape's spellcaster focus
- Artillery mastery cape - Reaching level 100 in Artillery now awards a mastery cape with +10% Charged damage, +14% Projectile resistance, and +16 Health - matching the Archery cape's ranged combat focus
- Fishing mastery cape - Reaching level 100 in Fishing now awards a mastery cape with gathering-focused stats (+14% Physical resistance, +10% Projectile resistance, +22 Health)
Item Rewards
- Per-reward auto-claim - Each item reward can now be individually marked as "auto-claim" in the Edit Rewards admin page. Rewards with auto-claim enabled are automatically given to players on level-up without requiring manual claiming from the Item Rewards UI. Custom skills now also receive item reward notifications on level-up
Admin UI
- Skill Tree Overrides page redesigned with native Hytale styling - The skill tree overrides editor now uses the standard Hytale container layout with themed panels, scrollbars, separators, and button sounds, matching the rest of the admin dashboard
- All skills can now add extra tiers - The "+" button to add new tiers beyond the defaults is no longer restricted to custom skills. Built-in skills (Mining, Swords, etc.) can now have additional tiers added through the Skill Tree Overrides page. Resetting an added tier removes it entirely instead of reverting to a nonexistent default
- Custom Skill Creation Wizard - A new 4-step guided wizard accessible from the Custom Skills admin page combines skill definition, XP map configuration, item requirements, and skill tree rewards into a single flow. Step 1 defines the skill (ID, display name, category, triggers), Step 2 configures XP patterns, Step 3 sets item level requirements, and Step 4 builds skill tree tiers with milestone rewards. Steps 3 and 4 can be skipped. The wizard also supports editing existing custom skills with all fields pre-populated. All changes are saved atomically on finish - cancelling at any point discards everything
Technical Details
- Rewrote
SkillTreeOverridesPage.uifrom rawGrouproot to$C.@Container { @CloseButton = true; }pattern with$C.@PageOverlay,$C.@ContentSeparator,ContainerPanelPatch.pngpanel backgrounds,$C.@DefaultScrollbarStyle, and$C.@ButtonSoundson all button styles - Added
addBuiltinSkillTier()toSkillTreeConfigfor expanding built-in skill trees beyond their default tier range - Fixed
setTierOverride(),applyOverrides(),setSkillMilestones(), andremoveSkillMilestones()to expand the effective node list when overrides exist beyond the default tier count (was silently dropping them withif (tier < effective.size())) - Fixed
removeTierOverride()to shrink the effective list and re-index higher overrides when removing a tier beyond the default range (mirrors custom skill removal logic) - Created
Cape_Skill_Magic.jsonandCape_Skill_Artillery.jsonitem definitions with stats matching their respective skill archetypes (Staves-like for Magic, Archery-like for Artillery) - Fixed
Cape_Skill_Fishing.jsonIcon and Texture paths fromCape_Skill_Bluntreferences toCape_Skill_Fishing - Added
reward.cape.magicandreward.cape.artillerylocalization keys to all 9 language defaults - Added cape item translations (
name/description) to all 9items.langfiles for Fishing, Magic, and Artillery - Created
CustomSkillWizardPage.java(extendsInteractiveCustomUIPage) withWizardEventDatacodec (15 fields) andTierDatainner class for step 4 state - Created
CustomSkillWizardPage.uiwith step indicator bar, 4 content panels, and navigation bar. Row templates:WizardXpRow.ui,WizardReqRow.ui,WizardTreeRewardRow.ui - Added "Wiz" button to
CustomSkillRow.uiand "Wizard" button toCustomSkillsPage.uiform header, withopenWizard/wizardEditevent handlers inCustomSkillsPage.java
v0.11.1 - February 2026
Admin UI
- Built-in custom skills can now be disabled - Magic and Artillery now appear in the Built-in Skills table on the Custom Skills admin page with the same enable/disable toggle as the 22 core skills. Previously they had no toggle and could not be turned off
Item Rewards
- Default item rewards for Magic - Magic now has milestone item rewards at levels 10, 25, 50, 65, 80, and 100, progressing from wooden wands through spellbooks. Level 100 rewards include Frost, Fire, and Demon spellbooks, a mastery cape, and a global XP boost
- Default item rewards for Artillery - Artillery now has milestone item rewards at levels 10, 25, 50, 65, 80, and 100, progressing from basic bombs through guns. Level 100 rewards include large fire bombs, frag grenades, a mastery cape, and a global XP boost
Bug Fixes
- Fixed XP tokens not working for custom skills - Consuming a flat XP token for a custom skill (e.g., Magic or Artillery) would silently fail and not grant any XP. Custom skill tokens now correctly grant XP, trigger level-up notifications, and check for milestone rewards
- Fixed boost tokens crashing when targeted at custom skills - Boost tokens and active boosts that targeted a custom skill by name would crash during deserialization, causing the boost to be lost. Skill-targeted boosts now work for any registered skill
- Fixed boosts for custom skills not applying - XP boosts targeting a specific custom skill (e.g., "2x Magic XP") would never apply because the boost system only recognized built-in skill names. Boosts now correctly match against all registered skills including custom ones
- Fixed boost target showing "Unknown" for custom skills - When a boost targeted a custom skill, the UI and notifications would display "Unknown" instead of the skill name. Boost targets now show the correct localized skill name for all skills
Technical Details
BoostTarget: AddedskillIdstring field andskillByName(String)factory.fromPermissionValue()now checksSkillRegistryfor custom skills.appliesByName()uses string comparison instead of enum equality, supporting category matching for custom skills via registry lookupBoostToken/ActiveBoost: Serialization usesgetSkillId()instead ofgetSkill().name(). Deserialization usesBoostTarget.skillByName()instead ofSkillType.valueOf(), eliminating crashes on custom skill IDsXpTokenConsumeInteraction: Switched fromSkillType-based to string-based API —getSkillId(),getLevelByName(),addByName(),getXpByName(),handleLevelUpByName(),checkMilestonesByName(),getSkillNameById()Messages.getBoostTargetNameForLang(): UsesgetSkillNameForLangById()viatarget.getSkillId()instead of deprecatedgetSkillNameForLang(SkillType)XpTokenItemGenerator: ReplacedSkillType.fromString()lookups withSkillRegistry.getInstance().getDisplayName()for token display namesCustomSkillsPage: Built-in custom skills (MAGIC, ARTILLERY) moved from Custom Skills table to Built-in Skills table, sharing the same toggle logic. Max level filter now usesisBuiltinDisableddirectly instead of only checking enum skillsCommandRewardsDefaults: AddedgetMagicDefaults()andgetArtilleryDefaults()with full 6-tier milestone reward progressionCommandRewardsConfig.loadDefaults(): Now seeds built-in custom skill defaults viaSkillDefaults.getBuiltinCustomSkillIds().writeDefaultsReference()andremoveOverrideByName()also updated to handle built-in custom skill code defaults
v0.11.0 - February 2026
New Skills: Magic & Artillery
- Magic - A new combat skill for wands and spellbooks, split from Archery. Gain XP by dealing damage with any wand or spellbook. Includes a mana-heavy skill tree (350+ potential mana from tree rewards alone), XP tokens, boost templates, and item level requirements
- Artillery - A new combat skill for bombs and guns, split from Archery. Gain XP by dealing damage with explosives and firearms. Includes a damage/crit/health-focused skill tree, XP tokens, boost templates, and item level requirements
- Archery refocused - Archery now covers bows, crossbows, blowguns, and arrows/darts only. Wand, spellbook, bomb, and gun XP patterns have been moved to their respective new skills. The Archery skill tree has been updated to a stamina-focused ranged tree, replacing the old mana-heavy rewards that were designed for wand users
- Both new skills ship as "built-in custom skills" — they appear in all UI pages, admin editors, leaderboards, and quest requirements automatically, just like any other combat skill
Ranged Weapon Level Requirements
- Shortbow requirements added - Shortbows now have Archery level requirements matching the standard material progression (Copper: 1, Iron: 10, Cobalt: 40, Mithril: 50, Adamantite: 60, Thorium: 70, Onyxium: 80)
- Longbow requirements added - Longbows now have Archery level requirements (Copper: 1, Iron: 10, Cobalt: 40, Adamantite: 60, Thorium: 70)
- Per-material crossbow requirements added - Individual crossbow materials now have Archery level requirements instead of just a generic crossbow entry (Iron: 10, Cobalt: 40, Mithril: 50, Adamantite: 60, Thorium: 70, Onyxium: 80)
- Wand requirements - Wands require Magic skill levels (Wood: 1, Tribal: 10, Root: 20, Stoneskin: 40)
- Spellbook requirements - Spellbooks require Magic skill levels (Brown Grimoire: 15, Purple Grimoire: 25, Fire/Frost: 50, Demon: 70)
- Bomb and gun requirements - Artillery weapons require Artillery skill levels (Bomb: 1, Gun: 20, Handgun: 30, Blunderbuss: 35, Assault Rifle: 50)
- Item level requirements now support custom skills - Any skill (built-in or custom) can be used as a level gate for items, not just the 22 built-in skills. Existing requirements continue to work unchanged
Leaderboard
- Custom skills now appear in leaderboard filters - When viewing a category leaderboard (e.g., Gathering), custom skills in that category now show as individual filter buttons alongside built-in skills. Players can sort and view the leaderboard by any custom skill
- Total level no longer counts untouched skills - Skills at level 1 (no XP earned) are excluded from total and category level calculations on both the leaderboard and XP page. A player with Mining 10 and all other skills untouched now shows total level 10 instead of an inflated number
API Improvements
- Unified skill ID API for custom skills - All public API methods now accept string-based skill IDs alongside the original enum-based parameters. This makes custom skills fully interchangeable with built-in skills when other plugins interact with MMO Skill Tree's data. The original enum-based methods continue to work but are now deprecated in favor of the string-based versions
- For mod authors - You can discover all available skill IDs at runtime via
SkillRegistry.getInstance().allSkillIds()(returns both built-in names like"MINING"and custom IDs like"HERBALISM"). UseSkillRegistry.getInstance().getDisplayName(id)for the localized display name and.getCategoryName(id)for the category ("GATHERING","COMBAT","CRAFTING","MISC"). Then pass any skill ID string toMMOSkillTreeAPI.getLevel(store, ref, id),.getXp(...),.addXp(...), etc. — no need to resolve built-in vs custom yourself
Bug Fixes
- Fixed ranged weapons not earning combat XP on hit - Wands, bows, spellbooks, bombs, guns, and other ranged weapons were not awarding per-hit or damage-based XP. The combat XP system only recognized melee damage sources, so any damage dealt via a projectile (arrows, spells, explosions) was silently ignored. Ranged weapons now correctly award XP on every hit across all combat XP modes
- Fixed ranged weapons not applying combat bonuses - Damage bonuses, critical chance, lifesteal, and defense reduction from skill tree rewards were not being applied to ranged weapon hits. Only melee attacks received these bonuses. All combat bonuses now apply to both melee and ranged attacks
- Fixed combat XP blocked by hostility check on ranged hits - Hitting a mob from range before it detected the player would award zero XP because the mob wasn't "hostile" yet. The hostility check has been removed from per-hit XP — the entity blacklist already prevents XP from unwanted targets, and this matches kill-based XP which never had a hostility requirement
- Fixed Magic and Artillery not appearing in Custom Skills admin page - The built-in custom skills (Magic, Artillery) were missing from the Custom Skills section of the admin UI. They now appear with a "(built-in)" label and blue highlight, with edit/delete buttons hidden since they ship with the mod
- Fixed combat XP not earned with wands, spellbooks, bombs, and guns - Weapons without a durability system (wands, spellbooks, bombs, guns) were incorrectly treated as "broken" by the combat XP system, silently blocking all XP gain. Damage bonuses and critical hits still applied because the damage modification system did not have this check. The broken-item guard has been removed — zero-damage hits are already filtered naturally
- Fixed Magic and Artillery defaults not showing in XP Overrides admin page - When selecting Magic or Artillery in the admin XP Overrides editor, the default XP patterns (wands, spellbooks, bombs, guns) were not displayed. Only user overrides appeared, making it impossible to see or compare against the built-in defaults. The defaults now display correctly for all skills including built-in custom skills
Technical Details
Magic & Artillery Architecture
- Created
MagicDefaults.javaandArtilleryDefaults.javaXP default files with wand/spellbook and bomb/gun patterns respectively - Removed wand, spellbook, bomb, and gun
putAllcalls fromArcheryDefaults— Archery now only loadsBowDefaultsplus ranged fallback patterns SkillDefaults.getBuiltinCustomSkillIds()returnsList.of("MAGIC", "ARTILLERY")— config loaders use this to seed defaults for built-in custom skillsSkillDefaults.getDefaults(String)checks built-in custom skill IDs before returning empty, enablingXpMapsConfig,SkillTreeConfig, etc. to load defaults for MAGIC/ARTILLERYCombatTarget.ARTILLERYadded to the combat target enum.CombatTarget.MAGICwas already presentSkillTreeDefaults.getMagicDefaults()— mana-heavy 10-tier tree with damage/crit rewards usingCombatTarget.MAGICSkillTreeDefaults.getArtilleryDefaults()— damage/crit/health 10-tier tree usingCombatTarget.ARTILLERYSkillTreeDefaults.getArcheryDefaults()— reworked to stamina-focused tree, removing mana rewards- MAGIC and ARTILLERY registered as
CustomSkillobjects inSkillRegistrybefore config loading, soCombatWeaponUtilcan resolve weapons to the correct skill XpTokenDefinition: AddedskillIdString field andflatXpByName()factory for custom skill tokens. 26 new tokens (16 flat XP + 10 personal boost) for MAGIC/ARTILLERY (279 total, up from 253)BoostTemplateDefaults: Added 4 boost template permission nodes for MAGIC/ARTILLERY (1.5x and 2.0x global boosts)ItemRequirementsDefaults: RefactoredItemRequirementrecord fromMaptoMap. UpdatedItemRequirementsConfigoverride serialization,ItemRequirementServicelevel checks, and all default entries to use string skill IDs
SkillType Deprecation: Unified Loops
- Replaced
SkillType.values()iteration withSkillRegistry.getInstance().allSkillIds()across all services, commands, pages, and utilities SkillTreeService: All 6 core loops (getActiveRewards,sumFilteredRewards,reapplyStatRewards,validateAllRewards,removeAllStatModifiersx2) now useallSkillIds()withByNamemethod dispatchCommandRewardService: All 4 loops (checkAllRewards,extractLevelFromRemainder,extractSkillNameFromRemainder, reward validation) unified toallSkillIds()SkillService:findBestXpMatchandinitializeDebugXpunified to singleallSkillIds()loop withXpMapsConfig.getXpMapByName()SkillComponent:getTotalClaimedRewardCountandgetSummedTotalLevelconverted (codec loops intentionally left as enum-indexed storage)CombatWeaponUtil.resolveCombatSkillFromItemId(): Replaced dual built-in/custom loop with singleallSkillIds()loop- Commands:
BulkSetXpCommand,SetXpCommand,MMOConfigCommand(list + diff),SkillTreeCommandall converted - Pages:
AwardBoostPage,BoostPage,QuestPage,LeaderboardPage,SkillTreePage,ItemRewardsPageall converted. Navigation (prev/next skill) now includes custom skills - i18n:
Messages.extractSkillIdFromRewardId()returns String. All 3 language defaults files generate keys for custom skills viaallSkillIds() RewardEffectContext: AddedskillIdString field and string-based constructor.SkillTypeconstructor deprecatedSkillRegistry: AddedhasTrigger(String skillId, TriggerType trigger)for unified trigger checkingMMOSkillTreePlugin,MMOSkillTreeAPI.getAllLevelsByName,LeaderboardDataStore.updatePlayerall converted toallSkillIds()
Ranged Combat & Projectile Source Fix
CombatXpEventSystem: Replacedsource instanceof Damage.EntitySourcewithresolveAttackerRef(source)helper that handles bothDamage.EntitySource(melee) andDamage.ProjectileSource(ranged). Previously only melee hits triggered the attacker-is-player XP logic; projectile hits fell through to the environment/fall-damage else branchCombatDamageEventSystem: SameresolveAttackerRefrefactor — damage bonuses, crit, lifesteal, weapon requirement checks, and defense reduction now apply to projectile damage. Defense reduction guard changed fromsource instanceof Damage.EntitySourcetoattackerRef != nullMobKillEventSystemalready handled both source types via its ownresolveAttackerRef, which is why kill-based XP worked for ranged weapons while per-hit XP did not- Removed
HostilityUtil.isHostileTowards()guard fromCombatXpEventSystem— ranged weapons hit mobs before they aggro, causing the check to fail on first contact. Entity blacklist provides sufficient filtering CustomSkillsPage.buildCustomSkillsTable(): Now combines built-in custom skills fromSkillRegistrywith user-defined skills fromCustomSkillsConfig. Built-in skills shown with "(built-in)" label, edit/delete buttons hiddenCombatXpEventSystem: RemovedgetDurability() <= 0broken-item check fromhandleDamageBasedXpandhandlePlayerDealsDamage. Items without a durability system (wands, spellbooks, bombs, guns) return 0 fromgetDurability(), causing the check to block all XP for these weapons.CombatDamageEventSystemnever had this check, which is why combat bonuses (crit, damage, lifesteal) applied correctly while XP did not. ThedamageAmount <= 0guard at the top ofhandle()already prevents XP from zero-damage hitsXpOverridesPage.buildPatternList(): ReplacedSkillType.fromString()+ ternary with directSkillDefaults.getDefaults(currentSkillId)call. TheSkillType-based overload returnednullfor non-enum skills (MAGIC, ARTILLERY), causing the ternary to fall through toMap.of()and display no defaults
Previous ByName API Additions (carried from 0.10.10 development)
SkillComponent.getSummedTotalLevel(),ViewXpPage.getSkillsTotalLevel(), andLeaderboardPage.getCategoryLevelFromEntry()now skip skills at level 1- Deprecated all
SkillType-param methods across the public API, services, configs, and utilities (~100+ methods) MMOSkillTreeAPI: AddedgetXp(store, ref, String),getLevel(store, ref, String),addXp(store, ref, String, long),removeXp(store, ref, String, long),setXp(store, ref, String, long),getLevelProgress(store, ref, String),getAllXpByName,getAllLevelsByNameSkillComponent: Added 7 missingByNameoverloads. Deprecated 23SkillType-param methodsSkillService: AddedaddXpByName,handleLevelUpByName,notifyLevelUpByNameSkillTreeService: Deprecated 21 methods, added 9ByNameoverloadsCommandRewardService: AddedcheckAndExecuteRewardsByName,validateCommandRewardsByName- Config layer: Deprecated
SkillType-param methods acrossSkillTreeConfig(16),XpMapsConfig(7),CommandRewardsConfig(7),ItemRequirementsConfig(1),SkillDefaults(1),SkillTreeDefaults(1),CommandRewardsDefaults(1) - Utils: Deprecated
SkillType-param methods inSkillDisplayUtils(10),PermissionUtil(1),LuckUtil(3),Messages(6),RewardEffectContext(1) LeaderboardPage: ChangedskillFilterfromSkillTypetoString.PlayerLeaderboardEntryusesMapinstead of separate built-in/custom mapsViewXpPage: Replaced 4 separate total methods with 2 unifiedgetSkillsTotalLevel/getSkillsTotalXptakingList
v0.10.9
Mod Compatibility
- Modded weapons now earn XP automatically - Added generic weapon-type fallback patterns to all combat skill XP maps so that weapons from other mods earn XP even without explicit configuration. For example, any item with "Longsword" in its name earns Swords XP, any item with "Bow" earns Archery XP, and so on. Specific vanilla weapon entries always take priority over these generic patterns
- New fallback patterns by skill:
- Swords - "Longsword" (8 XP), "Greatsword" (8 XP)
- Axes - "Battleaxe" (8 XP), "Hatchet" (4 XP)
- Archery - "Bow" (5 XP), "Shortbow" (6 XP), "Longbow" (6 XP), "Crossbow" (7 XP), "Arrow" (3 XP), "Blowgun" (4 XP)
- Magic - "Wand" (6 XP), "Spellbook" (8 XP)
- Artillery - "Bomb" (10 XP), "Gun" (25 XP), "Grenade" (30 XP)
- Daggers - "Knife" (4 XP), "Claw" (4 XP)
- Polearms - "Halberd" (6 XP), "Trident" (6 XP), "Glaive" (6 XP), "Lance" (6 XP), "Pike" (5 XP), "Javelin" (5 XP)
- Blunt - "Hammer" (6 XP), "Flail" (6 XP), "Maul" (8 XP)
- Staves - "Staff" (6 XP)
Bug Fixes
- Fixed Defense XP maps not using mob IDs - Defense XP map entries were matching against the damage cause type (e.g., "Physical", "Sword_") instead of the attacking mob's ID. This meant server owners couldn't customize Defense XP per mob through the XP maps config or admin UI. Defense XP maps now use mob IDs, matching how all other combat XP maps work. For non-mob damage (environmental, PvP), falls back to the damage cause ID (e.g., "Physical", "Fire"). Defaults rebalanced by mob tier: critters (2-3 XP), standard enemies (5-6 XP), tough enemies (8 XP), elites (12 XP), bosses (25-30 XP)
/mmoboostno longer requires the--world=flag - The boost command now resolves the world automatically from the player or server context, removing the need to specify a world argument
- Fixed combat reward targeting for custom skills - Skill tree combat rewards (Damage, Critical Chance, Lifesteal, Parry stats) can now target custom skill IDs in addition to built-in weapon types. Previously, entering a custom skill ID (e.g., "KATANA") as the target in the Skill Tree Overrides editor would silently default to "All Combat". Custom targets now work correctly - rewards targeting a custom skill only apply when using a weapon mapped to that skill
- Custom skill tree rewards now included in combat stat calculations - Rewards claimed from custom skill trees (e.g., a custom Katana skill tree) now contribute to combat stats like damage, crit chance, and lifesteal. Previously only rewards from built-in skill trees were counted
Technical Details
CombatXpEventSystem.handlePlayerTakesDamagenow resolves the attacker's mob ID viaEntityIdentifierUtil.getMobId()and uses it as the XP map identifier instead of the damage cause ID. Falls back to damage cause for environmental/PvP damage (no mob source)DefenseDefaultsrewritten with mob-ID-based patterns (e.g.,Trork,Skeleton,Dragon) matching the same identifiers used byMobKillXpDefaults, replacing the old damage-cause patterns (Physical,Sword_,Fire)- Generic fallback patterns added to aggregator defaults files (
SwordsDefaults,AxesDefaults,ArcheryDefaults,DaggersDefaults,PolearmsDefaults,BluntDefaults) andStaffDefaults - Substring matching with longest-pattern-wins ensures specific entries (e.g.,
Weapon_Longsword_Ironat 24 chars) always beat generic fallbacks (e.g.,Longswordat 9 chars) BoostCommandmigrated fromAbstractAsyncWorldCommandtoAbstractAsyncCommand. World is now resolved from the player sender viaplayer.getWorld(), or fromUniverse.get()for console sendersSkillRewardextended withcustomCombatTargetIdfield for targeting custom skill IDs. NewappliesToCombatSkill(CombatSkillResult)andappliesToCombatSkill(SkillType)methods route targeting logic: custom targets match by string ID, built-in targets useCombatTargetenumSkillTreeConfig.RewardDataserialization updated:fromReward()usesgetTargetString(),toReward()triesCombatTarget.fromString()first then falls back to treating unrecognized strings as custom skill IDsSkillTreeService.sumFilteredRewards()andgetActiveRewards()now iterate bothSkillType.values()andSkillRegistry.getCustomSkillIds()to include rewards from custom skill treesSkillTreeServicefilter predicates changed fromr.getCombatTarget().appliesTo()tor.appliesToCombatSkill()for proper custom target routingSkillTreeOverridesPageaddReward handler treats unrecognized target input as custom skill ID instead of silently defaulting to ALL
v0.10.8
Per-Skill Parry Stats
- Parry bonuses are now weapon-aware - The 4 parry stats (Counterattack, Reflect, Stamina Drain, Stun Damage) now respect combat targeting, matching how Damage, Critical Chance, and Lifesteal already work. A parry reward set to "Swords" only applies when holding a sword, while "All Combat" rewards apply with any weapon
- Parry rewards now show weapon targeting in tooltips - Parry rewards with a specific combat target (e.g., Swords, Melee, Ranged) now display the target in parentheses on the skill tree and buff displays, matching the format used by Damage and Critical Chance rewards
Parry Reward Rebalance
- Parry reward values rebalanced - All parry reward values updated based on feedback from the Perfect Parries mod developer. Values are now much more impactful and meaningful, scaling from 20% at level 40 up to 45% at level 100
- Defense skill provides global parry rewards - Defense parry rewards apply with any weapon: Reflection Damage (9% at Lv40, 15% at Lv100), Counterattack Damage (20% at Lv50, 40% at Lv100), Stamina Loss reduction (15% at Lv65, 30% at Lv100), and Stun Damage (25% at Lv80, 40% at Lv100)
- Weapon skills provide weapon-specific parry rewards - Swords, Daggers, Polearms, Staves, Axes, Blunt, and Unarmed now offer Counterattack Damage and Stun Damage rewards that only apply when holding that weapon type. Values scale from 20-30% at level 40 up to 30-45% at level 100
- Fishing parry rewards updated - Fishing retains global Reflection Damage and Stamina Loss reduction at roughly half the Defense values, fitting its role as a gathering skill with light combat utility
Technical Details
SkillReward.isCombatReward()expanded to includePARRY_COUNTERATTACK,PARRY_REFLECT,PARRY_STAMINA_DRAIN,PARRY_STUN_DAMAGE- enablesgetFormattedValue()andMessages.getRewardDisplayText()to append combat target suffixSkillTreeService: added 8 per-skill parry methods (getParryCounterattackForSkill,getParryReflectForSkill,getParryStaminaDrainForSkill,getParryStunDamageForSkill) with bothSkillTypeandCombatSkillResultoverloads, reusinggetFilteredCombatReward()MMOSkillTreeAPI: existinggetParryCounterattackBonus,getParryReflectBonus,getParryStaminaDrainBonus,getParryStunDamageBonusnow resolve held weapon viaCombatWeaponUtil.resolveCombatSkill(store, ref)and delegate to per-skill methods instead of global totals. External callers (e.g., Perfect Parries mod) get weapon-aware filtering without API changesSkillTreeDefaults: Defense parry rewards updated to new values (Reflect 9→15%, Counter 20→40%, StamDrain -15→-30%, Stun 25→40%) withCombatTarget.ALL. Weapon skills (Swords, Daggers, Polearms, Staves, Axes, Blunt, Unarmed) standardized to Counterattack + Stun Damage pair with per-skillCombatTarget(20-30% at T5-T7, 40-45% at T8-T9). Fishing scaled to ~half Defense values for Reflect + StamDrain (global)
v0.10.7
Bug Fixes
- Fixed custom skills not showing skill tree or item reward buttons on the XP overview page - Custom skills were missing the "View Tree", "View Items" / "Claim Items" buttons on their skill cards, even when skill tree nodes or item rewards were configured for them. All buttons now appear and link to the correct pages, matching built-in skill behavior
- Fixed skill tree page not working for custom skills - Opening the skill tree from a custom skill card now correctly shows tree tiers, allows claiming rewards, and supports resetting. Previously, clicking the tree button had no effect since the page only supported built-in skills
- Fixed item rewards page not working for custom skills - Opening the item rewards page from a custom skill card now correctly shows and allows claiming per-skill and global skill rewards
UI Improvements
- Skill tree reward choices redesigned as cards - Reward options on the skill tree page are now displayed as styled cards with the reward name, optional description, color-coded status (Available/Claimed/Locked), and an explicit Claim button. Replaces the flat text buttons used previously
- Skill tree choices auto-wrap to multiple rows - Tiers with many reward options now automatically wrap cards to the next line instead of being limited to a fixed row of 6
- Item reward cards redesigned with panel styling - Reward cards on the item rewards page now use the standard Hytale inset panel background with improved spacing, a thin separator between the name and description, and consistent visual hierarchy matching the skill tree cards
- Card text now wraps for long names and descriptions - Reward text and descriptions on both skill tree and item reward cards now auto-wrap instead of being truncated by fixed-height labels. Cards auto-size vertically to fit their content
- Fixed tab buttons on item rewards page - The Skill Rewards / Total Level tab buttons now correctly update their hover and pressed states when switching tabs, instead of only changing the default appearance
- Fall damage reduction now shown in the buff bar - The top buff bar on the skill overview page now displays your total fall damage reduction percentage alongside existing combat stats
- Claiming rewards no longer scrolls page back to the top - Claiming skill tree or item rewards now updates the card in-place without re-rendering the entire page, preserving your scroll position
- Accepting, abandoning, and claiming quests no longer scrolls page back to the top - Quest actions now update the quest row in-place, preserving your scroll position in the quest list
- Quest page now shows active quest count vs maximum - The active quest counter in the header now displays as "X/Y" (e.g., "3/10") so you can see how many quest slots remain
Technical Details
- Added tree button, reward button, and tier metadata logic to
buildCustomSkillCard()inViewXpPage - Added
hasUnclaimedItemsByName()andgetRewardStatusByName()helpers inViewXpPagefor string-based skill ID lookups - Fixed NPE in
populateSkillCard()tree/reward button event bindings - usescard.customSkillIdwhencard.skillis null ViewXpPagetree and skillRewards event handlers now fall through to custom skill constructors whenSkillType.fromString()returns null- Added
SkillTreePage(PlayerRef, String)constructor for custom skill trees with@Nullable selectedSkill/customSkillIdfields SkillTreePageusesgetSkillKey()with byName methods (getSkillTreeByName,getNodeByName,getClaimedCountByName, etc.) for all tree operations- Added
ItemRewardsPage(PlayerRef, String)constructor for custom skill rewards ItemRewardsPageusesgetSkillKey()/getLevelRewardsByName()for reward lookup and claim tracking- Custom skills on both pages hide prev/next navigation arrows (no cycling through built-in skills)
- Added byName methods to
SkillComponent:getClaimedRewardIdsByName,getClaimedCountByName,hasClaimedSpecificRewardByName,claimRewardByName,getPendingRewardsByName,getClaimedRewardCountByName,getClaimedTierCountByName,resetRewardsByName - Added
SkillTreeService.claimRewardByName()andresetRewardsByName()- delegate to built-in methods forSkillTypematches, usecustomClaimedRewards/customPendingRewardsmaps for custom skills - Replaced
ChoiceButton.uitext button template withSkillTreeChoiceCard.ui- 180x130px card withContainerPanelPatch.pngbackground,#RewardText,#RewardDesc,#RewardStatus, and#ClaimBtnwidgets TierRow.uichanged from fixedHeight: 90withLayoutMode: Leftchoices container to auto-height withLayoutMode: LeftCenterWrapfor automatic card wrapping- Deleted
TierRowExpanded.ui,ChoiceButton.ui,ChoiceButtonSpacer.ui- wrapping is now handled byLeftCenterWrapin the singleTierRow.ui SkillTreePage.javauses singleCHOICE_CARD_TEMPLATEconstant, populates#RewardText,#RewardDesc,#RewardStatus, and#ClaimBtnper card state; removedCHOICES_PER_ROW/MAX_CHOICESlimits and second-row/spacer logicRewardCard.uiredesigned from flat 186x95px card to 186x140px with outer padding wrapper and innerContainerPanelPatch.pngbackground- Replaced
RewardCardRow.ui(fixedLayoutMode: Leftrows) withRewardCardGrid.ui(LayoutMode: LeftCenterWrapgrid) ItemRewardsPage.javausesCARD_GRID_TEMPLATE- one grid per level tier, cards flat-appended. RemovedCARDS_PER_ROWconstant and row-chunking logic. Background color targeting changed from.Backgroundto#CardInner.Background.Color- Added
ui.skilltree.not_selectedlocalization key to all 9 language files SkillTreeChoiceCard.uiandRewardCard.uilabels useWrap: truein styles andAnchor: (Bottom: X)instead of fixedHeight- enables text wrapping and auto-height cards following the PrefabEditorSettings/SubcommandCard pattern- Fixed
ItemRewardsPage.javatab styling to set all 3 button states (Default/Hovered/Pressed) when switching to Total Level tab - Added
#BuffFalllabel (80px,#87ceeb) toViewXpPage.uibuffs bar;populateBuffsBar()readsFALL_DAMAGE_REDUCTIONfrom active rewards SkillTreePage.handleChoiceSelectionnow usessendUpdate()instead ofrefreshPage()- updates claimed card appearance, tier status, unclaimed card dimming, selection info, and header counts via partial UI update. Reset action still uses full page refreshItemRewardsPageclaim handlers (handleClaimSkillReward,handleClaimTotalReward,handleClaimGlobalSkillReward) now usesendUpdate()viasendCardClaimedUpdate()helper - updates card status/background/button visibility and recalculates header claimed count in-place- Added
Selectorfield toItemRewardsEventDatacodec - card UI selector passed frompopulateCardthrough claim button event data for targeted partial updates - Removed
refreshSkillRewardsPage()fromItemRewardsPage- replaced bysendCardClaimedUpdate(),getSkillRewardsClaimedText(), andgetTotalRewardsClaimedText()helpers QuestPageaccept/claim/abandon handlers now usesendUpdate()viasendQuestAcceptedUpdate(),sendQuestClaimedUpdate(), andsendQuestAbandonedUpdate()helpers - updates quest row status, hides action button, and refreshes header counts without full page refresh- Added
Selectorfield toQuestEventDatacodec - quest row selector passed frompopulateQuestRowthrough action button event data QuestPageactive count display changed from plain count to "X/Y" format showing active quests vsSkillConfig.getMaxActiveQuests()
v0.10.6
Bug Fixes
- Fixed daily harvest quest not tracking crop pickups - The "Harvest Season" daily quest was using a block-break objective type instead of item pickup, so harvesting crops via interactive pickup wasn't counting toward quest progress
- Fixed HyFishing mod not being detected - The optional dependency declaration for HyFishing used an incorrect group ID, preventing the mod from being recognized when installed. Additionally, optional mods that load after MMO Skill Tree are now detected via a deferred re-check on first player connect
- Fish kills no longer award Fishing XP when a fishing mod is installed - When a fishing integration like HyFishing is active, killing fish mobs awards weapon combat XP instead of Fishing XP. Fishing XP only comes from rod catches via the fishing mod. Without a fishing mod, fish kills always award Fishing XP regardless of combat mode
- Combat XP now requires hostile targets in all modes except Kill-Only - Damage-Based and Damage+Kill modes previously awarded XP for hitting any mob (including passive/friendly). Now all modes except Kill-Only check if the target is hostile before awarding XP, consistent with Per-Hit mode
Admin
- Max Active Quests setting added to admin config page - Server admins can now change the maximum number of simultaneous active quests per player directly from the admin dashboard instead of editing config files
UI Improvements
- Boost Templates admin page redesigned with native Hytale styling - The boost templates editor now uses the standard Hytale container layout with themed panels, scrollbars, separators, and button sounds, matching the XP Overrides page
- XP Token Overrides admin page redesigned with native Hytale styling - The XP token/boost token editor now uses the standard Hytale container layout with themed panels, scrollbars, separators, and button sounds
- Fixed crash when viewing Combat skills on Boost Templates page - Selecting the Combat category (which has 11 skills) caused a disconnect because the skill selector buttons overflowed their container. Skill buttons now wrap to multiple rows
- Fixed crash when viewing Combat skills on XP Token Overrides page - Same overflow fix applied to the XP token editor's skill selector
- Wider level headers on item rewards page - The "Level X" section headers on the item rewards claim page are now wider to avoid text truncation
- Wider status labels on skill tree page - The AVAILABLE, LOCKED, and COMPLETE status labels on skill tree reward tiers are now wider to prevent text from being cut off
- Fixed arrow navigation buttons overlapping outside skill tree page - The prev/next skill arrows in the header were placed inside the container's title bar, causing them to extend beyond the page border. Navigation controls are now properly inside the content area
- Fixed arrow navigation buttons overlapping outside item rewards page - Same fix applied to the item rewards claim page where prev/next skill arrows extended beyond the container border
Technical Details
- Added
#MaxActiveQuestsRowtoAdminConfigPage.uiwith text field and save button, wired inAdminConfigPage.javaviamaxActiveQuestsInputfield,@MaxActiveQuestsInputcodec key, andapplyMaxQuestsaction handler - Added
ui.admin.max_active_quests_labellocalization key to all 9 language files - Changed
QuestDefaultsdaily_harvest objective fromBREAK_BLOCKtoPICKUP_ITEM - Fixed
manifest.jsonoptional dependency group ID fromHyFishing:HyFishingtoTheRedLotus:HyFishing - Added
IntegrationRegistry.resolveDeferredIfNeeded(): re-resolves inactive integration domains once on firstPlayerConnectEvent, fixing race where optional mods (e.g., HyFishing) load after MMO Skill Tree'ssetup() - Widened
RewardTierSeparator.ui#TierLabelfrom 100px to 140px - Widened
TierRow.uiandTierRowExpanded.ui#TierStatusfrom 100px to 140px MobKillEventSystem: fish mob kills always award Fishing XP when no fishing integration is active (regardless of combat mode). When a fishing integration is active, fish kills fall through to normal weapon kill XP. Kill XP inDAMAGE_PLUS_KILLmode requiresHostilityUtil.isHostileTowards()check;KILL_ONLYdoes notCombatXpEventSystem: removedDAMAGE_BASED/DAMAGE_PLUS_KILLbypass fromcanAwardXpcheck - all modes now require hostility viaHostilityUtil.isHostileTowards()(KILL_ONLY is unaffected as its switch case is empty)- Rewrote
BoostTemplateOverridesPage.uiandXpTokenOverridesPage.uito use$C.@Container,$C.@PageOverlay,$C.@ContentSeparator,$C.@PanelSeparatorFancy,ContainerPanelPatch.pngsection backgrounds,$C.@DefaultScrollbarStyle, and$C.@SmallSecondaryTextButton - Changed
#TargetSelectorfromLayoutMode: Left(height 28) toLayoutMode: LeftCenterWrap(height 64) on both pages to support wrapping when categories exceed container width - Fixed
XpTokenOverridesPage.buildTargetSelector()index mismatch: used separatebtnIndexcounter so skipped skills (viaisImplementedSkillcheck) don't produce out-of-bounds#TargetSelector[i]selectors - Removed manual
#CloseButtonevent bindings - Container's built-in@CloseButton = truehandles page dismissal - Moved
SkillTreePage.uiandItemRewardsPage.uinav buttons (#PrevSkill,#NextSkill) and skill info row from#Titleinto#Content; simplified#Titleto centered skill name only
v0.10.5
Health & Mana Regeneration
- Health Regen - New skill tree reward type that passively regenerates a percentage of your max health per second. Available in Defense (Lv65: +0.5%/s, Lv100: +1%/s) and gathering skill trees - Mining, Woodcutting, Excavation, Fishing, and Building (Lv80: +0.2%/s, Lv100: +0.3%/s)
- Mana Regen - New skill tree reward type that passively regenerates a percentage of your max mana per second. Available in Staves (Lv65: +0.5%/s, Lv100: +1%/s) and Harvesting and Crafting (Lv80: +0.2%/s, Lv100: +0.3%/s)
- Combat trees offer stronger regen rates as primary sources, while gathering trees provide lower supplementary regen for dedicated gatherers
Bug Fixes
- Harvest quest progress now works from interactive crop pickups - Harvesting crops via interactive pickup was awarding XP correctly but not advancing quest objectives. The pickup event now fires both
PICKUP_ITEMandBREAK_BLOCKquest events, so harvest quests (which useBREAK_BLOCKobjectives) track progress as expected - Fixed quest page failing to open - A layout error in the quest page UI file prevented it from rendering
Technical Details
- Added
HEALTH_REGENandMANA_REGENtoRewardTypeenum, registered as passive effects inRewardEffectRegistry - Added
SkillTreeService.getTotalHealthRegen()/getTotalManaRegen()for querying accumulated regen bonuses - Added
StatModifierUtil.restoreMana()for mana restoration (mirrorshealEntity()) - New
RegenTickingSystem(EntityTickingSystem) - ticks each frame, applies percentage-based health/mana regen scaled bydtto all players with regen rewards. Skips dead entities and entities at full health/mana - Updated exhaustive switch statements in
SkillReward.getFormattedValue()andMessages.getRewardFormattedValue()to include the new percentage-based types - Added
reward.health_regenandreward.mana_regenlocalization keys to all 9 language files - Added regen rewards to
SkillTreeDefaults: Defense tiers 7/9 getHEALTH_REGEN, Staves tiers 7/9 getMANA_REGEN - Added gathering regen rewards to
SkillTreeDefaults: Mining/Woodcutting/Excavation/Fishing/Building tiers 8/9 getHEALTH_REGEN(0.002/0.003), Harvesting/Crafting tiers 8/9 getMANA_REGEN(0.002/0.003)
v0.10.4
UI Improvements
- Buff bar shows equipped weapon stats - The combat stats in the buff bar (DMG, CRIT, STEAL) now reflect your currently equipped weapon instead of showing the highest value across all combat skills. The title shows which weapon skill is active (e.g., "BUFFS (Swords)"). Works with both built-in and custom combat skills
- Total Level card shows global combat buffs - The Total Level summary card now shows "All Combat" reward bonuses (Damage, Critical Chance, Lifesteal) alongside other global buffs. Per-skill and per-category combat rewards are excluded since they only apply with specific weapons. Luck and Parry bonuses remain excluded as they are always skill-specific
- Cleaner card metadata - Skill card metadata lines now show only tier progress (e.g., "Tier 3/10") instead of repeating XP and Luck stats that are already visible as buff labels
- Skill cards redesigned with native Hytale styling - Skill cards on the overview page now use the Hytale panel background with proper separators, a real progress bar widget with percentage label, and auto-wrapping buff labels. Cards are laid out in an auto-wrapping grid instead of fixed 3-per-row, adapting to the available space
- XP Overrides page redesigned with native Hytale styling - The XP Maps admin editor now uses the standard Hytale container layout with themed panels, scrollbars, separators, and button sounds, matching the rest of the admin dashboard
- Skill buttons now wrap in the XP editor - Categories with many skills (e.g., Combat with 10+ weapon types) now wrap to multiple rows instead of overflowing off-screen
- Wider buttons on Skill Tree and Item Rewards pages - Back and Reset buttons on the skill tree reward claim and item reward claim pages are now wider for easier clicking
- Close X icon on XP, Quests, and Boosts pages - The "CLOSE" text button at the bottom of the Skill Overview, Quests, and Boosts pages has been replaced with a native close X icon in the header, matching the standard Hytale UI pattern
Bug Fixes
- Custom combat skills now gain XP in damage-based and kill-based modes - Custom skills with melee combat triggers (e.g., a custom Katana skill) were always awarding Unarmed XP instead of the custom skill when using Damage, Damage+Kill, or Kill-Only combat XP modes. The weapon-to-skill lookup now checks both built-in and custom skill XP maps, using longest pattern match. Per-hit mode was already working correctly
- Custom skills now included in total level and leaderboard - Custom skills with XP maps configured were not counted in the Total Level card on the skill overview page, leaderboard totals, category totals, or the leaderboard skill breakdown text. All totals and rankings now correctly include custom skills alongside built-in skills
- Custom combat skills now receive correct combat rewards - Holding a weapon mapped to a custom combat skill (e.g., a custom Katana skill) was incorrectly applying Unarmed-specific damage bonuses, crit chance, and lifesteal instead of the global "All Combat" rewards. Custom combat skills now correctly receive only All Combat rewards from the skill tree, and the buff bar displays the custom skill name next to weapon stats
Technical Details
ViewXpPage.populateBuffsBar(): now acceptsStoreandRefparameters. Resolves equipped weapon viaCombatWeaponUtil.resolveCombatSkillFromItemId()(supports custom skills) and usesSkillTreeService.getDamageBonusForSkill()/getCriticalChanceForSkill()/getLifestealForSkill()for the equipped skill. Weapon skill name shown in#BuffsTitle(e.g., "BUFFS (Swords)") instead of per-stat suffixViewXpPage.buildTotalLevelCard(): buff entry loop skips per-skill reward types (LUCK), then queriesgetCombatRewardByExactTarget(skills, type, CombatTarget.ALL)for combat-targeted types (STAT_DAMAGE,CRITICAL_CHANCE,LIFESTEAL,PARRY_*) to show only the ALL-target portion. Metadata line simplified to just tier progressViewXpPage.buildSkillCard(): metadata line simplified to just tier progress (removed XP bonus and luck stats that were redundant with buff labels)ViewXpPage.ui:#BuffsTitlewidened (50→150) to accommodate weapon skill name;#BuffDamage/#BuffCrit/#BuffLifestealshrunk (85→75/90/95) since weapon name moved to titleMessages.getSkillNameById(): added!localized.isEmpty()guard -LocalizationConfig.get()returns""for missing keys, which was bypassing theSkillRegistrydisplay name fallback for custom skills- Added
ui.viewxp.buffs_titlei18n key to English, Italian, and Russian language files SkillCard.ui: rewritten - outer wrapper286x248with 5px padding, inner#CardInnerusesContainerPanelPatch.pngbackground. Category strip 2px→3px. Replaced flex-weight progress bar hack withProgressBar #CardProgresswidget +#CardPercentlabel.#CardBuffsRowchanged fromLayoutMode: LefttoLeftCenterWrap. Three#1e2a36separators between sections. Buttons widened (Tree 90px, Items 82px, Boost 60px)ViewXpPage.ui: added#SkillGridgroup withLayoutMode: LeftCenterWrapinside#SkillsListfor auto-wrapping card gridCardBuffLabel.ui: addedRight: 6to anchor for horizontal spacing when wrapped- Deleted
SkillCardRow.ui- replaced byLeftCenterWrapauto-wrapping ViewXpPage.java: removedSKILL_CARD_ROW_TEMPLATE/CARDS_PER_ROWconstants. Card loop now flat-appends to#SkillGrid. AddedprogressPercentfield toSkillCardData.populateSkillCard()usesProgressBar.Valueinstead of flex-weight hack, sets#CardPercenttext, targets#CardInner.Backgroundfor total card override. AddedgetNextRewardPreview()method usingMessages.getRewardDisplayText()CombatWeaponUtil: addedCombatSkillResultclass (wraps eitherSkillTypeor custom skill ID) and newresolveCombatSkillFromItemId()/resolveCombatSkill()methods that check both built-in and custom skill XP maps. AddedgetWeaponXpValue(CombatSkillResult, itemId)for weapon XP scaling lookup. Removed deprecatedgetCombatSkillFromItemId()/getCombatSkill()- all callers migrated toresolve*variantsCombatTarget.appliesTo(CombatSkillResult): new overload - custom combat skills match onlyALLtarget (no melee/ranged classification). Built-in skills delegate to existingappliesTo(SkillType)SkillTreeService: addedCombatSkillResultoverloads forgetDamageBonusForSkill(),getCriticalChanceForSkill(),getLifestealForSkill(),getCombatStatBreakdown(). Built-in skills delegate toSkillTypeoverloads; custom skills use newgetFilteredCombatReward(SkillComponent, RewardType, CombatSkillResult)that filters viaCombatTarget.appliesTo(CombatSkillResult)CombatDamageEventSystem: switched fromgetCombatSkill()toCombatWeaponUtil.resolveCombatSkill()returningCombatSkillResult.applyAttackerEffects()andapplyLifesteal()parameter changed fromSkillTypetoCombatSkillResult- method bodies unchanged,SkillTreeServiceoverloads resolve correctlyViewXpPage.populateBuffsBar():isCombatcheck now includes custom results (always combat since they matchedDEAL_DAMAGE_PHYSICAL). UsesMessages.getSkillNameById()for weapon label supporting both built-in and custom skill namesCombatXpEventSystem.handleDamageBasedXp(): switched fromgetCombatSkillFromItemId()toresolveCombatSkillFromItemId(), dispatches toaddCustomXp()oraddXp()based on resultMobKillEventSystem.handleKillXp(): same pattern - usesresolveCombatSkillFromItemId()and dispatches XP to the correct skill. Removed privategetWeaponXpValue()(now inCombatWeaponUtil)XpOverridesPage.ui: rewritten to use$C.@Containerwith@CloseButton,$C.@PageOverlay,@SectionCard,$C.@PanelSeparatorFancy,$C.@ContentSeparator,$C.@DefaultScrollbarStyle,$C.@SmallSecondaryTextButton, and$C.@ButtonSoundson all button stylesSkillButton.ui: addedRight: 4, Bottom: 4margin andPadding: (Horizontal: 8)for proper wrap spacing inLeftCenterWraplayoutXpPatternRow.ui: alignedPatternNamewidth (360→300) with table header columnXpOverridesPage.java: renamed#SkillTabs→#CategoryTabs, removed manual#CloseButtonevent binding (now handled by Container's built-in close), removed"close"action handlerViewXpPage.ui,QuestPage.ui,BoostPage.ui: wrapped two-panel layout in$C.@Container { @CloseButton = true; }with hidden#Title(Height: 0) and#Contentset toLayoutMode: Leftwith no padding. RemovedContainerPatch.pngbackground from#RightPanel(Container provides it). Removed bottom$C.@SmallSecondaryTextButton #CloseButton. Removed.Textsetter from corresponding Java pagesViewXpPage.buildTotalLevelCard(): card level and XP now add custom skill totals via existinggetCustomSkillsTotalLevel()/getCustomSkillsTotalXp()helpers (previously only the summary stats at the top included them)LeaderboardPage.PlayerLeaderboardEntry: addedcustomSkillLevels/customSkillXpmaps (keyed by custom skill ID) alongside the existingSkillType-keyed mapsLeaderboardPage.createEntryFromCache()andcreateEntry(): now populate custom skill data from cache/SkillComponent respectivelyLeaderboardPage.getCategoryLevelFromEntry()/getCategoryXpFromEntry(): now include custom skills matching each category via newgetEnabledCustomSkillsForCategory()helperLeaderboardPage.buildLevelBreakdown(): refactored to useBreakdownEntryrecord andcollectBreakdownEntries()helper, including both built-in and custom skills in the top-3 breakdown text- Removed duplicate
getImplementedCategoryLevel()/getImplementedCategoryXp()methods -createEntry()now reusesgetCategoryLevelFromEntry()/getCategoryXpFromEntry()since entry maps are already populated at that point
v0.10.3
Leaderboard Improvements
- Misc category now appears in leaderboard filters - The Misc category (Building and custom misc skills) was missing from the leaderboard category filters. A new "Misc" filter button lets you view and drill into Misc skill rankings
- All combat skills now visible as leaderboard skill filters - Combat has 11 skills but the filter bar only supported 10 buttons, cutting off Acrobatics. Increased to 12 filter slots
Quests
- Catch Fish quest objective - New
CATCH_FISHobjective type for quests. Tracks fish caught via fishing integrations (HyFishing) and fish mob kills when no integration is active. Use it in quest JSON to create fishing-themed quests - Built-in fishing quests - Four new default quests for the Fishing skill: a repeatable "Daily Catch" (catch 10 fish), and a three-part quest chain - "First Catch" (catch 5 fish), "Salmon Run" (catch 10 Salmon + reach Fishing 10), and "Deep Water Haul" (catch 5 Lobsters + 5 Crabs + reach Fishing 20)
Bug Fixes
- Fixed leaderboard page crashing when skills are disabled or permission-gated - Opening the leaderboard would fail if any skill in a category was disabled or the player lacked permission for it
- Total level and XP now exclude disabled skills - The "Total" level and XP shown on the leaderboard and XP page previously counted all skills, including ones that were disabled or had no XP data configured. Totals now only include enabled skills with active XP maps
Technical Details
LeaderboardPage.getSkillsForCategory(): fixedArrays.asList()returning unmodifiable list causingUnsupportedOperationExceptiononremoveIf()ingetSkillsForCategoryFiltered()- changed to mutableArrayList; addedMISCcase mapping toSkillCategory.MISCLeaderboardPage: addedMISCtoCategoryFilterenum withmiscLevel/miscXpfields inPlayerLeaderboardEntry,#FilterMiscUI button + event binding, MISC cases in sorting/display/styles. Increased skill filter buttons from 10 to 12 (#SkillFilter10,#SkillFilter11). Addedui.leaderboard.filter_miscto all 9 language filesSkillComponent.getSummedTotalLevel()andgetTotalXp(): now checkCustomSkillsConfig.isBuiltinDisabled()in addition to XP map emptiness.getTotalXp()changed from summing allxpMapvalues to iterating entries and filtering by XP map + disabled statusLeaderboardPage: addedgetEnabledSkillsForCategory()method that filters by XP map + disabled status (no player permissions). UpdatedgetImplementedCategoryLevel/Xp(),getCategoryLevel/XpFromEntry(), andbuildLevelBreakdown()to use it. Added disabled check togetSkillsForCategoryFiltered()ViewXpPage.getFilteredSkills(): addedCustomSkillsConfig.isBuiltinDisabled()check- Added
CATCH_FISHtoObjectiveTypeenum.QuestService.onCatchFish()uses nullableCommandBufferpattern (no buffer available from fishing adapters).QuestService.processEvent()refactored to accept@Nullable CommandBuffer.HyFishingAdapter.awardFishingXp()now passesfishItemIdand callsQuestService.onCatchFish().MobKillEventSystemalso firesCATCH_FISHfor fish mob kills (fallback path)
v0.10.2
Custom Skills
- Define your own skills - Server owners can now create fully custom skills beyond the 22 built-in ones. Custom skills have a unique ID, display name, description, category (Gathering, Combat, Crafting, or Misc), and trigger types - all configured through the admin UI or
mods/mmoskilltree/custom-skills.json - Custom Skills admin page - A new page accessible from the admin dashboard (
/mmoadmin> Custom Skills) lets you create, edit, and delete custom skills. Set the skill's display name, pick a category, toggle XP trigger types, and manage everything visually - Disable built-in skills - Any of the 22 built-in skills can be disabled from the Custom Skills page. Disabled skills are hidden from all UI pages, stop awarding XP, and are excluded from admin editors
- Full admin editor support - Custom skills appear alongside built-in skills in the Skill Tree Overrides, Command Rewards, and XP Maps admin editors. Add reward tiers, configure milestone commands, and set XP values for custom skills the same way you would for any built-in skill
- Skill tree tiers for custom skills - Add reward tiers to custom skills through the Skill Tree Overrides page. A "+" button lets you add new tiers, and tiers can be removed since custom skills have no built-in defaults
- Persisted in config - Custom skill definitions are saved to
mods/mmoskilltree/custom-skills.json. Skill tree tiers, command rewards, and XP maps for custom skills are saved to their respective override config files
Per-Skill Max Level Setting
- Configurable max levels - Server admins can now set a default max level for all skills and override it for individual skills. The default max level is 200. Set any skill higher or lower to fit your server's progression design
- XP stops at max level - Once a skill reaches its max level, no further XP is awarded. This applies to all XP sources: passive gains, XP tokens, boost multipliers, and the API
- Admin UI - A new "Max Levels" section on the Custom Skills page (
/mmoadmin> Custom Skills) lets you set the default max level and per-skill overrides visually. Skills with overrides are highlighted orange. Use "Apply All" to save changes or "Reset All" to clear overrides - Admin commands clamped - The
/setxpcommand automatically clamps values to each skill's max level XP threshold - Persisted in config - Max level settings are saved to
mods/mmoskilltree/custom-skills.jsonand survive server restarts
Fishing Skill Tree
- Parry rewards for Fishing - When Perfect Parries is installed, Fishing now includes Parry Reflect and Parry Stamina Drain rewards in its skill tree at levels 40-100, matching the same progression as combat skills
Multi-Skill Quest Requirements
- Multiple skill requirements per quest - Quests can now require players to meet multiple skill levels before accepting. For example, a quest can require both Mining 30 AND Crafting 20. All skill requirements must be met for the quest to unlock
- Backward compatible - Existing quests with a single skill requirement continue to work unchanged. The new array format is optional and only needed when gating on more than one skill
- UI shows all requirements - The quest log displays every skill requirement in the "Requires:" line, so players can see exactly what they need
Dynamic Quest Category Filters
- Quest filter buttons are now dynamic - The quest page category filter tabs (Main, Daily, Misc) are no longer hardcoded. Filter buttons are generated automatically based on which categories have enabled quests. Custom categories defined in quest JSON files (e.g., "seasonal", "events", "pvp") now get their own filter button
- Custom category colors - Custom categories get a unique accent color from a rotating palette, used on the category strip of each quest row
Bug Fixes
- Harvest quests now track interactive crop pickups - Previously, harvesting quests only counted crops broken directly. Crops picked up interactively (the primary harvest method) now also count toward quest progress
- Fishing now appears in leaderboard filters - Fishing was missing from the Gathering category on the leaderboard page
Technical Details
QuestDefinition: replacedminSkill/minSkillLevelfields withListwhereSkillRequirementis a new inner record(String skill, int level). Builder retainsminSkill()/minSkillLevel()convenience methods via pending fields that merge into the list inbuild(). NewskillRequirements(List)builder method for the array pathQuestConfig.parseQuest():requirementsfield now accepts both a single JSON object (backward compatible) and a JSON array of objects. Array format takes maxminLevelacross entries and collects allminSkill/minSkillLevelpairs into the skill requirements listQuestDefaults.questToJson(): serializes 0-1 skill requirements as a single object (backward compatible), 2+ as a JSON array with separate entriesQuestService:canAcceptQuest()(both null-quests and main branches) andisQuestVisible()now loop overgetSkillRequirements()instead of checking a single skillQuestPage.buildRequirementsText(): loops over all skill requirements, displaying each one in the "Requires:" lineCustomSkillsConfig: addeddefaultMaxLevel(int, default 200) andmaxLevels(Map, keyed by uppercase skill name) fields. New methods:getMaxLevel(skillName)returns override or default,setDefaultMaxLevel(),setMaxLevel(),removeMaxLevel(),setMaxLevels()(batch),clearAllMaxLevels(),hasMaxLevelOverride(),getMaxLevelOverrides().ConfigDatainner class extended with nullableInteger defaultMaxLevelandMap. No schema version bump (GSON handles missing fields as null)maxLevels SkillService.addXp(): after computingadjustedAmount, checkscurrentXp >= maxXp(skip XP, zero fractional remainder) or clampsadjustedAmountso total doesn't exceedgetXpForLevel(maxLevel). Same pattern inaddCustomXp()for custom skillsXpTokenConsumeInteraction.processFlatXpToken(): checks max level before consuming - returns false (don't consume token) if already at max, or clampsxpAmountto not exceed capSetXpCommand: both "all" and specific-skill paths clampvaluetogetXpForLevel(maxLevel)per skill before callingsetXp()MMOSkillTreeAPI.addXp(): enforces max level cap - returns false if already at max, clamps amount otherwiseCustomSkillsPage: newbuildMaxLevelsSection()method dynamically populates a 4-per-row skill grid fromSkillRegistry.allSkillIds()usingMaxLevelSkillRow.uitemplate, skipping disabled built-in skills. Labels colored orange for overridden skills. New event handlers:applyDefaultMaxLevel,applyAllMaxLevels(batch save, removes overrides matching default),resetAllMaxLevels,maxLevelInput(stores field value without refresh).CustomSkillsEventDataextended withdefaultMaxLevelInputandmaxLevelValuefieldsCustomSkillsPage.ui: new "Max Levels"@SectionCardsection at top of#ContentAreawith default max level row (label + field + button + hint),#MaxLevelGridcontainer, and action row (Apply All + Reset All + status label)PickupItemEventSystem: now fires bothQuestService.onPickupItem()andQuestService.onBlockBreak()soBREAK_BLOCKquest objectives also trigger on interactive item pickupsSkillTreeDefaults.getFishingDefaults(): tiers 5-9 converted fromList.of()to mutableArrayListwith conditionalisParryModActive()parry reward additions (PARRY_REFLECT + PARRY_STAMINA_DRAIN, +3%/+5%/+7% progression)LeaderboardPage.getSkillsForCategory(): addedSkillType.FISHINGtoGATHERINGcategoryQuestConfig.getActiveCategories(): new method returns all categories with enabled quests, sorted with built-in categories (main, daily, misc) first, then custom categories alphabeticallyQuestPage.java: replaced hardcoded#FilterMain/#FilterDaily/#FilterMiscsetup with dynamic loop overgetActiveCategories(). RemovedupdateCategoryFilterStyles(), addedapplyCategoryTabStyle()andgetCategoryDisplayName()helpers.getCategoryColor()extended with hash-based rotating palette for custom categoriesQuestPage.ui: replaced hardcoded category buttons with#DynamicCategoriesgroup populated at runtimeQuestCategoryTab.ui: new template for a single dynamic category filter button
Technical: Custom Skills System
CustomSkill: new data class withid,displayName,categoryName,description, andSet. ID auto-uppercased, category defaults toMISCSkillRegistry: singleton registry holding all custom skills alongside built-inSkillTypevalues.init(Listcalled at startup before config loading. Key methods:) isKnownSkill(),isCustomSkill(),getDisplayName(),getCategoryName(),allSkillIdsByCategory()(returns built-in + custom IDs matching a category)CustomSkillsConfig: loads/saves custom skill definitions and disabled built-in list fromcustom-skills.json. Supports add/update/remove/reorder operations.SkillRegistry.reloadFromConfig()called after mutationsCustomSkillsPage: admin UI page for CRUD operations on custom skills - form with ID, display name, description, category dropdown, and trigger type toggles. Built-in skills section with disable/enable toggle per skillSkillTreeConfig: 9 new*ByName(String skillId, ...)methods that accept string skill IDs and delegate to built-in (skillTrees/skillTreeOverrides) or custom (customSkillTrees/customSkillTreeOverrides) maps. Query:getNodeByName,getMaxTierByName,isDefaultByName(always false for custom),isTierDisabledByName,getDefaultNodeByName(null for custom). Mutation:setTierOverrideByName,disableTierByName,removeTierOverrideByName(removes tier + re-indexes for custom),addCustomSkillTierCommandRewardsConfig: 4 new*ByNamemethods -isDefaultByName,setRewardsByName,disableLevelByName,removeOverrideByName. Delegate to built-in or custom skill reward mapsXpMapsConfig: 4 new*ByNamemethods -getOverridesByName,getDefaultValueByName(null for custom),setXpByName,removeXpByName. Delegate to built-in or custom XP mapsSkillTreeOverridesPage,CommandRewardsPage,XpOverridesPage: refactored fromSkillTypeenum toString-based skill IDs. Removed staticSkillType[]category arrays,getSkillsForCategory()now returnsListviaSkillRegistry.allSkillIdsByCategory().buildSkillSelector()usesSkillRegistry.getDisplayName(). All config calls use*ByNamemethods.SkillTreeOverridesPageadds "+" button for custom skill tier creation- UI templates:
BuiltinSkillRow.ui,CustomSkillRow.ui,CustomSkillsPage.ui,MaxLevelSkillRow.ui- new templates for the custom skills admin page
v0.10.1 - Parry Rewards & Scroll Recovery Quest
Parry Skill Tree Rewards
- Parry rewards in combat skill trees - When the Perfect Parries mod is installed, 7 melee combat skills (Swords, Daggers, Polearms, Staves, Axes, Blunt, Unarmed) and Defense now include parry-themed rewards in their skill trees at levels 40-100
- 4 new reward types - Parry Counterattack (chance to counterattack after parrying), Parry Reflect (reflect a percentage of blocked damage), Parry Stamina Drain (drain attacker's stamina on parry), and Parry Stun Damage (bonus damage to stunned enemies)
- Reward flavor per weapon - Each weapon skill offers a unique pair of parry rewards that fit its playstyle: Swords get Counterattack + Reflect, Daggers get Counterattack + Stamina Drain, Polearms get Reflect + Stun Damage, Staves get Reflect + Stamina Drain, Axes get Counterattack + Stun Damage, Blunt gets Stun Damage + Reflect, Unarmed gets Counterattack + Stamina Drain, and Defense gets all four types
- Scaling with tiers - Parry bonuses start at 3% at level 40 and scale up to 7-8% at level 100, with two parry options available at the final tier
- No mod, no clutter - Parry rewards only appear in skill trees when Perfect Parries is detected at startup. Servers without the mod see the same reward trees as before
- API access - Other mods can query a player's total parry bonuses via
MMOSkillTreeAPI.getParryCounterattackBonus(),getParryReflectBonus(),getParryStaminaDrainBonus(), andgetParryStunDamageBonus()
Leaderboard
- Redesigned leaderboard page - The leaderboard now uses the standard Hytale container layout with a proper header bar, themed scrollbar, decorative separator above your rank section, and native close button styling. Filter buttons are wider for better readability across all languages
UI Improvements
- Native Hytale styling across all pages - All UI pages now use the standard Hytale Common.ui layout system with themed containers, scrollbars, separators, and button sounds for a consistent look and feel
- Admin dashboard sticky footer - The admin config page's action buttons and back button now stay fixed at the bottom of the screen instead of scrolling with the content
- Boost multiplier precision - XP boost multipliers now display up to 3 decimal places (trimming trailing zeros) instead of being locked to 1 decimal place. A 1.125x boost now shows as "1.125x" instead of "1.1x"
Bug Fixes
- Skill tree tiers with more than 6 reward options now wrap to a second row - Previously, only the first 6 options were shown. Tiers with 7+ choices (e.g., from mod integrations adding extra rewards) now display all options across two rows
Quests
- "Forgot Something?" recovery quest - A new repeatable quest that gives players a replacement Skills Menu Scroll. Hidden until the Getting Started quest is completed, available once per hour, and must be manually accepted from the quest log
Technical Details
- New
IntegrationDomain.PARRIESenum value andParriesIntegration/NoOpParriesIntegrationinterfaces in the API subproject PerfectParriesAdapter: detection-only adapter usingPluginManager.hasPlugin()- simpler thanHyFishingAdaptersince the parry mod queries MMOSkillTree's API rather than needing event subscriptionsIntegrationRegistry: registersPerfectParriesAdapter, addsgetParriesIntegration()accessor andhasActiveIntegration()checkRewardType: 4 new enum values -PARRY_COUNTERATTACK,PARRY_REFLECT,PARRY_STAMINA_DRAIN,PARRY_STUN_DAMAGERewardEffectRegistry: parry reward types registered as passive effects (values queried by the parry mod via API, not applied as entity stat modifiers)SkillRewardandMessages: parry types added to percentage-format switch casesSkillTreeDefaults: combat skill tree methods (Swords, Daggers, Polearms, Staves, Axes, Blunt, Unarmed, Defense) useisParryModActive()to conditionally add parry rewards to tiers 5-9. Lists converted fromList.of()to mutableArrayListfor conditional appendingSkillTreeService: 4 new static methods -getTotalParryCounterattack(),getTotalParryReflect(),getTotalParryStaminaDrain(),getTotalParryStunDamage()MMOSkillTreeAPI: 4 new public methods exposing parry bonuses for external modsMMOSkillTreePlugin: reloadsSkillTreeConfigafter integration resolution when Parries integration is active, so parry rewards are included in the defaults- Parry reward localization keys added to all 9 language files (English, French, Spanish, German, Russian, Italian, Portuguese, Turkish, Hungarian)
QuestDefaults: added "forgot_something" quest - hidden, requires "getting_started" prerequisite, repeatable with 3600s cooldown, not auto-acceptedLeaderboardPage.ui: migrated from raw Group layout to Common.ui@Containerpattern - uses$C.@PageOverlay,$C.@Containerwith@CloseButton = true,$C.@DefaultScrollbarStyle,$C.@PanelSeparatorFancy,$C.@SmallSecondaryTextButton,$C.@TitleStyle,$C.@SubtitleStyle,$C.@DefaultLabelStyle,$C.@ButtonSounds, andContainerPanelPatch.pngbackgrounds for table header and rank sectionLeaderboardPage.java: removed#CloseButton.Textset since Container's built-in close button is an icon-only Button, not a TextButton- UI migration to Common.ui patterns for 7 additional pages:
ViewXpPage.ui,BoostPage.ui,QuestPage.ui,SkillTreePage.ui,ItemRewardsPage.ui,CommandRewardsPage.ui,AdminConfigPage.ui- each now uses$C.@PageOverlay,$C.@DefaultScrollbarStyle,$C.@ContentSeparator,$C.@ButtonSounds, label spread syntax (...$C.@DefaultLabelStyle), and where applicable$C.@Containerwith@CloseButton = true,$C.@PanelSeparatorFancy,$C.@SmallSecondaryTextButton, andContainerPanelPatch.pngbackgrounds SkillTreePage.java,ItemRewardsPage.java: removed#CloseButton.Textsets since Container's built-in close button is icon-onlyAdminConfigPage.ui: restructured#Contentfrom single scrollable area toLayoutMode: Topwith aFlexWeight: 1scrollable#ContentAreachild, keeping action buttons and back button as non-scrolling siblingsSkillDisplayUtils.formatMultiplier(): new utility method formatting doubles with up to 3 decimal places, trimming trailing zeros- 14 occurrences of
String.format("%.1f", ...)replaced withSkillDisplayUtils.formatMultiplier()acrossBoostPage.java,ViewXpPage.java,AdminConfigPage.java,BoostCommand.java,XpBoostService.java,ActiveBoost.java,BoostToken.java,BoostPermission.java TierRow.ui: added hidden#ChoicesContainer2second row (Height: 0 by default).SkillTreePage.java:MAX_CHOICESincreased from 6 to 12, newCHOICES_PER_ROW = 6constant - choices beyond 6 are appended to the second container, tier row height dynamically expanded from 90 to 146
v0.10.0 - Fishing Skill, Mod Integrations & White Label Fixes
Fishing Skill
- Fishing is now active - By default, killing fish and aquatic mobs awards Fishing XP. Minnows, Salmon, Crabs, Sharks, and more all grant Fishing XP based on their difficulty - no additional mods required
- Automatic fish detection - The plugin recognizes 18+ aquatic mob types automatically. When a fish mob is killed, it awards Fishing XP instead of combat weapon XP
- Fishing Mastery Cape - Reach Fishing level 100 to earn the Fishing Mastery Cape with item rewards at every milestone (levels 10, 25, 50, 65, 80, 100)
- Fishing XP Tokens - 8 flat XP token tiers and 5 personal boost token tiers for Fishing, matching all other skills
- HyFishing mod support - When HyFishing is installed, fish catches from HyFishing's fishing rod system award Fishing XP instead of mob kills. Fish mob kills fall back to combat weapon XP so there's no double-dipping
- Default fish XP values - Fish XP scales with difficulty: tiny fish (10 XP), small fish (20-24 XP), standard fish (30-35 XP), medium catches (40-50 XP), large catches (60-75 XP), hostile aquatic (90-100 XP), dangerous aquatic (150-200 XP)
- Configurable - Override fish XP values via
/mmoconfig fishingormods/mmoskilltree/xp-maps.json - Fishing skill tree - 10-tier reward tree with health, stamina, block, mana, and XP bonuses. High-tier rewards include all-combat lifesteal at levels 80 and 100, plus fall damage reduction at level 100
Admin UI
- Fishing in admin pages - Fishing now appears under the Gathering category in all admin pages: Command Rewards, Skill Tree Overrides, Boost Templates, and XP Tokens. Edit fishing rewards, milestones, and configurations from the dashboard
Optional Mod Integrations
- Automatic mod detection - MMO Skill Tree can now detect and integrate with other installed mods automatically. No server configuration required - adapters are discovered at startup
- Fishing integration support - Other mods can provide fishing adapters that hook into MMO Skill Tree's XP system. When a compatible fishing mod is installed, fish catches automatically award Fishing XP
- Multi-mod fallback - If multiple mods provide adapters for the same feature, the highest-priority adapter that initializes successfully is used. If none succeed, the system falls back to a safe default
- Server owner overrides - Optionally disable or re-prioritize mod adapters via
mods/mmoskilltree/integrations.json. The file is generated automatically on first startup - no editing needed unless you want to customize - Startup logging - The full integration resolution process is logged at startup with
[Integrations]prefix, showing which adapters were discovered, which were activated, and why others were skipped - Developer API - Third-party mod authors can build integration adapters by depending on the lightweight
mmoskilltree-apijar (compile-only). See the Integration API docs for details
Combat XP
- Damage-based modes skip hostility check - In Damage and Damage+Kill combat XP modes, all mobs now award XP when hit, regardless of hostility. Previously, only hostile mobs awarded offensive XP. Per-Hit and Kill-Only modes still require the target to be hostile
Bug Fixes
- Fixed a disconnect when opening UI pages with White Label branding enabled
- Server name and description now display correctly for both left and right branding positions. Left position shows the logo, server name, and description in the side panel without overriding page titles. Right position shows the logo in the title area with your server name as the heading and description below it
Technical Details
CombatXpEventSystem:DAMAGE_BASEDandDAMAGE_PLUS_KILLmodes now bypassHostilityUtil.isHostileTowards()check, awarding offensive XP for any mob hit.PER_HITandKILL_ONLYmodes retain the hostility gateCommandRewardsDefaults: addedgetFishingDefaults()with milestone rewards at levels 10/25/50/65/80/100 including Fishing Mastery Cape at 100- Added
mmoskilltree.skill.fishingpermission node tomanifest.json - Added
reward.cape.fishinglocalization key to all 9 language files - Added 13 Fishing XP token item definitions (8 flat + 5 personal boost) and
Cape_Skill_Fishing.jsonto Server resources SkillType.FISHINGnow hasKILL_ENTITYtrigger in addition toCATCH_FISHFishMobPatterns: static set of 18 aquatic mob identifier substrings withisFishMob(String)substring matchingFishingDefaults: default XP values for fish/aquatic mobs, tiered by difficulty (10-200 XP)SkillTreeDefaults: addedgetFishingDefaults()with 10 tiers - health/stamina/defense/mana/XP bonuses,CombatTarget.ALLlifesteal at tiers 8-9, fall damage reduction at tier 9HyFishingAdapter: fixed XP lookup to useXpMapsConfig.getXpMap(FISHING)with longest-substring match instead ofMobKillXpConfig- Added
SkillType.FISHINGtoGATHERINGarray in CommandRewardsPage, SkillTreeOverridesPage, BoostTemplateOverridesPage, and XpTokenOverridesPage MobKillEventSystem: fish mob kills checkFishMobPatterns.isFishMob()before combat XP. When no fishing integration is active, awards FISHING XP (raw mob XP, no weapon scaling) and returns early. When integration is active, falls through to combat weapon XPHyFishingAdapter: reflection-based adapter for HyFishing mod. UsesProxy.newProxyInstance()to subscribe to HyFishing's event API without compile-time dependencies. Resolves players by UUID viaUniverse.get().getPlayers(), dispatches XP awards to the world thread- New
apisubproject producingmmoskilltree-api-1.0.0.jar- contains only integration interfaces, annotations, and data classes with zero plugin dependencies (only JSR-305) - API classes in
com.ziggfreed.mmoskilltree.integration:OptionalIntegration(base interface),FishingIntegration(domain interface),IntegrationDomain(enum),IntegrationMeta(annotation withdomainandpriorityfields),NoOpFishingIntegration IntegrationRegistry: singleton using explicit adapter registration (no ServiceLoader - Hytale mod classloaders break META-INF/services discovery). Reads@IntegrationMetaannotation for domain/priority/plugin identity. Groups by domain, sorts by effective priority, tries eachinitialize()until one succeeds. SkipsPluginDetectorgate - adapters self-detect viaClass.forName()ininitialize()PluginDetector: reflection-safe plugin detection viaPluginManager.get().hasPlugin(new PluginIdentifier(group, name)). Caches reflection handles, returns false if API unavailable. Currently bypassed in favor of adapter self-detectionIntegrationsConfig: override-only config atmods/mmoskilltree/integrations.json. Keys are fully qualified adapter class names. Supportsenabled(boolean) andpriority(integer) overrides- Integration lifecycle hooks added to
MMOSkillTreePlugin:resolve()during startup,shutdownAll()during shutdown - Removed references to non-existent
#ServerNameUI element from BoostPage, QuestPage, ViewXpPage, LeaderboardPage, and SettingsPage - Added
#BrandingServerNameand#BrandingDescriptionlabels to left-panel.uifiles (ViewXpPage, BoostPage, QuestPage) for left branding mode - server name and description display in the side panel without overriding page titles - Added
#BrandingDescriptionRightlabel to all 5 page.uifiles for right-mode description display below the header - Wrapped
#PanelTitlein#TitleContainergroup on QuestPage.ui and BoostPage.ui so the branding template can be appended in right mode - Right mode:
serverNamereplaces#PanelTitle/#SettingsTitletext;serverDescriptionshown via#BrandingDescriptionRight - Left mode: page titles unchanged; branding (logo, name, description) contained entirely within the left panel
- BoostPage and QuestPage now support both left and right branding positions (previously only left)
v0.9.0 - Combat XP Modes & Item Requirements
New Combat XP Modes
- Damage-based combat XP - New combat XP mode where offensive weapon skills gain XP equal to damage dealt (1:1 ratio). A hit dealing 15 damage awards 15 XP. Simple and intuitive progression that scales naturally with weapon power
- Damage+Kill hybrid mode (new default) - Combines damage-based XP with mob kill bonuses. You earn 1 XP per damage dealt during combat, plus bonus XP when you land the killing blow (from mob-kill-xp.json with optional weapon scaling). This is now the default mode for new servers, providing more satisfying combat progression
- Four combat XP modes available - Choose from Damage (1:1), Damage+Kill (hybrid), Per-Hit (traditional weapon XP maps), or Kill-Only (XP only on kills) in the admin dashboard under General Settings
Item Level Requirements
- Skill-gated items - Weapons, tools, ores, and gems can now require specific skill levels to use. Players who don't meet the requirements see a red notification and cannot mine restricted blocks or deal damage with restricted weapons
- Tool requirements enforced when mining - When breaking a block, both the block AND the held tool are checked for level requirements. Using a high-tier pickaxe you don't meet the requirements for will prevent mining
- Multiple skill requirements - Items can require multiple skills with different levels (e.g., Mining 50 AND Smithing 30) plus an optional total level requirement
- Pattern-based matching - Requirements use the same longest-substring pattern matching as XP maps, so patterns like
Ore_Mithrilmatch all mithril ore variants - Default ore progression - Ores are gated by Mining level out of the box: Copper (1), Iron (10), Silver (20), Gold (30), Cobalt (40), Mithril (50), Adamantite (60), Thorium (70), Onyxium (80). Gems require Mining 50-90
- Default tool/weapon progression - Tools and weapons are gated by their respective skills (10 tiers from level 1 to 80)
- Admin UI page - Configure item requirements from the admin dashboard via the new "Item Requirements" button. Browse by category (Tools, Weapons, Armor), view defaults vs overrides, add/edit/disable patterns
- Enable/disable toggle - The entire system is disabled by default. Enable it from the admin UI or config when ready
- Override-based config - Only your customizations are saved to
mods/mmoskilltree/item-requirements.json. Defaults are preserved across updates
Tool & Weapon Requirements
- Tool-gated blocks - Blocks can now require specific tool types to break. Players who try to mine ores without a pickaxe, or chop logs without an axe, see a red notification and the action is cancelled
- Weapon-gated mobs - Mobs can require specific weapon types to damage. Server owners can configure special enemies that require specific weapons (e.g., ethereal enemies needing magic staves, armored enemies needing blunt weapons)
- Default block tool requirements - Out of the box, ores and rocks require pickaxes, logs require axes, and soil/dirt/sand blocks require shovels
- Pattern-based matching - Tool requirements use the same longest-substring pattern matching as other systems.
Tool_Pickaxe_matches all pickaxe variants regardless of material tier - Separate from level requirements - Tool requirements check what TYPE of tool you need (pickaxe vs axe), while item level requirements check your SKILL LEVEL to use that tool. Both systems work together
- Admin UI page - Configure tool and weapon requirements from the admin dashboard via the new "Tool Requirements" button. Browse block tool requirements and mob weapon requirements in separate tabs, add/edit patterns with comma-separated tool lists
- Enable/disable toggle - The entire system is disabled by default. Enable it from the admin UI or config when ready
- Override-based config - Only your customizations are saved to
mods/mmoskilltree/action-requirements.json. Defaults are preserved across updates
Admin UI
- Boost template batch controls - New "Disable All" and "Enable All" buttons on the Boost Templates page let you quickly disable or enable all templates for the selected target/category
Technical Details
- New
CombatXpModeenum values:DAMAGE_BASED(1:1 damage XP) andDAMAGE_PLUS_KILL(damage + mob kill XP). Default mode changed toDAMAGE_PLUS_KILL CombatXpEventSystem:handleDamageBasedXp()method awards XP equal to damage dealt for bothDAMAGE_BASEDandDAMAGE_PLUS_KILLmodes. UsesCombatWeaponUtil.getCombatSkillFromItemId()to determine skillMobKillEventSystem: awards kill XP forDAMAGE_PLUS_KILLmode (in addition to existingKILL_ONLYsupport)- New
ItemRequirementsConfig: override-based singleton config atmods/mmoskilltree/item-requirements.json. Schema version 1.enabledfield defaults tofalse. Pattern lookup viagetRequirement(itemId)uses longest substring match - New
ItemRequirementsDefaults: default requirements for tools (Mining/Woodcutting/Excavation/Harvesting), weapons (combat skills), ores (Mining 1-80), and gems (Mining 50-90) - New
ItemRequirementrecord:Map+skillRequirements int totalLevelRequiredwith factory methodsskill(),skills(),totalLevel(),skillsAndTotal() - New
ItemRequirementService: staticcheckRequirement(store, ref, itemId)returnsCheckResultwithmeetsRequirement(),getFailureMessage(),getMissingSkillLevels(),getMissingTotalLevels() BreakBlockEventSystem: checks requirements for both target block AND held tool viagetHeldItemId(). Callsevent.setCancelled(true)when requirements not metCombatDamageEventSystem: checks weapon requirements viagetHeldWeaponId()before applying damage effects. Sets damage to 0 and notifies player when requirements not met- New
ItemRequirementsPage: admin UI page with category tabs, pattern list, requirements input field (format:SKILL:LEVEL,SKILL:LEVEL,TOTAL:LEVEL), enable/disable toggle - Added
notify.item_requirement_not_metlocalization key to all 9 language files BoostTemplateOverridesPage: added#DisableAllBtnand#EnableAllBtnwith handlers that iterategetTemplateIds()and callconfig.setDisabled()for each- New
ActionRequirementsConfig: override-based singleton config atmods/mmoskilltree/action-requirements.json. Schema version 1.enabledfield defaults tofalse. Separate maps for block tool requirements and mob weapon requirements - New
ActionRequirementsDefaults: default tool requirements for blocks (ores/rocks → pickaxes, logs → axes, soil/sand/gravel → shovels). Mob weapon requirements empty by default (all weapons work on all mobs) - New
ActionRequirementrecord:ListwithallowedPatterns matches(heldItemId)method using substring matching. Factory methodsnone(),requiring(String...) - New
ActionRequirementService: staticcheckBlockToolRequirement(blockId, heldItemId)andcheckMobWeaponRequirement(mobId, heldWeaponId)returningCheckResultwithmeetsRequirement(),getFailureMessage(),getRequiredToolsSummary() - New
ActionRequirementsPage: admin UI page with "Block Tools" and "Mob Weapons" tabs, pattern list, target/tools input fields, enable/disable toggle. Accessible via "Tool Requirements" button in admin dashboard BreakBlockEventSystem: checks tool type requirements viaActionRequirementService.checkBlockToolRequirement()before checking item level requirements. Callsevent.setCancelled(true)when tool type not metCombatDamageEventSystem: checks weapon type requirements viaActionRequirementService.checkMobWeaponRequirement()usingEntityIdentifierUtil.getMobId()before checking weapon level requirements. Sets damage to 0 when weapon type not met- Added
notify.tool_requirement_not_metandnotify.weapon_requirement_not_metlocalization keys to all 9 language files
v0.8.15
Admin UI
- White-label configuration page - Server owners with the WHITE_LABEL entitlement can now edit branding settings directly from the admin UI. Access via the "White Label" button in the Actions section. Configure server name, description, branding position, accent color, and credit visibility without editing JSON files
- Entitlement-gated admin sections - Boost configuration sections (limits, stack mode, templates, award boost) are now hidden in the admin UI unless the server has the PERMISSION_BOOSTS entitlement active
Improvements
- Branding extended to all pages - Custom server branding now appears on the Quests, Boosts, Leaderboard, and Settings pages when branding position is set to "left"
- Consistent left panel styling - Settings button in Quest and Boost pages now matches the tab styling used in the main Skills Overview page
v0.8.14
New Quests
- Getting Started - New introductory quest that rewards a Skills Menu Scroll for breaking your first block. Auto-accepts and auto-claims to welcome new players. The crafting recipe for the Skills Menu Scroll has been removed in favor of this quest
Configuration
- Max active quests setting - New
maxActiveQuestsconfig option limits how many quests a player can have active at once (default: 10)
Improvements
- Quest rewards now check inventory space - Autoclaim quests will wait in "ready to claim" status if your inventory is full, and manual claims will be blocked until you free up space
- Settings moved to sidebar - The Settings button is now in the left panel navigation list instead of at the bottom
- Better reusable boosts messaging - When reusable boosts are not enabled on the server, shows a clear message instead of "No reusable boosts available"
v0.8.13
Custom Server Branding
- White-label branding support - Server owners with the WHITE_LABEL entitlement can now customize the skill menu with their own branding. Configure your server name in
mods/mmoskilltree/white-label.jsonand add your logo as an asset to replace the default header
New Quests
- Bear Bounty - New daily quest to slay 20 bears, rewarding 2 Building XP Shards. Resets every 24 hours
Bug Fixes
- Fixed Block XP from fire damage - Players no longer gain Block XP from taking fire damage
v0.8.12
New Items
- Skill Tree Reset Scroll - A consumable item that resets all skill tree reward selections. Hold for 3 seconds to reset your reward choices across all skills, allowing you to pick new rewards. XP and levels are preserved - only your reward selections are cleared. The scroll is consumed on use
Bug Fixes
- Fixed non-fatal errors on startup for invalid item qualities - Removed invalid quality JSON files that were causing errors during server startup
- Fixed combat XP exploit with invulnerable entities - Players can no longer farm combat XP by attacking invulnerable entities
Technical Details
- New
MmoSkillAllResetInteractioninteraction handler registered asmmo_skill_all_reset- extendsSimpleInstantInteraction, callsSkillTreeService.removeAllStatModifiers()thenskills.resetAllRewards()to clear all reward selections - Added
Mmo_Reset_Skills_Scroll.nameandMmo_Reset_Skills_Scroll.descriptiontranslations to all 9 language item files - Added
notify.skill_tree_resetlocalization key to all 9 language defaults files
v0.8.11
New Items
- Skills Menu Scroll - A new consumable item that opens the Skills menu when used. Left-click to instantly open your skills page without typing commands. The scroll is reusable and not consumed on use
Technical Details
- New
MmoMenuOpenInteractioninteraction handler registered asmmo_menu_open- extendsSimpleInstantInteraction, opensViewXpPagefor the player on Primary interaction - Added
Mmo_Menu_Scroll.nameandMmo_Menu_Scroll.descriptiontranslations to all 9 language files
v0.8.10
Boost Permissions Overhaul
- Boost permissions now use native permission checks - The custom
permissions.jsonfile-based boost permission system has been replaced with nativeplayer.hasPermission()checks, ensuring compatibility with any permission mod like LuckPerms or HyperPerms - 84+ built-in boost permission nodes - A comprehensive set of boost templates is now available out of the box, covering all-skills, category, per-skill, and global boosts at various multipliers and durations. Server owners grant the corresponding permission nodes through their preferred permission mod
- Removed
/mmoconfig reloadpermscommand - No longer needed since permissions are checked live through the native permission system - Boost page refresh no longer requires file reload - The Refresh button on the Boosts page now re-renders instantly, checking permissions in real time
Configurable Boost Templates
- Boost templates are now a proper override-based config - All boost permission templates are stored in
mods/mmoskilltree/boost-templates.jsonusing the same override pattern as XP tokens. Only customizations are persisted; defaults are always built in - Custom boost nodes require registration - The mod only recognizes boost nodes that are registered as templates. To create a custom boost with values not in the 124 defaults, use the "Add" button in the admin UI - simply granting an unregistered node via a permission mod will not work
- New admin UI page for boost templates - Browse, add, disable, and delete boost templates from the admin config page. Accessible via the "Boost Templates" button in the Actions section. Includes a copyable permission string field for pasting directly into permission mod commands
- Add custom templates - Create new boost templates with any multiplier, duration, and cooldown combination. Select a target and scope, enter the values, and click Add - the new permission node is generated automatically and appears in the list
- Delete custom templates - Remove custom templates you no longer need. Default templates cannot be deleted (use Disable instead)
- Disable specific boost templates - Disable any boost template to prevent players from using it, even if they have the permission node
- Trim redundant overrides - Clean up overrides that match their default values with a single button click
Offline Player Support for Bulk XP
/bulksetxpnow queues changes for offline players - When targeting*(all players) or naming specific players who aren't online, XP changes are saved and applied automatically when they next join the server- Queued resets apply on login - Pending XP changes are applied in order, rewards are validated (revoked if levels dropped), and the leaderboard is updated - all before stat effects are reapplied
- Persistent queue - Pending XP changes survive server restarts, stored in
mods/mmoskilltree/pending-xp-resets.json
Bug Fixes
- Fixed skill tree rewards showing duplicated values - Rewards on the skill tree page were displaying doubled text like "+10% +10% Mining XP" instead of "+10% Mining XP". The display text for custom reward names is now shown as-is without prepending the formatted value again
Technical Details
XpBoostService: removedbuildDefaultTemplates()method (hardcoded permission strings).initTemplates()now reads fromBoostTemplateConfig.getInstance().getAllDefinitions(), converts non-disabled definitions toBoostPermissionviatoBoostPermission(), and populatesavailableTemplates- New
BoostTemplateConfig: singleton override-based config atmods/mmoskilltree/boost-templates.json. Schema version 1. Same load/save pattern asXpTokenConfig- mergesBoostTemplateDefaultswith user overrides - New
BoostTemplateDefaults: staticgetAllDefaults()producesLinkedHashMapfrom permission node strings (previously inXpBoostService.buildDefaultTemplates()) - New
BoostTemplateDefinition: immutable value object wrappingBoostPermissionwithwithOverrides()andtoBoostPermission()methods for config round-tripping - New
BoostTemplateOverridesPage: admin UI page with Personal/Global scope tabs, category/skill target selectors, and 7-column table (DEF MULT/DUR/CD, CUR MULT/DUR/CD, STATUS). Edit section supports multiplier, duration, and cooldown fields. All changes triggerXpBoostService.initTemplates()to reload templates MMOSkillTreePlugin.setup(): addedBoostTemplateConfiginitialization beforeXpBoostService.initTemplates()AdminConfigPage: added#EditBoostTemplatesBtnbutton andeditBoostTemplatesaction handler- Added
ui.admin.edit_boost_templatesandui.boosttemplates.titlelocalization keys to all 9 language files - New
PendingXpResetStore: file-based singleton store atmods/mmoskilltree/pending-xp-resets.json, keyed by lowercase username. Each entry is a list ofPendingXpResetobjects (skill+value). Same load/save/drain pattern asPendingQuestRewardStore BulkSetXpCommand: after applying to online targets, iteratesLeaderboardDataStore.getAllPlayers()(for*) or the named target list to queue resets for any player not found online. Reports queued count separately in yellowMMOSkillTreePlugin:PendingXpResetStoreinitialized and loaded insetup(), saved in shutdown hook.PlayerReadyEventhandler drains pending resets beforereapplyStatRewards()- sets XP per operation, then runsSkillTreeService.validateAllRewards()andCommandRewardService.validateAllCommandRewards()to revoke rewards where levels dropped
v0.8.8
Quest Visibility System
- Hidden quests - Quests can now be configured to stay invisible in the UI until specific conditions are met. Hidden quests automatically appear once the player starts them (via auto-accept or admin commands)
- Permission-gated visibility - Hide quests until the player has a specific permission, useful for secret quest chains or premium content
- Prerequisite-gated visibility - Hide quests until prerequisite quests are completed, preventing players from seeing spoilers for quest chains they haven't reached yet
- Level-gated visibility - Hide quests until the player meets the level requirements, keeping the quest list focused on achievable content
Quest Page Filters
- Quest search - Filter quests by typing part of a quest name or description. Click Search to apply or the X button to clear. Case-insensitive matching against both display name and description text
- Status filter buttons - Quickly filter quests by status: All, Active, Available, or Completed. Filter state is preserved when accepting, claiming, or abandoning quests
New Default Quests
- 8 new default quests covering excavation, combat, crafting, woodcutting, harvesting, and mining progression:
- Dirt Digger (main) - Dig 200 soil blocks for Excavation XP
- First Blood (main) - Slay 5 Trorks for Defense XP
- Apprentice Crafter (main) - Craft 20 items and reach Crafting level 5
- Lumberjack's Path (main) - Chop Oak and Birch wood, reach Woodcutting level 10
- Big Game Hunter (main) - Slay Bears and Outlanders. Requires First Blood and total level 25
- Harvest Season (daily) - Harvest 40 crops on a 24-hour cooldown
- Undead Cleanup (daily) - Slay 15 Zombies on a 6-hour cooldown
- Gem Collector (misc) - Mine 10 each of Diamonds, Emeralds, and Rubies. Requires Mining level 30
Bug Fixes
- Fixed custom reward names not displaying on the skill tree page - When a skill tree reward's display name was overridden in config, the skill tree page still showed the default auto-generated name instead of the custom one
Technical Details
- New
QuestVisibilitydata class withhidden,permission,requirePrerequisites, andrequireLevelfields. Parsed from"visibility"JSON object in quest definitions QuestDefinitionnow uses a Builder pattern instead of a 17-parameter constructor. Old constructor removed; all creation goes throughQuestDefinition.builder(id, objectives).displayName(...).build()QuestService.isQuestVisible(): new method evaluating visibility conditions. Already-started quests (ACTIVE, COMPLETED_UNCLAIMED, COMPLETED, ON_COOLDOWN) always visible. Whenhidden=truewith no specific conditions, falls back tocanAcceptQuest()for backward compatibilityQuestPage: addedfilterStatusandsearchTextfields to constructor.getFilteredQuests()now applies visibility, text search, and status filter stages.QuestEventDatacodec extended withStatusandSearchInputkeyed fields. All page reopens (accept/claim/abandon/filter) preserve filter stateQuestConfig.parseQuest(): parses"visibility"JSON object intoQuestVisibility. Newmining_expertdefault quest demonstrates hidden+prerequisite visibility- Extracted
QuestDefaultsclass fromQuestConfig- default quest definitions use theQuestDefinition.builder()API instead of positional parameters.questToJson()serializes anyQuestDefinitionto JSON for file output QuestCommand.handleList(): appends[HIDDEN]tag for quests withvisibility.hidden=trueMessages.getRewardDisplayText(): added early return whenreward.getDisplayName()differs fromreward.getType().getDisplayName(), using the overridden name with formatted value instead of auto-generating from reward type and skill ID- Added 6 localization keys (
ui.quests.search_placeholder,ui.quests.search_btn,ui.quests.filter_status_all/active/available/completed) to all 9 language files
v0.8.7
New Weapon Defaults
- Added Kurabiye's Longbows mod support - All 10 weapons from the Kurabiye's Longbows mod now have default Archery XP values: 9 longbow variants matching their material tier (Adamantite, Cobalt, Thorium, Iron, Copper, Crude, Flame, Frost, Poison) and the Kurabiye bow
Bug Fixes
- Fixed skill tree rewards showing 0% instead of fractional percentages - Rewards with decimal percentage values (e.g., 0.5% Critical Chance) were displayed as 0% on the skill tree page. Fractional percentages now display correctly with one decimal place (e.g., "+0.5%"), while whole-number percentages remain unchanged (e.g., "+5%")
- Broken weapons no longer award combat XP - Attacking with a broken weapon (0 durability) was still granting XP for the weapon's skill. Broken weapons now award no combat XP
- Quest level objectives now refresh when opening the Quests page - Quests with "reach level" objectives could appear incomplete even after the player exceeded the required level, if the XP was gained through admin commands, tokens, or other means that didn't trigger a quest event. Opening the Quests page now re-evaluates all level-based objectives against current skill levels
Technical Details
Messages.getRewardFormattedValue()andSkillReward.getFormattedValue(): replaced(int)(value * 100)cast with conditional formatting - whole-number percentages useString.valueOf((int) percent), fractional percentages useString.format("%.1f", percent). Prevents truncation of values like 0.5% to 0%CombatXpEventSystem.handlePlayerDealsDamage(): added early return whenisBroken && identifier != null, preventing XP awards for broken (0 durability) weaponsBowDefaults: added 9Weapon_Longbow_*entries (Adamantite, Cobalt, Thorium, Iron, Copper, Crude, Flame, Frost, Poison) andWeapon_Kurabiye(60L)QuestService.refreshReachLevelObjectives(): new method that iterates allREACH_LEVELobjective refs, checks each active quest's objectives against the player's current skill levels, and completes any that are now satisfied. TriggerscheckQuestCompletion()for newly completed objectivesQuestPage.build(): callsrefreshReachLevelObjectives()after fetchingQuestComponent, before display logic
v0.8.6
Kill-Only Combat XP Mode
- New Kill-Only Combat XP toggle - When enabled, offensive weapon skills (Swords, Axes, Polearms, Staves, Blunt, Daggers, Archery, Unarmed) only gain XP when you kill a mob, not on every hit. Defense and Acrobatics continue gaining XP per-hit regardless of this setting. Toggle it from the admin dashboard under General Settings
- Kill XP scales with weapon and mob - Your weapon's per-hit XP value acts as a percentage bonus on the mob's base XP. A legendary sword with 70 per-hit XP grants a 70% bonus, so killing a Trork (65 base) yields 110 XP. Weapon quality matters equally whether fighting critters or bosses
- Weapon XP Scaling toggle - Control whether weapon quality applies as a percentage bonus on mob base XP. Toggle from the Mob Kill XP admin page. When disabled, kill XP equals only the mob's base value
- Comprehensive mob XP defaults - Over 100 mob types have XP values sourced from the Hytale mob database, organized into clear tiers: critters/passive (5-22 XP), minor hostile (35-45), standard enemies (65), tough enemies (85), elite (130), endgame (250), and bosses (up to 500). All values are overridable
- New Mob Kill XP config - Customize how much base XP each mob type awards on kill via the new
mods/mmoskilltree/mob-kill-xp.jsonconfig file. Uses the same override-based system as other configs - only your customizations are saved, defaults auto-update with mod versions - New Mob Kill XP admin page - Browse and edit mob kill XP values from a visual editor accessible via the "Mob Kill XP" button on the admin dashboard. Add custom patterns, disable entries, reset to defaults, trim redundant overrides, and toggle weapon XP scaling
- Pattern-based mob matching - Mob kill XP uses longest substring matching against mob type names (e.g., "Skeleton" matches "Skeleton_Fighter", "Skeleton_Archer", etc.)
Bug Fixes
- Fixed
/mmoboostnot working when executed by other plugins - Third-party plugins like Tip4Serv that execute commands through the server's command system were rejected with "Only players or console can use this command". The boost command now correctly accepts commands from any non-player sender, such as plugin-dispatched command sources - Fixed crafting XP not awarded when using workstations - Crafting at workstations (anvils, furnaces, etc.) was not awarding XP or tracking quest progress because the craft event was emitted by the workstation entity, not the player. Crafting XP and quest objectives now work correctly regardless of whether you craft from your inventory or a workstation
- Fixed quests with level requirements not completing retroactively - Quests with "reach level" objectives would stay incomplete if the player already exceeded the required level when the quest was accepted. Level-based objectives are now checked and completed immediately at accept time, so players no longer need to gain another level to unlock quest rewards
- Milestone announcement commands now use server language - The
{skill}placeholder in milestone announcement commands now resolves to the server's default language instead of the triggering player's language, so broadcast messages are consistent for all players
Technical Details
BoostCommand: changed sender validation from requiring exactlyPlayerorConsoleSenderto treating any non-Playersender as console-like. This allows third-party plugins (Tip4Serv, etc.) that dispatch commands viaCommandManagerto use/mmoboostwithout being rejectedCraftRecipeEventSystem: changed ECS query fromPlayerRef.getComponentType()toArchetype.empty()so workstation-emitted craft events are captured. AddedtryGetPlayerRefFromEvent()(reflection-based) to resolve the crafting player from the event object, with fallback to the subject entity'sPlayerRefcomponent for inventory crafting. UsesplayerRef.getReference()to get the player entity ref for XP awards when the subject is a workstationQuestService.acceptQuest(): new overload accepting@Nullable SkillComponent. After initializing objective progress, iterates allREACH_LEVELobjectives and immediately completes any where the player's current skill level (or total level) already meets the requirement. All callers (processEvent,QuestPage,PlayerQuestCommand,QuestCommand) updated to passSkillComponentCommandExecutor.processPlaceholders(): new overload accepting@Nullable SkillComponentandboolean useServerLocale. WhenuseServerLocaleis true,{skill}resolves viaMessages.getSkillNameForLang()using the server's default language. When false with a non-nullSkillComponent, resolves using the player's language. Falls back to hardcoded English display name otherwise.SkillService.checkMilestoneAnnouncement()updated to use server locale
- Added
combatXpOnKillOnlyboolean toSkillConfig(defaultfalse), with getter/setter, save/load, andreloadDefaults()reset CombatXpEventSystem: guardshandlePlayerDealsDamage()behind!SkillConfig.getInstance().isCombatXpOnKillOnly()check. Defense (TAKE_DAMAGE) and Acrobatics (FALL_DAMAGE) paths unchangedMobKillEventSystem: addedhandleKillXp()- determines weapon skill viaCombatWeaponUtil, looks up mob base XP fromMobKillXpConfig, applies weapon XP as a percentage bonus viamobBaseXp * (100 + weaponXp) / 100(whenweaponXpScalingenabled), callsSkillService.addXp()- Extracted
CombatWeaponUtil- shared weapon→skill detection utility used by bothCombatDamageEventSystemandMobKillEventSystem. Uses longest substring match against DEAL_DAMAGE_PHYSICAL skill XP maps - New
MobKillXpConfig- override-based config (schema v1) with longest substring matching for mob types,weaponXpScalingboolean (defaulttrue). Stores overrides inmods/mmoskilltree/mob-kill-xp.json, generates defaults reference file - New
MobKillXpDefaults- 100+ default XP entries sourced from hytaleitemids.com API (295 mobs). XP tiers: critters 5, small passive 8, livestock 10-22, minor hostile 35, small hostile 45, medium hostile 55, standard enemies 65, tough 85, elite 130, high elite 175, endgame 250, bosses 400-500, fallback 25. Substring collision handling (e.g., explicit "Pigeon" to avoid "Pig" match). Undead livestock variants get hostile-appropriate XP - New
MobKillXpPage- admin page modeled afterXpOverridesPagewith single scrollable pattern list, add/update/disable/reset/trim functionality AdminConfigPage: added#ToggleCombatXpKillOnlytoggle,#EditMobKillXpBtnnavigation button, and corresponding event handlersMobKillXpPage: added#ToggleWeaponScalingtoggle withweaponXpScalingaction handler, dynamic mode note text- Added 4 localization keys (
ui.admin.combat_xp_kill_only_label,ui.admin.edit_mob_kill_xp,ui.mobkillxp.title,ui.mobkillxp.weapon_scaling_label) to all 9 language files
v0.8.5
Bug Fixes
- Fixed Acrobatics skill not gaining XP - The Acrobatics skill was not awarding any XP because its default XP values were never loaded. Acrobatics now correctly awards XP when players survive fall damage
- Improved fall damage detection - Fall damage is now detected more reliably for both Acrobatics XP awards and fall damage reduction rewards, using the damage cause ID as the primary check with a fallback to the environment source type
- Improved mob identification in combat - Combat events now extract mob identities from their model asset ID (e.g., "Skeleton_Fighter", "Bear_Grizzly"), providing more reliable entity detection for combat XP, quest objectives, and entity blacklisting
Quests
- New default quest: Forest Patrol - A repeatable kill quest to slay 10 Skeletons (pattern-matched, so Skeleton_Fighter, Skeleton_Archer, etc. all count). Rewards 2 Woodcutting XP Tokens. 90-minute cooldown
Technical Details
- Uncommented
case ACROBATICSinSkillDefaultsswitch, enablingAcrobaticsDefaults.getDefaults()to be registered AcrobaticsDefaults: changed XP key from"Fall"to""(empty catch-all pattern matching all acrobatics triggers)- Extracted
DamageUtil.isCause(damage, id)- checksdamage.getCause().getId()first, falls back toEnvironmentSource.getType(). Used by bothCombatXpEventSystem(acrobatics XP) andCombatDamageEventSystem(fall damage reduction) - Extracted
EntityIdentifierUtil- consolidates mob identification fromCombatDamageEventSystem,CombatXpEventSystem, andMobKillEventSysteminto a single utility. CombinesModelComponent.getModel().modelAssetIddeep extraction (with regextoString()fallback), directModelComponentmethod probing, andDisplayNameComponentfallback. Also providesdescribeEntity()for debug logging - Replaced wildcard import in
SkillDefaultswith explicit imports
v0.8.4
Quest Config
- Default quests are now auto-generated on every server start - Default quests are written to
quests/_defaults/default-quests.jsoneach time the server loads. This file is always overwritten, so default quests stay up-to-date with new mod versions without any manual steps - User quest files fully replace defaults - Create your own
.jsonfiles anywhere in thequests/folder (or subdirectories). If any user files exist, the defaults in_defaults/are ignored entirely. The generated default file remains as a reference you can copy and customize - Legacy example-quests.json automatically migrated - Servers upgrading from older versions will have their
example-quests.jsonmoved toquests/backups/automatically. No quest data is lost - 5 new default quests - Combat Initiate (kill Trorks), Builder Basics (place planks), Crafter Apprentice (craft planks), Daily Hunt (repeatable mob bounty), and Boss Bounty (defeat Trork Chieftain with manual reward claim). Total default quests increased from 3 to 8
Balance
- Reduced Max Health rewards across all skill trees by 50% - Health bonuses from skill tree rewards were too powerful, allowing high-level players to accumulate excessive health pools. All Max Health reward values have been halved across every skill (Gathering, Combat, Crafting, and Misc). Defense remains the highest health-granting skill (up to +28 at level 100, down from +55). Server owners with custom skill tree overrides are not affected
Quest UI
- Quest requirements now shown in the UI - Locked quests now display a "Requires:" line below the description showing what's needed to unlock them: prerequisite quest names, total level gates, and skill level gates. The requirements text appears in green when all requirements are met, or gray when any are unmet. Fully localized in all 9 languages
Bug Fixes
- Quest completion notification no longer references /quest command - The "quest ready to claim" notification now directs players to the Quests page instead of the removed
/quest claimcommand
Technical Details
QuestConfigrestructured:load()now runs a 4-step pipeline -migrateLegacyExampleQuests(),writeDefaultQuestsFile(), load quests (user xor defaults),buildIndex()- New
hasUserQuestFiles()method recursively checks for.jsonfiles outside_defaults/andbackups/. If any exist,loadDefaultQuests()is skipped entirely and onlyloadUserQuests()runs (all-or-nothing) - Removed
isDirectoryEmpty(),writeDefaults(),loadDirectory(). Replaced bywriteDefaultQuestsFile()(always regenerates),loadDefaultQuests()(non-recursive_defaults/scan),loadUserQuests()(recursive, skips_defaults/andbackups/).loadFile()simplified back to no parameters (no per-ID override logic needed) _defaults/default-quests.jsonincludes_noticefield warning users the file is auto-generated- All 89
STAT_HEALTHreward values inSkillTreeDefaultsreduced by ~50% across 12 skills: Mining, Woodcutting, Excavation, Harvesting, Swords, Polearms, Staves, Axes, Blunt, Defense, Crafting, and Building notify.quest_ready_to_claimlocalization key updated in all 9 languages to reference Quests UI page; removed unused{1}(questId) parameter fromMessages.get()call inQuestServiceQuestRow.ui: added#RequirementsLabelelement (hidden by default) between#QuestDescriptionand#ObjectivesContainerQuestPage.populateQuestRow(): forNOT_STARTEDquests, calls newbuildRequirementsText()method. Reads prerequisite IDs,minLevel,minSkill/minSkillLeveldirectly fromQuestDefinitionand builds a comma-separated display string via localization keys- Added 4 localization keys (
ui.quests.requires_label,ui.quests.req_complete_quest,ui.quests.req_total_level,ui.quests.req_skill_level) to all 9 language files
v0.8.3
Improvements
- Underscore delimiter for boost commands -
/mmoboost giveand/mmoboost globalnow accept underscores as an alternative to pipes for separating arguments (e.g.,/mmoboost give Ziggfreed_mining_2_30instead of/mmoboost give Ziggfreed|mining|2|30). Pipes still work as before
v0.8.2
Admin Dashboard
- Leaderboard toggle - Server owners can now show or hide the Leaderboard tab from the admin dashboard. When disabled, the Leaderboard tab is hidden from all sidebar pages (Skill Overview, Boosts, Quests) and players cannot navigate to it. Found in General Settings alongside the existing Quests toggle
- Reset to Defaults warning - The admin dashboard now shows a prominent warning before resetting to defaults, reminding server owners to back up their overrides first
Bug Fixes
- Fixed orphaned stat bonuses persisting after skill tree changes - When an admin removed or changed a skill tree reward while players were offline, those players could retain phantom health, stamina, or mana bonuses on their next login. Offline players now receive full reward validation on reconnect regardless of whether the change was to XP config or the skill tree
- Fixed stat bonuses not updating during live skill tree edits - Editing skill tree rewards via the admin dashboard or
/mmoconfig reloadnow fully refreshes stat bonuses for all online players. Previously, changing a reward's type or removing a reward could leave stale health, stamina, or mana bonuses applied - Stat bonuses are now cleared on disconnect - Health, stamina, and mana bonuses from skill tree rewards are stripped when a player disconnects. On reconnect, only rewards that still exist in the current config are reapplied. This prevents stale stat modifiers from carrying over if the skill tree is edited between sessions
Technical Details
- Added
enableLeaderboardboolean toSkillConfig(defaulttrue), with getter/setter, save/load, andreloadDefaults()reset AdminConfigPage: new#LeaderboardRowtoggle in UI,#ToggleLeaderboardevent binding, andtoggleLeaderboardaction handlerViewXpPage,BoostPage,QuestPage:#TabLeaderboardset toVisible: falsein UI files; Java conditionally shows tab and guards action handler withisLeaderboardEnabled()check- Added
ui.admin.leaderboard_labellocalization key to all 9 language files AdminConfigPage.ui: added#WarningLabelwith red warning text above the Reset to Defaults buttonSkillService.validateOnReady()andprocessTrigger(): expandedif (configChanged)guard toif (configChanged || skillTreeConfigChanged)soSkillTreeService.validateAllRewards()→cleanupOrphanedRewards()runs for offline players after skill tree config changes (previously only ran for XP config changes)SkillTreeService.removeAllStatModifiers(): new method (two overloads -Store/RefandHolder) that iterates all claimed rewards and removes health, stamina, and mana modifiers fromEntityStatMapMMOSkillTreePlugin: registeredPlayerDisconnectEventhandler that callsremoveAllStatModifiers()on disconnect. TriesRef/Storepath first (playerRef.getReference()), falls back toHolderpath (playerRef.getHolder()) when the player is between worldsSkillTreeOverridesPage.revalidateOnlinePlayers()andMMOConfigCommand.validateAllOnlinePlayers(): addedremoveAllStatModifiers()call beforevalidateAllRewards()+reapplyStatRewards()for a clean-slate approach - strips all stat modifiers first, then only reapplies valid ones
v0.8.1
Bug Fixes
- Fixed crash when opening Quest page in a new world - Opening the Quest page before performing any quest-tracked action caused a server error. Players with no quest data (new world or first join) can now browse and accept quests immediately
- Fixed quest JSON files using unicode escapes - Generated quest files wrote
\u003dinstead of=and\u0027instead of'in reward commands, making manual editing confusing. Quest files are now written with clean, human-readable characters - Fixed boost quantity flag only awarding one token - Using
/mmoboost givewith--quantity=greater than 1 could silently discard all but one token. Multiple tokens created in rapid succession shared the same internal ID, causing each to overwrite the previous one. Token IDs are now unique regardless of creation speed - Renamed admin quest command from
/queststo/mmoquestadmin- Follows the existingmmoprefix naming convention (/mmoconfig,/mmoboost,/mmoadmin). Avoids conflicts with other mods that may register/quests - Default XP notification threshold raised to 3 - New players no longer see notifications for every 1 XP gain. The default minimum XP threshold for notifications is now 3, reducing chat spam from low-XP actions. Existing players keep their current setting
Technical Details
QuestService.canAcceptQuest(): changedquestsparameter from@Nonnullto@Nullable. When null (noQuestComponenton entity), prerequisites block acceptance, level/permission gates still apply, quest state checks are skipped (player has no quest history)QuestConfigandPendingQuestRewardStore: addeddisableHtmlEscaping()toGsonBuilderto prevent Gson's default HTML-safe encoding of=,<,>,&,'- Fixed
mining_beginnerdefault quest displayText mismatch:amount: 50but text said "Mine 150 Stone blocks" - corrected to "Mine 50 Stone blocks" BoostToken.create(): token ID generation changed fromSystem.currentTimeMillis()suffix toUUID.randomUUID()(first 8 chars). The millisecond-resolution timestamp caused ID collisions whenawardToken()was called in a tight loop, and since tokens are stored in aMapkeyed by ID, duplicates silently overwrote each otherQuestCommand: constructor changed fromsuper("quests", ...)tosuper("mmoquestadmin", ...). Updated Javadoc and all 9 language files (usage strings)MMOSkillTreePlugin: addedholder.ensureAndGetComponent(QuestComponent.TYPE)inPlayerConnectEventhandler, matching the existingSkillComponentpattern. Guarantees QuestComponent exists on every player from connect, eliminating null checks in QuestPage and QuestService for new players
v0.8.0
Data-Driven Quest System
- Fully configurable quests via JSON files - Define quests in
mods/mmoskilltree/quests/using JSON. Each quest has objectives, rewards, categories, prerequisites, and level requirements. An example file with 3 starter quests is generated on first run - 8 objective types - BREAK_BLOCK, PLACE_BLOCK, CRAFT_ITEM, KILL_ENTITY, DEAL_DAMAGE, PICKUP_ITEM, REACH_LEVEL, and TALK_TO_NPC. Each objective supports exact, contains, or prefix matching against target identifiers
- Quest categories - Organize quests into Main, Daily, Misc, or custom categories. Players can filter by category in the UI
- Repeatable quests with cooldowns - Mark any quest as repeatable with a configurable cooldown period. After completing and claiming, the quest resets once the cooldown expires
- Sequential objectives - Optionally require objectives to be completed in order. Only the next incomplete objective tracks progress when sequential mode is enabled
- Auto-accept and auto-claim - Quests can be set to automatically start tracking when a matching event occurs, or to auto-deliver rewards on completion without manual claiming
- Prerequisites and level gates - Quests can require other quests to be completed first, a minimum total level, or a minimum level in a specific skill before they become available
- Command-based rewards - Rewards execute server commands with
{player}and{quest}placeholders. Supports running as console or player, delayed execution, and offline queueing so players receive rewards on next login - Progress notifications - Players see a notification on every objective increment showing current progress (e.g., "Mine Stone Blocks (3/10)")
- Enable/disable toggle - Turn the entire quest system on or off from skill-config.json. When disabled, the Quests tab is hidden from all UI pages
Quest UI Page
- New Quests page accessible from the sidebar - Browse all available quests from a dedicated page with the same sidebar navigation as the Skill Overview. The Quests tab appears in the left panel when quests are enabled
- Category filter tabs - Filter the quest list by All, Main, Daily, or Misc categories
- State-based display - Each quest shows its current state with color-coded status labels: Available (orange), In Progress (blue), Ready to Claim (gold), Completed (green), or Cooldown with time remaining (gray)
- Objective progress tracking - Active quests display each objective with current/required progress counts. Completed objectives are highlighted in green
- Accept, Claim, and Abandon actions - State-appropriate action buttons on each quest row. Accept available quests, claim completed rewards, or abandon quests in progress
- Reward preview - Quest rows show a comma-separated list of reward display names so players know what they'll earn
- Smart sorting - Quests are sorted by relevance: active quests first, then ready-to-claim, then available, then on cooldown, then completed
Quest Commands
- Player command
/quest- Players can accept, claim, abandon, and check status of quests via commands./quest claim allclaims all pending rewards at once - Admin command
/quests- Server admins can reload quest config, list quests by category, give quests to players, reset quest progress, complete quests instantly, and view detailed player quest status. Supports targeting all players with* - Offline reward delivery - Quest rewards for offline players are queued to a persistent file and automatically delivered when the player next logs in
Boost Command
- Give multiple boost tokens at once - The
/mmoboost givecommand now accepts an optional--quantity=argument to award multiple tokens in a single command. Defaults to 1 if omitted
XP Token Overrides Editor
- New admin page for editing XP tokens and boost tokens - Browse and modify all 240 token definitions at runtime through a visual editor. Accessible from the admin dashboard via the new "Edit Tokens" button
- Three browsing modes - Switch between XP Tokens, Personal Boosts, and Global Boosts tabs. Each mode adapts the navigation and table columns to the token type
- Skill and category navigation - Browse tokens by skill (Mining, Swords, etc.) or by boost target category (All Skills, Gathering, Combat, Crafting). Personal boosts support both skill-specific and category-level targets
- Override any token value - Change XP amounts for flat tokens, or multiplier and duration for boost tokens. Only your customizations are saved; defaults auto-update with mod versions
- Disable individual tokens - Prevent specific tokens from being consumed without removing them entirely
- Reset to defaults - Restore any overridden token back to its default values with one click
- Trim redundant overrides - Remove overrides that match default values to keep your config clean
- Enable/disable toggle - Turn the entire token system on or off from the editor page
UI Improvements
- Boost page now has sidebar navigation - The Boosts page has been redesigned with the same left panel sidebar as the Skill Overview and Quest pages. Navigate between all sections without going back to the overview first
- Quests tab in sidebar - All pages with the sidebar (Skill Overview, Boosts, Quests) now include a Quests tab that links to the Quest page when quests are enabled
Skill Tree
- Admins can always reset skill trees - OPs and players with the
mmoskilltree.adminpermission can still reset skill tree rewards even when the "Allow Skill Tree Reset" toggle is disabled. This lets server owners lock respeccing for regular players while retaining the ability to fix issues
Bug Fixes
- Fixed leaderboard not updating after admin XP commands - Using
/setmmoxpor/bulksetxpnow immediately reflects changes on the leaderboard. Previously, XP changes from these commands were invisible on the leaderboard until the player next gained XP organically - Fixed ghost stat bonuses after reward revocation - Revoking skill tree rewards that had been renamed or removed from config could leave orphaned health, stamina, or mana bonuses on the player. These phantom modifiers are now properly cleaned up during revocation
- Fixed XP tokens not triggering full level-up effects - Consuming a flat XP token that caused a level-up now correctly fires milestone announcements, item reward notifications, and quest objective tracking. Previously only the level-up notification was shown
- Fixed combat damage tracking not identifying mob types - Damage dealt to regular mobs was not being identified by entity type, preventing DEAL_DAMAGE quest objectives and per-mob-type blacklisting from working. Mob types are now correctly resolved for all entities
- Entity blacklist now supports specific mob types - The combat XP entity blacklist (
/mmoconfig blacklist) can now block individual mob types (e.g., "hytale:trork") in addition to the existing Projectile and Deployable categories. Previously, only those two special categories could be blacklisted - Fixed player data not initializing until first action - Skill data is now created when a player connects to the server, not on their first block break or combat event. All UI pages, settings, and leaderboard entries work immediately on join. New players also receive the server's default language automatically
- Fixed Settings page becoming unresponsive - Clicking toggles or language buttons on the Settings page before skill data was initialized could leave the page stuck in a loading state. The page now refreshes correctly regardless of data availability
Technical Details
- New
quest/package:QuestComponent(ECS component, registered asmmoskilltree:quests),QuestDefinition,QuestObjective,QuestObjectiveProgress,QuestReward,QuestStateenum (NOT_STARTED, ACTIVE, COMPLETED, COMPLETED_UNCLAIMED, ON_COOLDOWN),ObjectiveTypeenum (8 types),PlayerQuestData(base64-encoded progress serialization) - New
QuestConfigsingleton: loads quests recursively frommods/mmoskilltree/quests/*.json, builds inverted index (ObjectiveType → quest/objective refs), generatesexample-quests.jsonon first run, supports hot-reload - New
QuestService: static event hooks (onBlockBreak,onBlockPlace,onCraftItem,onEntityKill,onPickupItem,onDealDamage,onLevelReached) called from all event systems.processEvent()routes events to active objectives withMatchMode(EXACT/CONTAINS/PREFIX) pattern matching, handles auto-accept, sequential objectives, and progress notifications - New
QuestRewardExecutor: executes command-based rewards with{player}/{quest}placeholder substitution,RunAs(CONSOLE/PLAYER),delayTickssupport (1 tick = 50ms via daemon thread), offline queueing viaPendingQuestRewardStore - New
PendingQuestRewardStore: file-based JSON queue atmods/mmoskilltree/pending-quest-rewards.json. Queues commands by UUID/username, drained onPlayerReadyEvent - New
MobKillEventSystem: detects player→mob kills viaDeathComponent/Damage.Source, resolves mob identifier viaModelComponent(reflection-based, cached) withDisplayNameComponentfallback, routes toQuestService.onEntityKill() - New
QuestPageextendsInteractiveCustomUIPage: sidebar layout matchingViewXpPage, category filter tabs (All/Main/Daily/Misc), state-based quest row display, accept/claim/abandon event handlers - New
QuestPage.ui,QuestRow.ui,QuestObjectiveRow.uitemplates - New
QuestCommand(admin,/quests): subcommandsreload,list,give,reset,complete,status - New
PlayerQuestCommand(player,/quest): subcommandsaccept,claim(supportsall),abandon,status BoostPagerestructured: standalone 860×990 layout replaced with 1100×1000 sidebar layout. AddedupdateTabStyles(),bindTabEvents(),hasPermissionForCategory(),isCategoryTab(),getCategoryFromTabId().BoostEventDataextended withcategorycodec field. Back button removed, replaced by sidebar tab navigationSkillConfig: addedenableQuestsboolean (default true),isQuestsEnabled(),setQuestsEnabled(). Config version unchanged (backward compatible)ViewXpPage: added#TabQuestsvisibility/binding when quests enabled- Quest hooks added to all event systems:
BreakBlockEventSystem,PlaceBlockEventSystem,CraftRecipeEventSystem,PickupItemEventSystem,CombatXpEventSystemeach call correspondingQuestService.on*()method MMOSkillTreePlugin: registersQuestComponentECS type, loadsQuestConfigandPendingQuestRewardStore, delivers pending rewards onPlayerReadyEvent, savesPendingQuestRewardStoreon shutdown- Added
LeaderboardDataStore.getInstance().updatePlayer()calls toSetXpCommand(both "all skills" and "single skill" paths) andBulkSetXpCommand(per-target loop) afterMMOSkillTreeAPI.setXp() SkillTreeService.validateAndRevokeRewards(): whengetChoiceById()returns null during tier revocation,removeOrphanedStatModifiers()is now called to clean up stat modifiers beforeclearClaimedRewardsForTier()wipes the component data.revokedCountnow increments regardless of whether the reward still exists in config- Extracted
SkillService.handleLevelUp()consolidatingnotifyLevelUp(),checkMilestoneAnnouncement(),checkAndNotifyItemRewards(), andQuestService.onLevelReached().addXp()refactored to call it.XpTokenConsumeInteraction.processFlatXpToken()now callshandleLevelUp()instead of onlynotifyLevelUp() SkillTreePage.build(): reset button visibility check now includesPermissionUtil.hasAdminPermission(player)bypass whenisAllowSkillReset()is falseBoostCommand: addedOptionalArgviaquantityArg withOptionalArg("quantity", ...). Parsed inexecuteAsync()with validation (>= 1, defaults to 1).handleGive()andgiveTokenToAllPlayers()loopawardToken()quantitytimes per target- New
XpTokenOverridesPageextendsInteractiveCustomUIPagewithTokenTypeTabenum (XP_TOKENS, PERSONAL_BOOSTS, GLOBAL_BOOSTS) for three-mode navigation - New
XpTokenOverridesPage.ui(950×700) andXpTokenRow.uirow template matching existing admin page styling XpTokenConfig: addedsetOverride(),setDisabled(),removeOverride(),hasOverride(),getDefaultDefinition(),setEnabled(),trimRedundantOverrides()public methodsAdminConfigPage.java+.ui: added#EditTokensBtnwith"editTokens"action binding- Added ~20
ui.quests.*localization keys, 1notify.quest_objective_progresskey,ui.admin.edit_tokens, andui.xptokenoverrides.titleto all 9 language files CombatXpEventSystem.getEntityType(): addedreadMobIdentifier()fallback after Projectile/Deployable archetype checks. UsesModelComponent(reflection-based method resolution) withDisplayNameComponentfallback - same pattern asMobKillEventSystem. Returns actual entity identifier (e.g.,"hytale:trork") instead ofnullfor regular mobs, enabling both DEAL_DAMAGE quest tracking and per-mob-type blacklistingMMOSkillTreePluginPlayerConnectEvent handler:Holder.ensureAndGetComponent(SkillComponent.TYPE)now creates the SkillComponent on connect instead of deferring toSkillService.processTrigger()on first action. Also sets default language viaSkillConfig.getDefaultLanguage()for new players with no language preferenceSettingsPage.handleDataEvent(): null SkillComponent fallback now refreshes the page viaopenCustomPage()instead of silently returning, preventing the UI from getting stuck in a loading state
v0.7.9
UI Improvements
- Item Rewards page now shows 5 cards per row - Reward cards are more compact, fitting an extra column so you can see more rewards at a glance without scrolling
Bug Fixes
- Fixed empty display names on reward cards - Rewards with localization keys that resolved to empty strings now properly fall back to the configured display name, then to auto-derived names from the item ID. Fixes blank titles on XP token and boost token reward cards
- Fixed command reward display name resolution - The fallback logic now correctly handles empty localization results instead of treating them as valid resolved names
Localization
- Boost target names are now fully localized - Boost descriptions, activation notifications, and the XP page boost summary now show skill/category names in the player's language instead of hardcoded English
- Global boost broadcast uses server default language - The server-wide boost activation announcement now uses the configured default language instead of always English
Item Reward Balance
- Added Void Leather to level 65 milestone rewards - Void Leather is now included at level 65 for Harvesting (x3), Daggers (x2), Archery (x2), Unarmed (x2), Block (x3), Acrobatics (x2), and Crafting (x2)
Technical Details
- Added
resolveDisplayName()helper toItemRewardsPage- tries localization key first, falls back todisplayName, handles empty strings fromLocalizationConfig.get() - Fixed
Messages.getCommandRewardDisplayName(): added!resolved.isEmpty()check alongside!resolved.equals(key), sinceLocalizationConfig.get()returns""for missing keys (not the key itself) - Added
XpToken_andBoostToken_prefix parsing toformatItemName()in bothItemRewardsPageandCommandRewardsDefaults - Added
Ingredient_Leather_Void→ "Void Leather" mapping toformatItemName()andgetItemColor()(COLOR_VOID) inCommandRewardsDefaults - Added
Messages.getBoostTargetName()/getBoostTargetNameForLang()- resolvesBoostTargetto localized name usingboost.target.all_skills,skill.*, orui.viewxp.category.*keys - Added
ui.boosts.desc_active,ui.boosts.desc_duration,ui.boosts.scope_globallocalization keys to all 9 language files BoostPage.java: replacedgetDisplayDescription()/getDisplayName()calls with localized equivalents usingMessagesViewXpPage.java: active boost target names now useMessages.getBoostTargetName()instead ofBoostTarget.getDisplayName()XpTokenConsumeInteraction.java: boost token consumption notification now uses localized target nameXpBoostService.broadcastGlobalBoostActivated(): now usesSkillConfig.getDefaultLanguage()instead of hardcoded"en"RewardCard.ui: card dimensions reduced from 228×110 to 186×95, padding 10→8, claim button 80×24→68×22, qty label width 40→32RewardCardRow.ui: row height reduced from 118 to 103ItemRewardsPage.java:CARDS_PER_ROWchanged from 4 to 5
v0.7.8
Bug Fixes
- Fixed combat XP exploit from non-hostile mobs - Players could farm combat XP by attacking passive creatures. Combat offense XP (Swords, Axes, Archery, etc.) is now only awarded when dealing damage to hostile entities
- Fixed XP token milestone rewards not giving items - XP tokens awarded at skill levels 25, 50, 65, 80, and 100 were using an incorrect item ID (e.g.,
XpToken_mining_Fragmentinstead ofXpToken_Mining_Fragment), causing the/givecommand to fail silently. Tokens now resolve correctly for all 16 skills - Fixed reward card display for XP tokens - XP token rewards on the Item Rewards page now show their proper display name (e.g., "Fragment XP Token") instead of raw placeholder text
- Fixed milestone announcement skill names - The
{skill}placeholder in announcement commands now uses the proper display name (e.g., "Mining") instead of the lowercased enum name ("mining") - Removed Woodcutting Fragment craft recipe - The craftable Woodcutting XP Fragment has been removed
New XP Defaults
- Added Emerald, Sapphire, Cinder, and Ruby weapon XP values - All gem-tier weapon variants now have default XP values for Swords, Daggers, Battleaxes, Axes, Maces, and Bows. Regular gem weapons match Adamantite-tier XP, Uncommon gem weapons match Iron-tier XP. Adds support for the Emerald Armor, Sapphire Armor & Weapons, and Ruby Armor & Weapons mods
Technical Details
- Added
HostilityUtilinutil/- checks NPC hostility via the BlackboardAttitudeView. Uses reflection to resolvegetAttitude*method at runtime (cached after first call) for forward-compatibility with server builds. Returnsfalse(non-hostile) if the entity has no blackboard or attitude data CombatXpEventSystem.handle(): deal-damage XP path now callsHostilityUtil.isHostileTowards(store, targetRef, attackerRef)after the blacklist check. PvP (target is a player) bypasses the hostility gateCommandExecutor.processPlaceholders():{skill}now resolves viaSkillType.fromString(skill).getDisplayName()instead ofskill.toLowerCase(). Falls back to lowercase for unrecognized stringsItemRewardsPage.parseGiveCommand(): checksreward.hasDisplayInfo()before falling back to parsing the item ID from the raw command string. AcceptsSkillComponentparameter for localized display names
v0.7.7
Consumable XP Token Items
- New item-based XP tokens - Physical items that players consume (hold-to-use) to instantly gain XP in a specific skill. Available in 8 tiers from Fragment (100 XP) to Artifact (500,000 XP), each for all 16 skills - 128 token variants total
- Boost token items - Consumable items that activate timed XP multipliers. 100 personal variants (5 tiers across 20 targets) and 12 global variants (3 tiers across 4 categories) - 112 boost token variants total
- 235 new items - Every skill × tier combination has its own item with unique name, description, and quality tier (Common, Uncommon, Rare, Epic)
- Hold-to-consume mechanic - Tokens use a charging interaction: hold right-click to consume, interrupted if you take damage
- Flat XP bypasses boosts - XP token grants are raw XP that won't be multiplied by active boosts, preserving their intended value
- Boost tokens use existing boost system - Boost items activate through the same pipeline as
/mmoboost, respecting stacking limits and expiration - XP tokens as milestone rewards - Skills now award XP tokens at levels 25, 50, 65, 80, and 100 as part of item rewards
- Craftable Woodcutting Fragment - The Woodcutting XP Fragment can be crafted at a Workbench using 200 wood, 2 light leather, and 2 iron bars
- Server-configurable - Override XP amounts, multipliers, durations, or disable specific tokens via
mods/mmoskilltree/xp-tokens.json. Only customizations are stored (override-based config)
Technical Details
- New data types:
XpTokenType(FLAT_XP, TIMED_BOOST),XpTokenTier(8 flat tiers),BoostTokenTier(5 boost tiers),XpTokenDefinition(immutable item-to-behavior mapping withwithOverrides()copy method) XpTokenDefaultsgenerates all 235 definitions via loops overIMPLEMENTED_SKILLS(16 skills) × tiers, plus category/skill boost targets × scopes. Count is 235 not 240 because "Crafting" overlaps between categories and individual skillsXpTokenConfigsingleton: override-based config atmods/mmoskilltree/xp-tokens.json(schema v1). Storesenabled,chargeDurationTicks, per-tokenOverrideEntry(nullable xpAmount, multiplier, durationMinutes, disabled). Merges defaults + overrides at load timeXpTokenConsumeInteractionextendsSimpleInstantInteraction: registered as"mmo_xp_token_consume"viagetCodecRegistry(Interaction.CODEC). OnfirstRun(): resolves held item ID →XpTokenDefinition, dispatches toprocessFlatXpToken()orprocessBoostToken(). Consumes held item viaplayer.getInventory().getHotbar().removeItemStack(held)- Flat XP path:
skills.add(skillType, xpAmount)directly (bypassesSkillService.addXp()multipliers), manual level-up detection via before/after comparison, callsSkillService.notifyLevelUp()andSkillTreeService.checkMilestones() - Boost path: creates temporary
BoostToken, stores inskills.getBoostTokens(), callsXpBoostService.activateToken(), removes token on failure (limit reached) - 235 item JSONs in
Server/Item/Items/Consumables/XpTokens/using vanilla quality strings (Common, Uncommon, Rare, Epic) and Charging → mmo_xp_token_consume interaction chain MMOSkillTreePlugin.setup(): loadsXpTokenConfigafterCommandRewardsConfig, registersXpTokenConsumeInteractioncodecCommandRewardsDefaults: addedgiveXpToken(tier, qty)andgiveBoostToken(target, scope, tier)factory methods using{skill}placeholder; added token rewards togetGlobalSkillDefaults()at levels 25/50/65/80/100- Added 6 localization keys to all 9 language files:
notify.xptoken.consumed_flat,notify.xptoken.consumed_boost,notify.xptoken.consumed_boost_global,notify.xptoken.disabled,notify.xptoken.boost_limit,notify.xptoken.not_recognized
v0.7.6
Skill Card Improvements
- Color-coded buff labels - Claimed tree buffs on each skill card now display as individually colored labels instead of plain gray text. Colors match the buff type: red for damage, blue for defense, orange for crit, green for lifesteal, purple for mana, and more
- Tier progress and metadata - Each skill card now shows a metadata line below buffs with tier progress (e.g., "Tier 4/10"), XP bonus, and luck percentage. The Total Level card aggregates tier progress across all skills
- Fixed boost button always showing active style - Skill cards with only unused boost tokens now correctly show a muted gray "Boosts" button instead of the bright yellow active style. Active boosts still display with yellow styling and the "+X%" label
Mastery Cape Updates
- Master Cape stat boost - The Master Cape now provides 20% physical and projectile damage resistance (up from 18%) and 5% damage enhancement across all attack types (up from 4%), solidifying its position as the best cape in the game
- New cape models - The Master Cape, Swords Cape, and Woodcutting Cape now have unique custom models
Milestone Announcement Commands
- Configurable level milestone announcements - Server owners can now run a command when players reach configurable level milestones (default: 10, 25, 50, 75, 100). Great for broadcasting achievements to the whole server via
/sayor triggering custom rewards - Admin UI controls - New section in the admin dashboard to enable/disable announcements, set milestone levels (comma-separated), and customize the command template
- Default command:
say {player} has reached level {level} in {skill}! - Supports placeholders:
{player},{level},{skill},{total_level}
Admin Dashboard Improvements
- Streamlined admin layout - The config overrides display section has been removed from the admin dashboard. Editor buttons (Edit XP Maps, Edit Rewards, Edit Skill Tree) are now in the Actions bar for quicker access
- Trim Overrides moved to editor pages - The "Trim Overrides" button now lives directly on the XP Overrides and Skill Tree Overrides pages, where it's most relevant. XP trimming also covers luck overrides
Online Status Privacy
- Players can hide their online status - New "Show Online Status" toggle in Settings lets players choose whether other players can see them as online on the leaderboard
- Admin bypass - Server admins (OP or
mmoskilltree.admin) always see true online status regardless of player privacy settings - Default: enabled (visible to all, same behavior as before)
Item Reward Balance
- Reduced ore and bar quantities across all milestone rewards - All metal bar rewards (Iron, Copper, Bronze, Cobalt, Adamantite, Thorium, Mithril, Onyxium, Prisma) have been reduced by approximately 50% across all per-skill and total level milestones. Weapons, essences, gems, capes, and boost rewards are unchanged
Technical Details
- Halved all
Ingredient_Bar_*quantities inCommandRewardsDefaults.javaacross all 16 per-skill methods andgetTotalLevelDefaults(). Non-bar rewards (essences, gems, weapons, tools, armor, capes, storm materials, voidheart, chitin, boosts) unchanged - Added
enableMilestoneAnnouncements(boolean),milestoneAnnouncementLevels(List\), milestoneAnnouncementCommand(String) toSkillConfigwith getters/setters, load/save/reloadDefaults support, andConfigDatafields (no config version bump - new fields use defaults when missing) - Added
checkMilestoneAnnouncement()inSkillService.addXp()after level-up detection; checks enabled flag, milestone list membership, processes placeholders viaCommandExecutor.processPlaceholders(), and executes viaCommandExecutor.executeAsConsole() - Added
#MilestoneAnnounceRow,#MilestoneLevelsField,#MilestoneCommandField,#ApplyMilestoneBtn,#MilestonePlaceholderstoAdminConfigPage.ui; increased#SettingsSectionheight 372 → 580 - Wired
AdminConfigPage.java:milestoneLevelsInput/milestoneCommandInputinstance vars,AdminConfigEventDatacodec fields, toggle/apply handlers, text field bindings, reload/reset sync - Added
showOnlineStatusboolean toSkillComponent(field, codec, default/full constructors, clone, getter/setter) - Added
hideOnlineStatusfield toLeaderboardDataStore.PlayerLeaderboardData, set inupdatePlayer()from!skills.getShowOnlineStatus() LeaderboardPage.gatherPlayerData()now acceptsviewerIsAdminparameter; filtersisOnlinefor cached players withhideOnlineStatus=truewhen viewer is not admin- Added
#OnlineStatusSettinggroup toSettingsPage.uiafter#CombatEffectsSetting; wired toggle inSettingsPage.javawith"toggleOnline"handler that updates SkillComponent and leaderboard cache - Added 6 localization keys to all 9 language files:
ui.admin.milestone_announce_label,ui.admin.milestone_levels_label,ui.admin.milestone_command_label,ui.admin.milestone_placeholders,ui.settings.online_status,ui.settings.online_status_desc - Removed
#OverridesSectionfromAdminConfigPage.ui(override counts display and trim/editor buttons); moved#EditXpBtn,#EditRewardsBtn,#EditSkillTreeBtninto#ActionsSectionas a second row - Moved trim functionality out of
AdminConfigPage:XpOverridesPagetrims XP + luck overrides,SkillTreeOverridesPagetrims skill tree overrides; added#TrimBtnto both.uifiles and event bindings/handlers in Java - Removed
countXpOverrides()method and unused imports (XpMapsConfig,LuckConfig,SkillTreeConfig,SkillType) fromAdminConfigPage.java - Cape_Master.json: Physical/Projectile DamageResistance 0.18 → 0.20, Light/Charged/Signature DamageClassEnhancement 0.04 → 0.05
- All 17 cape JSON files:
CosmeticsToHidechanged from["Overtop", "Cape"]to["Back"]for new model system - Fixed
card.hasActiveBoost = card.showBoostBtn→card.hasActiveBoost = hasPersonalBoostinbuildSkillCard()to match the total card's boost logic - Added
BuffEntryinner class (text + color) andgetRewardColor(RewardType)static method with per-type color mapping - Replaced
getSkillBuffsText()→getSkillBuffEntries()returningListwith color data - Changed
SkillCardData.buffsText(String) →buffEntries(List) and added metaTextfield populateSkillCard()now dynamically appendsCardBuffLabel.uitemplates into#CardBuffsRowwith per-label color viacommandBuilder.append()- New
CardBuffLabel.uitemplate: minimal colored label for dynamic buff row population - Updated
SkillCard.ui: replaced#CardBuffsTextLabel with#CardBuffsRowGroup (LayoutMode: Left), added#CardMetaTextLabel (9pt,#7a8a9e) buildSkillCard()computes metadata viaskills.getClaimedTierCount(),SkillTreeService.getActiveRewardsForSkill()for XP bonus and luckbuildTotalLevelCard()now acceptsSkillTreeConfigparameter and aggregates tier counts across all filtered skills
v0.7.5
Skill Cards Grid Layout
- Skill overview redesigned as a card grid - The skill list on the main XP page now displays as a grid of cards (3 per row) instead of flat rows. Each card shows skill name, level, progress bar, XP, claimed buffs, and next reward hint at a glance
- Total Level card in overview - A special summary card appears first when viewing all skills, showing your combined level, total XP, and aggregate buffs across all skills. Distinguished by a blue accent background
- Quick-action buttons on every card - Each card can show up to three buttons depending on context:
- Skill Tree - Opens the skill tree for that skill (shows "Reward Ready!" in orange when unclaimed rewards are available)
- Rewards - Opens the item rewards page filtered to that skill (visible when skill or global rewards are configured)
- Boost - Shows current boost percentage and opens the boost page (visible only when an active boost applies)
- Category color coding - Each card has a colored accent strip matching its category: green for Gathering, red for Combat, orange for Crafting, purple for Misc
- Total Level rewards button - The Total Level card includes a Rewards button that opens item rewards in total level mode when total level rewards are configured
Bug Fixes
- Fixed Storm Hide and Storm Leather item reward IDs - Corrected the item IDs to match the game's naming convention, so Storm materials now appear correctly when claimed
Admin Config: Skill Tree Reset Toggle
- New "Allow Skill Tree Reset" toggle in the admin dashboard - Server owners can now disable the skill tree reset button for all players. When turned off, the Reset button is completely hidden from the skill tree page, preventing players from respeccing their reward choices
- Toggle is located in the General Settings section of
/mmoadmin, between Item Rewards and Default Language - Default: enabled (players can reset as before)
- Saves automatically when toggled, like other admin settings
Technical Details
- Added
allowSkillResetboolean toSkillConfig(field,ConfigData, load/save/reloadDefaults(), getter/setter), bumpedCONFIG_VERSION10 → 11 - Added
#SkillResetRowwith#SkillResetLabeland#ToggleSkillResettoAdminConfigPage.ui, increased#SettingsSectionheight 336 → 372 - Wired toggle in
AdminConfigPage.java: label fromui.admin.skill_reset_label, event binding for"toggleSkillReset"action, handler flips config value SkillTreePage.javaenforces config: whenisAllowSkillReset()is false, sets#ResetButton.Visible = falsebefore the existing claimed-count check- Added
ui.admin.skill_reset_labellocalization key to all 9 language files - Fixed Storm material item IDs in
CommandRewardsDefaults:Ingredient_Storm_Hide→Ingredient_Hide_Storm,Ingredient_Storm_Leather→Ingredient_Leather_Storm; updatedformatItemName()andgetItemColor()matchers - Replaced
SKILL_ROW_TEMPLATE(SkillRow.ui) withSKILL_CARD_ROW_TEMPLATE(SkillCardRow.ui) andSKILL_CARD_TEMPLATE(SkillCard.ui),CARDS_PER_ROW = 3 - New
SkillCardRow.uitemplate: horizontal row container (LayoutMode: Left, Height: 210) - New
SkillCard.uitemplate: 270x200px card with#CategoryStrip,#CardSkillName,#CardLevel,#CardProgress(ProgressBar),#CardXpText,#CardBuffsText,#CardHintText, and#ButtonsRowcontaining#TreeBtn,#RewardsBtn,#BoostBtnwith spacer groups - New
SkillCardDatainner class inViewXpPageholds all display state for one card (name, level, progress, xp, buffs, hint, categoryColor, backgroundColor, button visibility flags, skill reference, isTotalCard flag) - New methods:
getCategoryColor(SkillCategory),buildTotalLevelCard(),buildSkillCard(),populateSkillCard()replace the inline skill row loop - Build method now constructs a
List, chunks into rows of 3, appends row and card templates, then populates each card - New
"skillRewards"action handler inhandleDataEvent(): routes"TOTAL"toItemRewardsPage(playerRef, MINING, true), otherwise opensItemRewardsPage(playerRef, skill)for the specific skill - Existing
ViewXpEventDatacodec reused unchanged (action/skill/category fields)
v0.7.4
Skill Tree Admin Editor
- New in-game Skill Tree editor - Server owners can now customize skill tree reward tiers directly from the admin panel. Open
/mmoadminand click "Edit Skill Tree" to browse skills by category, view all tiers, and modify individual rewards - Override-based customization - Changes are saved as overrides on top of defaults. Only your modifications are stored, so new default rewards from updates propagate automatically without losing your customizations
- Per-tier editing - Select any tier to view its rewards, edit values, add new rewards, remove rewards, or adjust level requirements and choice counts
- Disable or reset tiers - Disable entire reward tiers or reset them to defaults with one click
- Status indicators - Color-coded tier buttons (green = default, orange = overridden, red = disabled) and per-reward status labels (Default, Modified, Added) show exactly what's been customized
- Trim redundant overrides - The existing "Trim Redundant" button on the admin page now also cleans up skill tree overrides that match defaults
- Automatic migration - Existing
skill-tree.jsonfiles from previous versions are automatically detected and migrated, with a backup saved
Technical Details
- Converted
SkillTreeConfigfrom traditional full-regeneration to override-based storage pattern (dualskillTrees+skillTreeOverridesmaps) - New config format uses
schemaVersion: 1with per-skill per-tier override granularity; legacy V4 format auto-detected viaconfigVersionfield and migrated with diff comparison against code defaults - Added override query API:
isDefault(),isTierDisabled(),getOverrides(),getDefaultNode(),countTotalOverrides() - Added override mutation API:
setTierOverride(),disableTier(),removeTierOverride(),trimRedundantOverrides() - Reference file written to
_reference/defaults-skill-tree.jsonshowing all code defaults - New
SkillTreeOverridesPage.javawithSkillTreeOverridesEventDatacodec (13 fields), category/skill/tier navigation, reward list with template rows, and add/edit/remove/disable/reset actions - New UI templates:
SkillTreeOverridesPage.ui(1100x700),SkillTreeRewardRow.ui(row template) - Updated
AdminConfigPagewith#EditSkillTreeBtnbutton, skill tree override count display, and trim integration - Localization keys added to all 9 language files:
ui.admin.edit_skill_tree,ui.skilltreeoverrides.title - Internal
NodeDataandRewardDataclasses changed fromprivateto package-private forrewardsMatch()access
v0.7.3
Level 100 Mastery Boost Upgrade
- Mastering a skill now rewards a global XP boost token - Reaching level 100 in any skill now awards a 1.25x All-Skill Global XP Boost (30 min) token instead of the previous personal skill-specific boost. When activated, this boost benefits every player on the server across all skills
- Celebrating mastery is now a server-wide event - your achievement helps everyone!
Item Rewards Page Overhaul
- Fixed reward claiming not targeting the correct reward - Claim buttons now reliably trigger the correct reward. The rewards page has been rebuilt with a card grid layout where each reward has its own dedicated claim button
- Rewards are now grouped by level - Clear level separators (e.g., "Level 10", "Level 25") divide rewards into tiers, making it easier to find what you've unlocked
- Card-based layout - Rewards display as compact cards in rows of up to 4, showing item name, quantity, status, and claim button at a glance
- Reward descriptions - Server owners can now add optional description text to item rewards in
mods/mmoskilltree/command-rewards.jsonusing the"description"field. Descriptions appear below the item name on reward cards - Existing configs without descriptions continue to work unchanged
Storm Leather & Storm Hide Rewards
- New rare crafting materials added to item rewards - Storm Leather and Storm Hide now appear in milestone rewards for skills where leather and hide materials are thematically relevant
- Available at levels 80 and 100 for Defense, Daggers, Archery, Acrobatics, Unarmed, Harvesting, and Crafting
- Total Level milestones include Storm materials at levels 350, 500, 750, and 1000
- Defense gets the highest quantities as the armor skill
Item Reward Balance Adjustments
- Reduced Total Level 350 rewards - Material quantities cut roughly in half (Mithril/Onyxium 75 to 35, essences 50 to 25, etc.) and boost reduced from 2x 45m to 1.5x 30m to better match the progression stage
- Master Cape moved to Total Level 1500 - The Master Cape is now awarded at Total Level 1500 instead of 1000, making it a true long-term goal
Rewards Editor Improvements
- Click any reward to edit it - Clicking a reward entry in the admin Rewards Editor copies its command, display name, localization key, and color into the input fields for editing
- Saving updates existing rewards - If you edit a reward and save, it replaces the original entry (matched by command string) instead of adding a duplicate
Technical Details
- Replaced
boostSkill_2x_30m()(personal, single-skill, 2x, 30min) withboostGlobal_1_25x_30m()(global, all-skill, 1.25x, 30min) in all 16 skill level 100 reward defaults inCommandRewardsDefaults.java - Added
boostGlobal_1_25x_30m()factory method - Existing Global Skill milestone reward (1.25x Global 15min at level 100 in
getGlobalSkillDefaults()) remains unchanged and stacks with the per-skill reward - Added
COLOR_STORM(#5599dd) constant,formatItemName()andgetItemColor()handlers forIngredient_Storm_LeatherandIngredient_Storm_Hide - Total Level 350: reduced material quantities ~50%, removed Iron Head armor, removed Storm Hide, downgraded boost from
boostAll_2x_45m()toboostAll_1_5x_30m() - Moved
Cape_Masterfrom level 1000 tier to new level 1500 tier ingetTotalLevelDefaults()
Technical: Item Rewards Grid Layout
- Replaced
RewardTierRow.ui/RewardItem.uitemplates withRewardCard.ui,RewardCardRow.ui, andRewardTierSeparator.ui ItemRewardsPage.java: rewrotepopulateSkillRewards()andpopulateTotalRewards()to group rewards by level tier, chunk into rows of 4 cards, and populate each card individually viapopulateCard()- Added
CardDatarecord to bundle display info with claim state for grid population - Removed
populateSingleRewardItem()andgetRewardDisplayName()methods (replaced by card-based approach) CommandRewardEntry.java: addedString descriptionfield with 6-arg constructor,getDescription(),hasDescription(), builder.description()method; 5-arg constructor delegates with null descriptionCommandRewardsConfig.RewardData: addeddescriptionfield to serialization; Gson ignores missing fields on load for backwards compatibilityItemDisplayrecord: addeddescriptionas 4th field, propagated throughparseGiveCommand(),parseBoostCommand(), andparseRewardForDisplay()
Technical: Rewards Editor Click-to-Edit
CommandRewardsPage.buildRewardList(): addedActivatingevent bindings on#CommandTextand#DisplayNamelabels per row, firingselectRewardwith the row index- New
selectRewardhandler inhandleDataEvent()reads the reward at the given index and populatescommandInput,displayInput,locKeyInput,colorInput addRewardhandler now searchescurrentRewardsfor a matching command string; replaces in-place viaList.set()if found, otherwise appends- Extracted
getCurrentRewards(config)helper to reduce duplication acrossselectReward,addReward,disableReward,removeReward
v0.7.2
Bug Fixes
- Fixed skill tree stat modifiers not updating after config changes - Players who were offline during a skill tree config edit (or when the server restarted after edits) would keep outdated health, stamina, and mana bonuses from old reward values. Stat modifiers are now reconciled with current config on each player's first action after any server restart
- Fixed orphaned rewards from removed config entries - If a server owner removes or renames skill tree rewards, players who previously claimed those rewards now have the stale stat modifiers properly cleaned up on their next session
- Fixed item reward commands briefly blocking the server - Claiming item rewards with multiple items (e.g., level 100 rewards) would momentarily stall the server while commands were spaced out. Reward commands are now dispatched without blocking
Technical: Stat Modifier Reconciliation on Startup
MMOSkillTreePlugin.setup()now sets bothconfigChangedandskillTreeConfigChangedflags totrueat the end of initialization- On first skill action per player per restart,
SkillTreeService.reapplyStatRewards()removes and reapplies allSTAT_HEALTH,STAT_STAMINA,STAT_MANAEntityStatMap modifiers with current config values SkillTreeService.validateAllRewards()also runs, cleaning up orphaned rewards viacleanupOrphanedRewards()(removes modifiers for reward IDs no longer present in config)- Per-player cost is one-time iteration of claimed rewards on first action after restart; no ongoing overhead
Technical: Command Reward Execution No Longer Blocks World Thread
CommandRewardService.executeRewards()previously usedThread.sleep(25)insideworld.execute(), blocking the world thread between reward commands- Now uses
CompletableFuture.delayedExecutor()to schedule staggered commands from a background thread, dispatching each to the world thread viaworld.execute()without blocking it
v0.7.1
Mastery Cape Armor Attributes
- Skill capes now provide armor stats - Each mastery cape has unique attributes themed to its skill, making them functional gear on top of prestige cosmetics
- Damage resistance - All capes provide physical and projectile damage resistance (8-18%), surpassing Epic-tier chest armor
- Health bonuses - All capes grant bonus health (+14 to +28), scaling with the cape's combat relevance
- Combat capes include damage enhancements matching their weapon type:
- Swords: +8% Light damage | Daggers: +10% Light damage | Polearms: +10% Charged damage
- Staves: +8% Signature damage, +30 Mana | Axes: +5% Light & Charged damage
- Blunt: +8% Charged damage | Archery: +10% Charged damage | Unarmed: +10% Light damage
- Defensive capes have the highest resistance among skill capes:
- Defense: 16% Physical/Projectile, +26 Health (best pure tank)
- Acrobatics: 12% Physical, 16% Projectile, +18 Health (best projectile dodge)
- Gathering and production capes provide balanced survivability stats
- Harvesting cape also grants +30 bonus Mana
- Master Cape is the best cape - 18% Physical/Projectile resistance, +28 Health, +30 Mana, and +4% to all damage types (Light, Charged, Signature)
Technical: Cape Attribute Implementation
- Added
DamageResistance,StatModifiers, andDamageClassEnhancementblocks to all 17 cape item JSONs inServer/Item/Items/Armor/Capes/ - Stat ranges across all capes: Physical 8-18%, Projectile 8-18%, Health +14 to +28, Mana 0-30
DamageClassEnhancementused on combat capes:Light,Charged, and/orSignatureat 4-10% Multiplicative- Balance reference: base game Epic chest armor gives +24 HP / 14% resist (tank) or +17 HP / +16 Mana / 9% resist (mage); Legendary skill capes exceed both
Technical: Combat System Separation
- Split
CombatDamageEventSysteminto two separate systems for cleaner separation of concerns: -
CombatDamageEventSystem- Damage modification only (damage bonuses, defense reduction, crits, lifesteal, fall damage reduction) -
CombatXpEventSystem- XP awards only (combat XP, defense XP, acrobatics XP, entity blacklist checks) - XP system is read-only and never modifies damage amounts
- Entity blacklist logic (
getEntityType,isEntityBlacklistedForCombatXp) moved to the XP system - Both systems registered in
MMOSkillTreePluginsetup - No functional changes to combat behavior or XP awards
v0.7.0
Russian Language Support
- New language: Russian - Full Russian translation added as the 9th built-in language
- All UI text, notifications, commands, and skill tree content translated
- Select Russian in Settings like any other language
- 9 built-in languages - English, Spanish, French, Portuguese, Hungarian, Turkish, German, Italian, Russian
- Internal language system improved to make adding new languages easier in the future
- No changes to existing translations
Skill Mastery Capes
- 17 mastery capes - Cosmetic cape armor awarded for reaching level 100 in any skill (or total level 1000)
- 16 per-skill capes: Mining, Woodcutting, Excavation, Harvesting, Swords, Daggers, Polearms, Staves, Axes, Blunt, Archery, Unarmed, Block, Acrobatics, Crafting, Building
- 1 Master Cape for total level 1000
- Legendary armor stats - Each cape provides damage resistance, bonus health, and skill-themed bonuses (see v0.7.1). Equips in Chest slot, hides default cape cosmetic
- Reward-only - Not craftable; awarded automatically at level 100 through the existing command rewards system
- Custom item quality - "Mastery Cape" quality (gold text, legendary drop particles)
- Effectively indestructible - 9999 durability
- Fully localized - Cape names and descriptions in Hytale's
items.lang, reward display names in all 9 language files
Technical: Skill Capes Resource Structure
- 17 item JSON definitions in
Server/Item/Items/Armor/Capes/(shared model, per-cape textures) - Skill capes:
ItemLevel: 100, Master:ItemLevel: 200 - All use
Parent: Armor_Cloth_Wool_Head,Quality: MMO_Skill_Cape - Quality definition at
Server/Item/Qualities/MMO_Skill_Cape.json(QualityValue: 6, gold#ffaa00text) - Language files at
Server/Languages/en-US/items.langandgeneral.lang - CommandRewardsDefaults.java - Added
give()call at level 100 for each skill and total level 1000 - New
COLOR_CAPEconstant,formatItemName()andgetItemColor()handle cape IDs - 17 localization keys (
reward.cape.*) added to all 9 language default files - Art assets (model, textures, icons, quality UI) to be created separately
Technical: Localization Architecture Overhaul
- Extracted per-language defaults - Each language's default messages moved from
LocalizationConfig.javainto dedicated classes ini18n/lang/: -
LanguageDefaultsinterface (langCode(),displayName(),getDefaults()) -
EnglishDefaults,SpanishDefaults,FrenchDefaults,PortugueseDefaults,HungarianDefaults,TurkishDefaults,GermanDefaults,ItalianDefaults,RussianDefaults - Registry-based language loading -
LocalizationConfignow uses aBUILTIN_LANGUAGESregistry (LinkedHashMap) instead of hardcoded per-language methods - Reduced
LocalizationConfig.javafrom ~3,272 lines to ~377 lines - No functional changes to existing translations or file-based overrides
Default Server Language
- Server-wide default language - Server operators can now set a default language for new players
- New players inherit the server's chosen language instead of always defaulting to English
- Existing players keep their previously chosen language (only affects new players)
- CLI command -
/mmoconfig language --args=to set the default language - Validates against available (installed) languages
- Shows available language codes if invalid input is provided
- Admin UI - Language selector added to
/mmoadminGeneral Settings section - Dynamic buttons for each available language
- Current selection highlighted in green
- Config persistence - Saved in
skill-config.jsonasdefaultLanguagefield - Config version bumped to 10
- Resets to English on
reloaddefaults - Localized label - "Default Server Language" translated in all 9 language files
XP Reset Now Revokes Item Rewards
- Setting XP lower now properly resets item rewards - When using
/setmmoxpor/bulksetxpto reduce a player's XP, any claimed command rewards (items, boosts) for levels the player no longer qualifies for are now revoked - Per-skill rewards, total level rewards, and global skill rewards are all checked
- The revoked count is included in the existing "rewards revoked" message
- Previously only skill tree rewards were revoked; item milestone rewards were kept even when undeserved
Admin Rewards Editor: Custom Level Input
- Type any level number in the Command Rewards editor - A text input field with "Go" button now appears next to the level preset buttons
- Enter any level number to configure rewards at levels not in the defaults
- Selecting a preset button clears the text field for clean interaction
Bug Fixes
- Fixed Master Cape icon not loading - The Master Cape inventory icon and texture were named incorrectly (
Cape_Grandmaster) and didn't match the item definition (Cape_Master), causing missing icons in-game
Technical: Config Package Reorganization
- Moved per-skill XP defaults to
config/xpdefaults/subpackage (29 files) SkillDefaults.javaupdated with wildcard import for the new subpackage- Config managers, utilities, and top-level dispatchers remain in
config/
Technical: Command Reward Validation
- Added
validateCommandRewards()andvalidateAllCommandRewards()toCommandRewardService- parses claimed reward IDs, checks player level against each, and removes invalid claims - Added
removeCommandReward(String)toSkillComponentfor individual reward ID removal - Both
SetXpCommandandBulkSetXpCommandnow call validation after XP changes
v0.6.4
Leaderboard UX
- Hide XP column in category/total views - The "Total XP" column is now hidden when viewing Total Level, Gathering, Combat, or Production leaderboard views. XP is only shown when drilling down to a specific skill (e.g., Mining). Reduces visual clutter and keeps the focus on level rankings.
- Level breakdown in category/total views - When viewing category or total leaderboard views, each row shows a skill-level breakdown in place of the XP column (e.g., "15 Mining + 6 Woodcutting + 3 Excavation + 1 more"). Shows the top 3 skills by level with a count of remaining active skills. Applies to both leaderboard rows and the "Your Rank" section.
v0.6.3
Bug Fixes
- Fixed item reward commands not executing - Reward commands (like
/give) could fail silently when triggered from the Rewards UI. Commands now execute reliably with proper timing between multiple items. - Fixed inventory space check - Corrected the inventory storage check when claiming rewards.
Technical Details
- Reward commands dispatched via
world.execute()instead of running synchronously inside Store/UI event handlers, avoiding reentrancy issues - Multiple commands within a level staggered with 25ms delays for inventory state updates
- Fixed
getCombinedArmorHotbarStorage()→getStorage()for proper inventory space validation
v0.6.2
Inventory Space Check for Item Rewards
- Inventory space validation - Item rewards now check that you have enough inventory space before claiming
- Full inventory protection - If your inventory is full, rewards stay unclaimed so you can pick them up later
- Red notification tells you when your inventory is too full to claim
- Auto-claim respects space - Level-up auto-claim also checks inventory space, so no items are lost
- Non-item rewards unaffected - Boosts and other non-item rewards are not blocked by inventory checks
- Localized notification in all 8 languages (EN, ES, FR, PT, HU, TR, DE, IT)
Technical Details
- UI claim gating returns "inventory full" notification preserving unclaimed state
- Auto-claim paths (
checkAndExecuteRewards,checkGlobalSkillRewards,checkTotalLevelRewards,checkAllRewards) skip rewards when inventory full - Only
/givecommands are gated;/mmoboost giveand similar pass through
v0.6.1
Boost Stacking Limits
- Hard cap on total XP multiplier - New
maxBoostMultipliersetting (default: 5.0x) prevents runaway XP gains regardless of how many boosts are active - Active boost count limits - Configurable caps on simultaneous personal boosts (
maxPersonalBoosts, default: 3) and global boosts (maxGlobalBoosts, default: 3) - Stack mode selection - Choose between
"additive"(all boosts sum together) or"highest"(best global + best personal only) viaboostStackModesetting - Token preservation on limit - When a player tries to activate a boost token at the limit, the token is NOT consumed and an error message is shown
- Admin boost gating -
/mmoboost globalreturns an error when the global limit is reached instead of silently creating a boost - Boost Page UI awareness:
- Limits info line shows current state:
"Limits: 5.0x cap | additive | Personal 2/3 | Global 1/3" - Token and permission activate buttons are disabled (red "LIMIT REACHED") when the relevant limit is hit
- Admin UI section - New "Boost Stacking Limits" section in
/mmoadminwith: - Text fields for max multiplier, max personal, and max global boosts
- Toggle buttons for additive/highest stack mode
- Save button applies all changes at once
- Localized in all 8 languages (EN, ES, FR, PT, HU, TR, DE, IT)
Admin Button on XP Page
- Quick admin access - Admins now see an "Admin" tab in the
/xpsidebar, just above Settings - Opens the
/mmoadminconfiguration page directly from the skill overview - Only visible to players with
mmoskilltree.adminpermission (or OP when permissions disabled) - Server-side permission check on click prevents unauthorized access
Bug Fixes
- Fixed global boost permissions not working - Global boosts created through the Admin Award Boost UI were silently broken and never appeared. Global boost permissions now work correctly.
- Action required: Any previously awarded global boost permissions need to be re-issued. Re-award them through the Admin UI, or manually replace
.global.with.all.in existing permission entries.
Technical Details
AwardBoostPagegenerated"global"as the scope value in permission strings, butBoostScopeonly recognizes"all"for global boosts- Permissions like
mmoskilltree.xpboosts.all.global.2.30.60silently failed to parse - Now correctly generates
"all"scope (e.g.,mmoskilltree.xpboosts.all.all.2.30.60) skill-config.jsonversion 8 → 9 (auto-regenerated on first load)
v0.6.0
Command Rewards v2.0 - Schema Redesign
Major overhaul of the command rewards system with a unified schema and override-based config.
New Schema
- Unified
rewardslist - All rewards are now commands (items given via/givecommands) - Override-based config - Config stores only customizations, defaults preserved across updates
- Per-command customization:
-
localizationKey- Preferred way to set display name (supports all languages) -
displayName- Fallback literal display name -
color- Hex color for UI display (e.g.,#ffaa00) -
runAs-CONSOLE(default) orPLAYER - Display logic - Rewards without display info are executed but hidden from UI
New Config Format
{
"schemaVersion": 1,
"enabled": true,
"overrides": {
"MINING": {
"10": [
{
"command": "/give {player} Ingredient_Bar_Iron --quantity=25",
"localizationKey": "reward.item.iron_bars",
"color": "#ffaa00"
}
]
}
}
}Override Semantics
- Missing levels use defaults automatically
- Set level to empty array
[]to disable rewards at that level - New defaults propagate without losing your customizations
- Schema version only changes for structural changes
Migration
- Automatic V4 migration - Existing configs are converted and backed up
- Claimed rewards preserved - Level-based tracking maintained across upgrade
Global Skill Rewards
- New reward category: GLOBAL_SKILL - Bonus rewards given to ALL skills at milestone levels
- Tracked per-skill: Mining 50 and Swords 50 each independently trigger level 50 global rewards
- Configurable via
GLOBAL_SKILLkey incommand-rewards.jsonoverrides - Visible in the player Rewards UI as "Lv X Bonus" rows alongside per-skill rewards
- Default: 1.25x Global XP Boost (15 min) at Level 100 for every skill mastered
XP Boost Token Rewards
- Boost tokens as milestone rewards - Level-up milestones now include XP boost tokens
- Personal skill boosts at Lv 50 (1.5x 15min) and Lv 100 (2x 30min) for each skill
- Personal all-skills boosts at Total Lv 100 (1.5x 30min) and Total Lv 350 (2x 45min)
- Global server boosts at Total Lv 500 (1.5x 30min), 750 (2x 30min), 1000 (2x 60min)
- Global boosts broadcast to all online players via
/saywhen activated
Admin Command Rewards UI
- New
CommandRewardsPage- Visual editor for command rewards, accessible from/mmoadmin - Category tabs: Gathering, Combat, Crafting, Misc, Total Level, All Skills (Global)
- Skill selector for per-skill categories, hidden for Total Level and All Skills
- Level selector showing all configured milestone levels
- Reward table with columns: Command, Display Name, Loc Key, Color, Status
- Add/Edit section with fields for Command, Display Name, Loc Key, Color
- Inline Disable button on each default reward to create override without that reward
- Remove button on overridden rewards to delete individual entries
- Disable Level to set empty override (no rewards at that level)
- Reset to Default to remove override and restore defaults
Auto-Derived Display Info
- Item rewards auto-derive display names from item IDs (e.g.,
Ingredient_Bar_Iron→ "Iron Bars") - Item rewards auto-derive tier colors from item ID patterns:
- Common materials (Iron, Copper):
#c8c8c8 - Uncommon (Cobalt, Thorium):
#4aff7f - Rare (Adamantite, Mithril):
#4a9eff - Epic (Onyxium):
#c84aff - Legendary (Prisma, Voidstone):
#ffaa4a - Gems, essences, and tools each have appropriate colors
- Rewards without explicit display info now visible in admin UI with auto-derived names
Bug Fixes
- Fixed incorrect ore XP rates - Removed explicit low-XP entries for special terrain ores (
Ore_Cobalt_Slate,Ore_Adamantite_Magma,Ore_Iron_Basalt) that overrode wildcard patterns with incorrect values (4-6 XP instead of proper ore rates) - Fixed tier label missing in player Rewards UI - localization fallback returned empty string for undefined keys, causing blank tier headers
- Fixed Visible boolean type error - Hytale CustomUI requires boolean values for Visible property, not string representations
- Fixed display name override in tier headers - Auto-derived item names no longer replace "Level X" tier labels
Technical Changes
- CommandRewardEntry.java - New unified format with command, localizationKey, displayName, color, runAs
- LevelRewards.java - New container for level -> List
mapping - CommandRewardsConfig.java - Converted to override-based pattern (like XpMapsConfig)
- Stores
userOverridesseparately from effective config - Generates
_reference/defaults-command-rewards.jsonfor server owner transparency - Changed
configVersiontoschemaVersion - Added
globalSkillRewards/globalSkillOverrideswith full getter/mutator/save/load support - CommandRewardsDefaults.java - All ItemReward definitions converted to
/givecommands - Added
formatItemName()andgetItemColor()for auto-derivation - Added
getGlobalSkillDefaults()with milestone rewards for all skills - Added boost token reward factories:
boostSkill(),boostAll(),boostGlobal() - CommandRewardService.java - Simplified to only execute commands
- Added
checkGlobalSkillRewards()for per-skill global reward tracking - Updated
checkAllRewards()to process global skill rewards - Updated
claimLevelRewards()to handleGLOBAL_SKILL_*keys - CommandRewardsPage.java (NEW) - Admin UI page for viewing/editing command rewards
- CommandRewardsPage.ui (NEW) - Admin UI template with category tabs, skill/level selectors, reward table
- CommandRewardRow.ui (NEW) - Row template with Disable/Remove inline buttons
- ItemRewardsPage.java - Shows global skill bonus rewards alongside per-skill rewards
- Added
claimGlobalaction handler for claiming global skill rewards - Fixed
getDisplayName()to not use auto-derived displayName for tier headers - Messages.java - Updated
getCommandRewardDisplayName()to take level parameter - AdminConfigPage.java - Added "Edit Rewards" button navigating to CommandRewardsPage
v0.5.11
Admin Item Rewards Toggle
- New toggle in
/mmoadmin- Enable/disable item rewards from command-rewards.json - Toggle found in the "General Settings" section
- When disabled, the Rewards tab is hidden from ViewXpPage
- Config saved immediately on toggle
UI Improvements
- Active Boosts Display - ViewXpPage now shows active XP boosts above the buffs bar
- Displays up to 3 active boosts with time remaining
- Shows scope indicator:
[G]for global boosts,[P]for personal - Global boosts shown in blue, personal in green
- Format:
[G] 2.0x Mining - 14:32(scope, multiplier, target, time)
- Skill Tree Buff Indicator - Skill tree buffs now prefixed with
[Tree] - Example:
[Tree] +15 HP +5% DMG - Helps distinguish skill tree rewards from other sources
- Improved Time Display - Boost durations and cooldowns now show human-readable formats
- Under 1 hour:
mm:ss(e.g., "45:30") - 1-24 hours:
Xh Ym(e.g., "2h 30m") - Over 24 hours:
Xd Yh(e.g., "3d 5h") - Fully localized time units for all 8 languages
Leaderboard Fix
- Total Level Calculation - Fixed leaderboard to use summed skill levels
- Previously used formula-based level from total XP (confusing)
- Now sums individual skill levels for intuitive display
- Example: Mining 50 + Woodcutting 50 = Total Level 100 (not formula result)
Technical
- AdminConfigPage.java
- Added
#ToggleItemRewardsbutton with event binding - Calls
CommandRewardsConfig.getInstance().setEnabled()on toggle
- ViewXpPage.java
- Added
populateActiveBoosts()method - Modified
getSkillBuffsText()to add[Tree]prefix - Imports:
ActiveBoost,BoostScope
- ViewXpPage.ui
- Added
#ActiveBoostsBarsection with 3 boost labels - Added
#ActiveBoostsSpacerfor spacing control
- LeaderboardDataStore.java
- Changed
data.totalLevel = skills.getTotalLevel()todata.totalLevel = skills.getSummedTotalLevel()
- LocalizationConfig.java
- Added
ui.admin.item_rewards_labelin all 8 languages - Added
time.days_short,time.hours_short,time.minutes_shortfor localized time units - Added
notify.command_reward_receivedin all 8 languages
- XpBoostService.java
- Updated
formatTimeRemaining()to showXh Ymfor hours,Xd Yhfor days - Added localized overload
formatTimeRemaining(ms, skills)using language-specific time units
- CommandExecutor.java
- Added overload
executeAsConsole(command, username)for better logging context
- CommandRewardService.java
- Now passes username to console command execution for logging
v0.5.10
New Command: `/bulksetxp`
- Bulk XP command - Set XP for one, many, or all players at once
- Usage:
/bulksetxp[targets] - Targets: Single player name, comma-separated names, or
*for all online players - Examples:
-
/bulksetxp mining 1000- Set all players to 1000 mining XP -
/bulksetxp all 0 Player1- Reset all skills to 0 for Player1 -
/bulksetxp swords 5000 A,B,C- Set swords to 5000 for specific players - Aliases:
setxpall,masssetxp - Admin permission required
Command Architecture Update
- Async command pattern -
/mmoboostand/bulksetxpnow useAbstractAsyncWorldCommand - Better console support - commands work reliably from console
- Uses
withDefaultArgfor optional parameters (cleaner syntax) - Store operations properly dispatched to world thread via
world.execute()
- Simplified
/mmoboostsyntax - Removed--args=prefix - Old:
/mmoboost give --args=Steve|mining|2|30 - New:
/mmoboost give Steve|mining|2|30 - Still uses
|(pipe) to separate values
Technical
- BulkSetXpCommand.java - New command extending
AbstractAsyncWorldCommand - Uses
withDefaultArg("targets", ...)with default*for all players - Store access via
entityRef.getStore()insideworld.execute()callback - Validates rewards after XP change (revokes if level dropped)
- Reports success count and any failures
- BoostCommand.java - Refactored to
AbstractAsyncWorldCommand - Changed from
AbstractPlayerCommandtoAbstractAsyncWorldCommand - Uses
withDefaultArginstead ofwithOptionalArg - Subcommands needing Store access (
give,clear,status) wrapped inworld.execute() - Subcommands without Store access (
global,list) run directly - Simplified message handling (direct strings instead of localization for admin commands)
- CLAUDE.md - Documented async command pattern
- Added "Command Patterns" section explaining
AbstractAsyncWorldCommandvsAbstractPlayerCommand - Critical pattern:
world.execute()required for Store operations in async context - Store thread affinity causes
IllegalStateExceptionif accessed from wrong thread
- hytale-api.md - Added "Async Commands & World Thread" section
- Documents the thread safety requirement for Store access
- Code examples showing wrong vs right approach
-
world.execute(Runnable)dispatches work to world thread
v0.5.9
Skill Permission Filtering
- Permission-based skill visibility - When skill permissions are enabled, skills are now filtered throughout the UI
- Skills without permission are hidden from the XP overview page
- Skills without permission are excluded from total/category level calculations
- Navigation in Skill Tree and Item Rewards pages skips unpermitted skills
- Leaderboard skill filters only show skills the viewer has access to
- Category tabs hidden - Category tabs (Gathering, Combat, etc.) are hidden if player has no permission for any skill in that category
- Leaderboard category filters also hidden when no skills available in that category
- Admin permission help text -
/mmoadminnow shows permission nodes when permissions are enabled - Explains
mmoskilltree.skill.*for all skills - Explains
mmoskilltree.skill.mining,.swords, etc. for individual skills - Explains
mmoskilltree.command.xpfor the/mmoxpcommand
Award Boost Admin Page
- Award Boost Page - New admin UI for granting boost permissions and tokens to players
- Access via "Award Boost" button in
/mmoadmin - Dual input fields:
- UUID field for recurring permissions (uses
/perm user add) - Username field for one-time tokens (uses XpBoostService directly)
- Dynamic skill list - Shows all skills with XP values configured
- Skill buttons wrap after 8 per row using SkillButtonRow.ui template
- Target categories: All, Gathering, Combat, Production
- Target individual skills: Mining, Woodcutting, Swords, etc. (all configured skills)
- Preset + Custom inputs - Use quick buttons or type custom values
- Multiplier: 1.5x, 2x, 3x, 5x, 10x presets + custom field
- Duration: 15m, 30m, 1h, 2h, 24h presets + custom field (minutes)
- Cooldown: None, 1h, 24h, 7d presets + custom field (minutes)
- Choose scope (Personal or Global)
- Live permission preview updates as you configure
- Rate limiting - 500ms cooldown prevents button spam
- Improved readability with lighter text colors
- Full localization in all 8 languages
Technical
- ViewXpPage.java -
getFilteredSkills()now filters byPermissionUtil.canGainSkillXp() - Total/category calculations use filtered skill list
-
hasUnclaimedItemRewards()filters by permission - Added
hasPermissionForCategory()to check if player has any permitted skills in a category - Category tabs hidden when
hasPermissionForCategory()returns false
- SkillTreePage.java - Permission check before displaying skill tree
- Shows "No permission" message if player lacks skill permission
- Navigation methods filter by permission
- LeaderboardPage.java -
getSkillsForCategoryFiltered()filters skill buttons by permission - Category filter buttons hidden when no skills available in that category
- ItemRewardsPage.java - Permission check for skill rewards (total rewards always available)
- Navigation methods filter by permission
- AdminConfigPage.ui - Added
#PermissionsHelplabel and#AwardBoostBtnbutton
- AwardBoostPage.java - New admin page for awarding boost permissions and tokens
- Dual input: UUID for recurring permissions, Username for one-time tokens
- Dynamic skill buttons from
getConfiguredSkills()using SkillButton.ui + SkillButtonRow.ui templates - Skill buttons wrap after 8 per row (SKILLS_PER_ROW constant)
- Custom text fields for multiplier, duration, cooldown (override presets)
- Recurring permissions use
CommandExecutor.executeAsConsole()with/perm user add UUID permission - Token awards call
XpBoostService.awardToken()directly (finds online player for immediate delivery) - Rate limiting with ACTION_COOLDOWN_MS (500ms) prevents spam
- Permission string format:
mmoskilltree.xpboosts.. . . .
- AwardBoostPage.ui - Improved layout and readability
- Scrollable content area (TopScrolling)
- Dynamic skill buttons container with SkillButtonRow.ui template
- Dual text fields for UUID and Username inputs
- Custom text fields for multiplier, duration, cooldown
- Lighter text colors for better readability (#b0c4d8, #8090a0)
- SkillButtonRow.ui - New template for skill button rows (LayoutMode: Left, Height: 28)
- BoostCommand.java - Added console execution support
- Checks for ConsoleSender instance to allow
/mmoboostfrom console - Console commands bypass admin permission check
- All subcommands (give, global, list, clear, status) support console execution
- LocalizationConfig.java - Added keys for all 8 languages:
-
ui.skilltree.no_permission- "No permission" -
ui.admin.permissions_help- Permission explanation text -
ui.admin.award_boost- "Award Boost" button label
Bug Fixes
- Luck loot empty values - Empty string values in luck loot tables now correctly give no bonus item
- Allows disabling luck drops for specific blocks by setting empty value
- Disabled cracked ore variants in special terrain to prevent exploits:
-
Ore_Thorium_Mud_Cracked,Ore_Cobalt_Slate_Cracked,Ore_Adamantite_Magma_Cracked,Ore_Iron_Basalt_Cracked - XP maps also updated to disable XP for these cracked ore variants
- Disabled XP patterns - Patterns with 0 or negative XP (-1) are now properly skipped for combat skill detection
- Disabled weapons don't receive skill bonuses (damage, crit, lifesteal)
- Consistent behavior: disabled pattern = no XP and no combat bonuses
- Added comment clarifying
bestXp > 0check semantics
Config Compatibility
- No config version bump required - uses existing
skillPermissionsEnabledsetting - Existing permission nodes unchanged (
mmoskilltree.skill.*,mmoskilltree.command.*)
v0.5.8
Skill Overview UX Redesign
- Compact buff bar - All active buffs shown in a single horizontal bar at the top
- Shows: XP Boost, Damage, Block, Crit, Lifesteal
- Bright color-coded values for easy reading
- Displays best values across all combat skills
- Inline skill buffs - Each skill row now shows claimed rewards inline
- Buffs appear below the progress bar:
+15 HP +5% DMG +3% LUCK - Values of same type are combined (e.g., +5 HP + +10 HP = +15 HP)
- See exactly what bonuses each skill provides at a glance
- Improved readability - Better contrast and sizing
- Lighter text colors for better visibility on dark background
- Larger font sizes for buff displays
- XP values show whole numbers for millions (1M instead of 1.2M)
- Wider skill name and XP columns
- Streamlined header - Stats moved to right side of title
- Total Level, Total XP, Skills count in compact layout
- More space for skill progress list
Permissions System
- Reload permissions command -
/mmoconfig reloadperms - Reloads boost permissions from permissions.json without restart
- Shows count of loaded boost permission templates
- Dynamic boost permissions - XpBoostService reads from permissions.json
- Boost permissions extracted from users and groups sections
- Refresh button in Boost page reloads permissions in real-time
- Supports wildcard permissions (
mmoskilltree.xpboosts.*)
Bug Fixes
- Total level calculation - Now excludes skills without XP map data
- Skills not configured in xp-maps.json don't count toward total
- More accurate representation of actual progress
- Block label - Defense renamed to "Block" in all 8 languages
- More consistent with in-game terminology
Combat Reward Targeting
- Targeted combat rewards - Combat bonuses can now apply to specific weapons/skills
- New
CombatTargetenum: ALL, MELEE, RANGED, MAGIC, and individual skills - MELEE includes: Swords, Daggers, Polearms, Staves, Axes, Blunt, Unarmed
- RANGED includes: Archery
- Rewards default to ALL (backward compatible with existing configs)
- "All Combat" rewards at high tiers - New reward options for combat skills
- Tier 8: +3% Damage (All), +3% Critical (All)
- Tier 9: +4% Damage (All), +4% Critical (All), +2% Lifesteal (All)
- Global bonuses as alternative to skill-specific bonuses
Technical
- ViewXpPage.ui - Redesigned layout with horizontal buffs bar
- SkillRow.ui - Added inline buffs display section
- ViewXpPage.java - New
populateBuffsBar()andgetSkillBuffsText()methods - XpBoostService - File-based permissions loading with reload support
- MMOConfigCommand - Added
reloadpermssubcommand - SkillComponent -
getSummedTotalLevel()now filters by XP map data
Config Compatibility
- No config version bump required -
combatTargetis optional with ALL default - Existing skill-tree.json files work unchanged
- Defense bonus remains global (applies when taking damage, not dealing)
v0.5.7
XP Boost System
- Global and personal XP boosts - Players can activate boosts that multiply XP gains
- Global boosts affect all players on the server
- Personal boosts affect only the activator
- Boosts can target all skills, specific skills, or skill categories
- Multiple boosts stack additively (1.5x + 2x = 2.5x total)
- Boost Tokens - One-time boost awards via commands
- Admins award tokens with
/mmoboost give --args=| | | |[scope] - Tokens can be personal (default) or global scope - global tokens activate server-wide when used!
- Tokens are stored until the player chooses to activate them
- Works for offline players - tokens delivered on next login
- No permission required to activate awarded tokens
- Players activate tokens from the Boost UI when ready
- Permission-based boost activation - Server owners control who can activate repeatable boosts
- Permission format:
mmoskilltree.xpboosts.. . . . - Target:
all, skill name, or category name - Scope:
self(personal) orall(global) - Multiplier uses underscore for decimal:
1_5= 1.5x - Duration and cooldown in minutes
- Example:
mmoskilltree.xpboosts.mining.self.2_0.30.60- 2x Mining, self, 30min, 1hr cooldown
- Boost UI Page - New "Boosts" tab in ViewXpPage with three sections:
- Active Boosts - Currently running boosts (global + personal) with time remaining
- Boost Tokens - One-time tokens awarded via commands, ready to activate
- Reusable Boosts - Repeatable boosts available from permissions (with cooldowns)
- Refresh button to update display
- Admin
/mmoboostcommand - OPs can manage boosts directly (uses--args=with|separator) -
/mmoboost give --args=- Award boost token| | | |[scope] -
/mmoboost global --args=- Activate global boost immediately| | -
/mmoboost list- Show active global boosts -
/mmoboost clear --args=- Clear boosts and tokens -
/mmoboost status --args=- Show boost status including tokens
- Persistence - Boosts and tokens persist across server restarts
- Global boosts saved to
mods/mmoskilltree/active-boosts.json - Pending tokens for offline players saved in the same file
- Personal boosts and tokens stored in player's SkillComponent
- Cooldowns track last activation timestamp
Other Changes
- Defense skill renamed to Block - Display name updated to "Block" across all code and UI
- Updated in all 8 languages (EN: Block, ES: Bloqueo, FR: Blocage, PT: Bloqueio, HU: Blokkolás, TR: Blok, DE: Blocken, IT: Blocco)
- Command UX improvement -
/mmoboostuses--args=with pipe-separated values - Example:
/mmoboost give --args=Steve|mining|2|30
Technical
- New data classes: BoostScope, BoostTarget, ActiveBoost, BoostPermission, BoostToken
- XpBoostService - Singleton service for boost management with token support
- SkillComponent additions: personalBoosts, boostCooldowns, boostTokens maps with codec serialization
- SkillService integration - Boost multiplier applied additively with reward bonuses, token delivery on first action
- BoostPage.java - Interactive UI page for boost management (3 sections)
- BoostCommand.java - Admin command with pipe-separated args and token scope support
- Localization - Full boost UI translations for all 8 languages (EN, ES, FR, PT, HU, TR, DE, IT)
v0.5.6
Override-Based Config System (XpMapsConfig & LuckConfig)
- Configs now store ONLY overrides - Customizations are preserved across mod updates
- File stores only values that differ from defaults
- Missing keys automatically use built-in defaults
- New defaults propagate without losing your changes
- XpMapsConfig: Use value
-1to explicitly disable a default XP pattern - LuckConfig: Use value
"__DISABLED__"to explicitly disable a default luck pattern
- Reference file generation -
mods/mmoskilltree/_reference/ -
defaults-xp-maps.json- All default XP values per skill -
defaults-luck-loot.json- All default luck loot table entries - Auto-generated read-only files for server owner reference
- Updated on every config load
- New commands:
-
/mmoconfig diff --args=- Show only overrides (XP and luck) -
/mmoconfig trim- Remove redundant overrides from both XP and luck configs -
/mmoconfig disable --args=- Explicitly disable a default pattern
- Improved
/mmoconfig list- Now shows override status for each pattern - Cyan text indicates overridden values (shows default in brackets)
- Green text indicates default values
- Header shows override count per skill
- Schema versioning -
schemaVersionreplacesconfigVersionfor override-based configs - Schema version only bumps for structural changes (new fields)
- Default value changes no longer require version bump
- Old configs with
configVersionare read for backwards compatibility
Admin UI
- New
/mmoadmincommand - Opens interactive admin configuration UI - TextField inputs for direct value entry (base XP, scale multiplier)
- Toggle settings: creative mode XP, skill permissions, leveling formula
- Real-time milestone preview updates as you type
- Formula description shows actual calculation being used
- Config override statistics and trim functionality
- Quick actions: reload configs, reload language, trim overrides, reset to defaults
- Aliases:
/mmoconfig-ui,/mmosettings
- XP Overrides Page - Visual editor for skill XP values
- Access via "Edit XP Values" button in Admin Config page
- Category tabs: Gathering, Combat, Crafting, Misc
- Dynamic skill selector buttons per category
- Pattern list showing: name, default value, current value, status
- Status indicators: Default (green), Override (orange), Disabled (red)
- Add/Update patterns with text fields for pattern name and XP value
- Disable button to set patterns to -1 (disabled)
- Reset button per row to revert overrides to defaults
- Click pattern name to copy to text field for easy editing
- Permissions toggle - Single toggle controls both skill and command permissions
- UI label simplified to "Enable Permissions"
- When enabled: skill XP gains and commands require permissions
- When disabled: all players can gain XP and use commands
- Admin permission paradigm - Flexible OP/permission-based access
- OP players always have admin access regardless of permission settings
- When permissions enabled: non-OP players can get admin via
mmoskilltree.admin - Applied to both
/mmoconfigcommand and/mmoadminUI
Localization
- Config command messages fully localized - All 8 supported languages updated
- Override system messages (diff, trim, disable, reset, reload)
- Admin page labels and buttons (20 keys per language)
- Support for: English, Spanish, French, Portuguese, Hungarian, Turkish, German, Italian
- Falls back to English for players without skill data
Technical
- XpMapsConfig.java - Complete rewrite for override-based system
-
userOverridesmap tracks only customizations -
load()now merges file overrides on top of defaults -
save()writes only overrides (smaller config files) -
DISABLED_VALUE = -1sentinel for explicit pattern removal - New methods:
getOverrides(),getAllOverrides(),isDefault(),getDefaultValue(),resetToDefault(),disablePattern(),trimRedundantOverrides()
- LuckConfig.java - Complete rewrite for override-based system
- Same override-based architecture as XpMapsConfig
-
DISABLED_VALUE = "__DISABLED__"sentinel for explicit luck pattern removal - Reference file:
_reference/defaults-luck-loot.json
- ConfigVersionUtil.java - Added
extractSchemaVersion()method - Checks for
schemaVersionfirst, falls back toconfigVersion
- MMOConfigCommand.java - Added handlers for diff, trim, disable commands
- Updated list command to show override indicators
- Updated remove command to show revert-to-default behavior
- All config messages now use localization via Messages helper
- Added
getPlayerSkills()andmsg()helper methods
- LocalizationConfig.java - Added 40+ new localization keys per language
-
cmd.config.*keys for all override system messages -
ui.admin.*keys for admin config page (20 keys per language)
- New files:
-
CommandExecutor.java- Shared utility for executing commands as console or player -
AdminConfigPage.java- Interactive admin configuration UI page -
AdminConfigUICommand.java- Command to open the admin UI -
AdminConfigPage.ui- UI layout for admin configuration page -
XpOverridesPage.java- XP pattern editor page with category/skill navigation -
XpOverridesPage.ui- UI layout for XP overrides page -
XpPatternRow.ui- Template for pattern list rows -
SkillButton.ui- Template for dynamic skill selector buttons
- PermissionUtil.java - Added admin permission support
-
ADMIN_PERMISSION = "mmoskilltree.admin"constant -
hasAdminPermission(Player)- OP always has access; checks permission when enabled -
isOp(Player)- Helper for OP status checks
- CommandRewardService.java - Refactored to use CommandExecutor utility
- Removed duplicate command execution methods (DRY)
- Uses shared
CommandExecutor.executeAsConsole()andCommandExecutor.executeAsPlayer()
- MMOConfigCommand.java - Updated permission paradigm
- Removed hardcoded
setPermissionGroups("OP") - Uses
PermissionUtil.hasAdminPermission()for flexible access control
Bug Fixes
- Fixed combat XP exploit - Players can no longer gain XP from hitting projectiles (arrows, etc.)
-
CombatDamageEventSystemnow detectsProjectilecomponent in entity archetype - Projectiles are always blocked from giving XP (hardcoded, not config-dependent)
v0.5.5
UI Improvements
- Item Rewards Fully Disabled When Config Disabled - Complete protection when item rewards are turned off
- Rewards tab hidden in ViewXpPage
- ItemRewardsPage shows "Item Rewards Disabled" if somehow accessed directly
- All navigation (tabs, arrows) hidden on disabled page
- Event handlers check
isEnabled()before opening rewards page
Upgrade Safety
- Stat Rewards Reapplied on Config Update - Existing players get correct stat values after mod updates
- When SkillTreeConfig version changes, all stat modifiers (Health, Stamina, Mana) are reapplied
- Online players: stats recalculated immediately on reload/reloaddefaults
- Offline players: stats recalculated on first XP action after login
- Prevents stale stat values from old config versions
- Orphaned Reward Cleanup - Removes stat modifiers from rewards that no longer exist
- When skill tree config changes remove tiers or reward IDs, old stat modifiers are cleaned up
-
cleanupOrphanedRewards()now callsremoveOrphanedStatModifiers()for each removed reward - Tries removing all stat types (Health, Stamina, Mana) since original reward type is unknown
Balance Changes
- Block (STAT_DEFENSE) Nerfed by 50% - Block reduction values were too powerful
- All STAT_DEFENSE skill tree rewards reduced by half
- Before: 4% → 20% range across tiers
- After: 2% → 10% range across tiers
- Maximum stacked Block from all skills now ~45% (was ~90%)
- Item Reward Tiers Expanded - Added intermediate tiers for smoother progression with hybrid OSRS formula
- New Level 65 tier for all 16 skills (Expert-level materials)
- New Level 80 tier for all 16 skills (Advanced materials)
- Rewards include mid-to-late game materials like Mithril, Adamantite, Onyxium
- Legendary materials (Prisma, Voidheart, Elemental Essences) at Level 80+
- Total: 6 milestone tiers per skill (10, 25, 50, 65, 80, 100)
Technical
- SkillTreeService.java - Added
reapplyStatRewards()andremoveOrphanedStatModifiers()methods - SkillService.java - Added
setSkillTreeConfigChanged()flag for tracking skill tree config updates - SkillTreeConfig.java - Triggers stat reapplication when config version changes on load
- MMOConfigCommand.java -
validateAllOnlinePlayers()now also reapplies stat rewards - ViewXpPage.java - Added
CommandRewardsConfig.isEnabled()checks for tab visibility and event handling - ItemRewardsPage.java - Added disabled state check in
build()method - CommandRewardsDefaults.java - Added Level 65 and 80 reward entries for all skills
- SkillTreeDefaults.java - Reduced all STAT_DEFENSE values by 50%
- SkillTreeConfig.java - CONFIG_VERSION bumped to 3
- CommandRewardsConfig.java - CONFIG_VERSION bumped to 4
v0.5.4
Improvements
- Milestone XP display - When changing
formula,basexp, orscale, shows XP required for milestone levels - Format:
XP required: Lv10=14.3k | Lv25=95.6k | Lv50=446.2k | Lv80=2.3M | Lv100=10.0M - Helps server owners understand impact of config changes instantly
Technical
- MMOConfigCommand.java - Added
showMilestoneXp()helper method with formatted output
v0.5.3
⚠️ BREAKING CHANGE: Leveling formula completely changed. XP requirements are higher at all levels, so existing players will have lower levels with the same XP. Use /mmoconfig formula --args=legacy to restore the old formula.Hybrid OSRS Leveling Formula
- Complete formula overhaul - Combines OSRS exponential curve with quadratic floor
- Early/mid levels ~10% harder than previous quadratic formula
- Endgame scales exponentially (OSRS-style)
- Level 100 = ~10M XP (true endgame grind)
- New XP requirements (with default settings):
- Level 10: ~14,270 XP
- Level 25: ~95,573 XP
- Level 50: ~446,222 XP
- Level 80: ~2,274,045 XP
- Level 100: ~10,000,000 XP
- Config parameters:
-
baseXpPerLevel(default: 300) - Quadratic floor component -
levelScaleMultiplier(default: 200) - OSRS exponential factor - OSRS component uses
2^(level/7)- each level requires ~10% more XP than the last - Quadratic floor ensures early/mid levels aren't too easy
Improvements
/mmoconfig formula- Switch between hybrid (OSRS) and legacy (quadratic) leveling formulas-
hybrid: OSRS exponential + quadratic floor (default, ~10M XP at level 100) -
legacy: Simple quadratic (~1.5M XP at level 100 with base=150, scale=2) /mmoconfig reload- Now reloads ALL config files (was missing XpMaps, Luck, CommandRewards)/mmoconfig reloaddefaults- Now resets ALL configs to built-in defaults
Bug Fixes
- Item Rewards UI - Fixed crash when viewing Total Level rewards with more than 15 items per tier
- Now supports up to 30 items per tier (6 rows)
Technical
- SkillConfig.java - CONFIG_VERSION bumped to 8, defaults changed to hybrid values (base=300, scale=200)
- SkillComponent.java -
getXpForLevel()andcalculateLevel()use hybrid OSRS formula - ItemRewardsPage.java - Supports up to 30 items per tier (was 15)
- RewardTierRow.ui - Added ItemsRow4-6 for larger reward tiers
- CommandRewardsConfig.java - Added
reloadDefaults()method - MMOConfigCommand.java - Updated reload handlers to include all configs
v0.5.2
Bug Fixes
- Harvest Exploit Prevention - Players can no longer gain XP by placing and harvesting items
- Previously, placing a fruit/crop and then harvesting it would award XP via PickupItemEvent
- Now tracks placed item IDs in addition to block positions
- When you pick up an item you recently placed, XP is skipped
- Separate expiration timer for items:
placedItemExpireMinutes(default: 5 minutes) - Other players CAN still get XP from items you placed (fair multiplayer)
- Life Essence Crafting Exploit Prevention - Currency conversion recipes no longer give Crafting XP
-
Ingredient_Life_Essence_100_Recipe_→ 0 XP -
Ingredient_Life_Essence_Concentrated_Recipe_→ 0 XP
- No +0 XP Notifications - Actions that give 0 XP no longer show notifications
Configuration
placedItemExpireMinutes- New config option inskill-config.json- Minutes until placed items expire for harvest tracking (default: 5)
- Shorter than block expiration since items are typically harvested quickly
- Separate from
placedBlockExpireMinutes(which defaults to 0/never for blocks)
Technical
- SkillConfig.java - CONFIG_VERSION bumped to 5
- Added
placedItemExpireMinutesfield (default: 5) - Added getter/setter methods
- PlacedBlockTracker.java - Extended to track placed item IDs
- New
TrackedItemclass with atomic count and timestamp -
trackPlacedItem(playerHash, itemId)- Track when player places an item -
consumePlacedItem(playerHash, itemId)- Check and decrement on pickup (returns true if should skip XP) - Cleanup in
cleanupExpired()now uses separate expiration for blocks and items -
getTotalTrackedItems()- Stats method for debugging - PlaceBlockEventSystem.java - Now calls
trackPlacedItem()in addition to position tracking - PickupItemEventSystem.java - Checks
consumePlacedItem()before awarding XP - CraftingDefaults.java - Added 0 XP entries for Life Essence conversion recipes
- CraftRecipeEventSystem.java - Removed debug log statement
- SkillService.java - Skip XP processing when bestXp <= 0 (no notifications for 0 XP)
- MMOConfigCommand.java -
/mmoconfig reloadnow reloads ALL configs (was missing XpMaps, Luck, CommandRewards) - MMOConfigCommand.java -
/mmoconfig reloaddefaultsnow resets ALL configs to defaults - CommandRewardsConfig.java - Added
reloadDefaults()method
v0.5.1
New Features
- Target Player XP Command - OPs can now set other players' XP directly
-
/setmmoxp- Set XP for another player--target= - Requires
mmoskilltree.command.setxp.otherspermission (default: OP) - Target must be online and in the same world
- Works with all skills or "all" for bulk changes
- Example:
/setmmoxp mining 1000 --target=PlayerName
- Custom Reward Display Names - Configure display names for item rewards
- Add
displayNamefield to reward entries incommand-rewards.json - Value must be a localization key (e.g.,
"reward.mining.starter_kit") - Add the key to your
localization/messages-*.jsonfiles - Falls back to "Level X" if key not found or not configured
- Localized SetXP Command Messages - All command messages now support localization
- Error messages (permission denied, player not found, invalid XP, unknown skill)
- Success messages (skill set, all skills set, with/without target)
- Localized in all 8 supported languages
Technical
- SetXpCommand.java - Added
--targetoptional argument for targeting other players - Uses
world.getPlayers()to iterate players in the same world (deprecated but functional) -
findPlayerRefByUsername()method finds target player's entity ref - Uses
PermissionUtil.hasCommandPermission(player, "setxp.others")for permission check - Works with existing
MMOSkillTreeAPI.setXp()for data modification - All messages now localized via
Messages.get()helper
- manifest.json - Added new permission node
-
mmoskilltree.command.setxp.others- Set other players' XP with --target (default: OP)
- LocalizationConfig.java - Added setxp command message keys for all 8 languages
-
cmd.setxp.no_permission_others,cmd.setxp.player_not_found,cmd.setxp.no_skill_data -
cmd.setxp.xp_negative,cmd.setxp.unknown_skill,cmd.setxp.available_skills -
cmd.setxp.success_all,cmd.setxp.success_all_target,cmd.setxp.success_skill,cmd.setxp.success_skill_target -
cmd.setxp.rewards_revoked- Notification when rewards are revoked
- SkillTreeService.java - Added reward validation methods
-
validateAndRevokeRewards()- Revokes rewards for a skill when level drops below tier requirements -
validateAllRewards()- Validates all skills (used when setting all XP) - Uses RewardEffectRegistry to properly remove stat modifiers
- SkillComponent.java - Added helper methods for reward management
-
clearClaimedRewardsForTier()- Clears all rewards for a specific tier -
getAllClaimedRewards()- Returns map of tier -> list of reward IDs for a skill
- SetXpCommand.java - Integrated reward validation after XP changes
- Calls
SkillTreeService.validateAndRevokeRewards()for single skill changes - Calls
SkillTreeService.validateAllRewards()for "all" skill changes - Shows revoked count to admin if any rewards were revoked
- CommandRewardEntry.java - Added
displayNamefield for custom reward names - CommandRewardsConfig.java - Parses and saves displayName from JSON config
- Messages.java - Added
getCommandRewardDisplayName()helper for resolving display names - ItemRewardsPage.java - Uses custom displayName if configured
- CommandRewardService.java - Fixed ConsoleSender integration for console commands
Admin Commands
- Reset Item Rewards Command -
/mmoconfig resetrewards --args= - Resets all claimed item rewards for an online player
- Player can re-claim rewards at their current level milestones
- Example:
/mmoconfig resetrewards --args=PlayerName
Balance Changes
- Leveling Curve Rebalanced - Adjusted default leveling parameters for longer progression
-
baseXpPerLevelchanged from 100 to 150 -
levelScaleMultiplierchanged from 1.1 to 1.35 - Creates a steeper curve where high levels require significantly more XP
- Level 50 now requires ~127k XP (was ~69k), Level 100 requires ~1M XP (was ~297k)
- Existing servers can adjust via
/mmoconfig basexpand/mmoconfig scale
- Item Rewards Scaling Improved - Default item rewards scaled up to match harder leveling curve
- All skill rewards increased 3-5x at higher tiers
- Life Essence added to all skills at every tier
- Level 100 rewards now include ~100 Life Essence, ~80 rare bars, ~10-15 Voidheart
- Total Level rewards scaled up significantly (e.g., Total 1000 now gives 1000 Life Essence)
- Elemental essences (Fire, Ice, Lightning) quantities increased proportionally
Reward Validation
- Automatic Reward Revocation - Rewards are validated and revoked when player no longer qualifies
- Triggers when XP is reduced via
/setmmoxp - Triggers when leveling config changes:
/mmoconfig basexp,/mmoconfig scale,/mmoconfig reload,/mmoconfig reloaddefaults - Offline players are validated on their first skill action after logging in
- Revokes all rewards from tiers where player level is now below requirement
- Removes stat modifiers (health, stamina, mana) via RewardEffectRegistry
- Displays count of revoked rewards to admin
Config Refactoring
- Config File Split - Separated large SkillConfig into focused configs for easier management
-
skill-config.json- General settings only (leveling formula, permissions, placed blocks, entity blacklist) -
xp-maps.json(NEW) - XP values per skill (block patterns, weapon tiers, etc.) -
luck-loot.json(NEW) - Luck bonus loot tables (block pattern -> item ID) - Each config has its own version number for independent updates
- XpMapsConfig.java - New singleton config for XP values per skill
- LuckConfig.java - New singleton config for luck loot tables
- SkillConfig.java - Slimmed down, delegates XP/luck methods to new configs (marked deprecated)
- MMOSkillTreePlugin.java - Initializes all new configs on startup
v0.5.0
New Features
- Item Rewards at Skill Milestones - Receive item rewards when reaching specific skill levels
- New config file:
mods/mmoskilltree/command-rewards.json - Supports individual skill milestones (e.g., Mining level 10, 25, 50, 100)
- Supports total level milestones (50, 100, 200, 350, 500, 750, 1000)
- Comprehensive defaults included - All 16 skills have pre-configured item rewards
- Items scale with progression:
- Level 10 (Starter): 2-3 basic items (copper tools, common ingredients)
- Level 25 (Progressing): 3-4 items (iron/cobalt gear, uncommon materials)
- Level 50 (Skilled): 5-6 items (rare materials, gems, better gear)
- Level 100 (Master): 6-10 items including legendary weapons (Flame, Void, Spectral)
- Example: Mining 100 rewards include Flame Pickaxe, Mithril bars, Voidstones
- Manual claiming required - Rewards are NOT auto-claimed; use the Rewards UI
- Commands can also be executed (see Command Rewards below)
- Item Rewards UI Page - New page to view and claim your item rewards
- Access via "Rewards" tab on the Skill Overview page
- Shows all tiers with status: LOCKED (gray), READY (orange), CLAIMED (green)
- View reward contents before unlocking
- CLAIM button to receive items into your inventory
- Items wrap to new lines after 5 per row (supports up to 15 items per tier)
- Toggle between Skill Rewards and Total Level Rewards tabs
- Navigate between skills with arrow buttons
- Rewards tab highlights with orange color and
*when unclaimed rewards available
- Item Reward Notifications - Get notified when new rewards become available
- On level-up: "Mining Lv.10 item rewards available! Open /xp > Rewards"
- On total level milestone: "Total Lv.100 item rewards available! Open /xp > Rewards"
- Localized in all 8 supported languages
- Command Rewards - Server owners can also configure commands to run at milestones
- Placeholders:
{player},{level},{skill},{total_level} - RunAs modes:
CONSOLE(full permissions) orPLAYER(player's permissions) - One-time rewards - commands only execute once per milestone
- Example: Broadcast message at total level 100
Configuration
command-rewards.json- New config file for item and command rewards
`json
{
"configVersion": 3,
"enabled": true,
"rewards": {
"MINING": [
{"level": 10, "displayName": "reward.mining.starter", "giveItems": ["Item_Pickaxe_Copper:1", "Ingredient_Bar_Copper:5"], "runAs": "CONSOLE"},
{"level": 100, "displayName": "reward.mining.master", "giveItems": ["Weapon_Pickaxe_Flame:1", "Rock_Gem_Voidstone:3"], "runAs": "CONSOLE"}
],
"TOTAL": [
{"level": 100, "giveItems": ["Ingredient_Bar_Mithril:10", "Rock_Gem_Diamond:5"], "commands": ["say {player} reached total level 100!"], "runAs": "CONSOLE"}
]
}
}
`
- Per-Config Versioning - Each config file now has its own version number
- Previously: All configs shared a single
CURRENT_CONFIG_VERSION = 10 - Now:
SkillConfig.CONFIG_VERSION = 3,SkillTreeConfig.CONFIG_VERSION = 2,CommandRewardsConfig.CONFIG_VERSION = 3 - Only the changed config gets backed up when updating, not all configs
Technical
- CommandRewardEntry.java - Data class for level/commands/giveItems/runAs
-
ItemRewardinner class for item ID and quantity -
ItemReward.parse()static method to parse "ItemId:Quantity" format - CommandRewardsConfig.java - Singleton config loader following SkillConfig pattern
- CommandRewardsDefaults.java - Comprehensive item reward defaults for all 16 skills
- All item IDs verified against Hytale's item database
- Rewards use actual game items (weapons, tools, ingredients, gems)
- CommandRewardService.java - Checks milestones, gives items, executes commands
-
checkAndExecuteRewards(playerRef, skills, skillType, newLevel)- For skill level-ups -
checkTotalLevelRewards(playerRef, skills)- For total level milestones -
giveItemToPlayer()- Adds items directly to player inventory - Placeholder processing:
{player},{level},{skill},{total_level} - ItemRewardsPage.java - New UI page for viewing and claiming item rewards
-
populateSkillRewards()/populateTotalRewards()- Render reward tiers -
populateItems()- Distribute items across rows (5 per row, up to 3 rows) -
handleClaimSkillReward()/handleClaimTotalReward()- Manual claim handlers - ItemRewardsPage.ui - UI layout with skill/total tabs, tier list, navigation
- RewardTierRow.ui - Template for tier rows with claim button and 3 item rows
- RewardItem.ui - Template for individual item display (icon + quantity)
- SkillComponent.java - Added
claimedCommandRewardsfield for tracking claimed rewards - New codec entry for persistence
- New methods:
hasClaimedCommandReward(),claimCommandReward(),getClaimedCommandRewardIds(),resetCommandRewards() - New method:
getSummedTotalLevel()- Sum of individual skill levels (more intuitive than formula-based total) - ViewXpPage.java - Added "Rewards" tab navigation to ItemRewardsPage
- ViewXpPage.ui - Added
#TabRewardsbutton - ConfigVersionUtil.java - Removed shared
CURRENT_CONFIG_VERSIONconstant - Updated
checkAndBackupIfNeeded(configPath, storedVersion, expectedVersion)to take expected version parameter - SkillService.java - Integrated CommandRewardService after level-up detection
- MMOSkillTreePlugin.java - Loads CommandRewardsConfig in setup()
- LocalizationConfig.java - Added ItemRewardsPage localization keys in all 8 languages
-
ui.rewards.title,ui.rewards.total_title,ui.rewards.level,ui.rewards.claimed,ui.rewards.available,ui.rewards.locked,ui.rewards.claim, etc.
v0.4.9
New Features
- Deployable XP Exploit Fix - Totems and turrets no longer give combat XP
- Prevents XP farming by repeatedly hitting player-deployed entities
- Lays groundwork for future entity blacklist customization
- New command:
/mmoconfig blacklist- view current blacklist (expansion planned) - Config:
combatXpEntityBlacklistarray inskill-config.json
UX Improvements
- Fractional XP Accumulator - Small XP bonuses (like +5%) now properly accumulate instead of being lost to rounding
- Previously: 10 XP with 5% bonus →
10 * 1.05 = 10.5→ truncated to 10 (bonus lost!) - Now: Fractional remainder (0.5) is saved and added to next XP gain
- Example: Gain 1: 10.5 → award 10, store 0.5 | Gain 2: 10.5 + 0.5 = 11.0 → award 11
- Players see the bonus manifest every few actions (deterministic, no RNG)
- Mathematically accurate over time - no XP is ever lost
Technical
- SkillComponent.java - Added
fractionalXpMap(ConcurrentHashMap) - New codec entry
{SKILL}FracXpfor persistence - New methods:
getFractionalXp(skill),setFractionalXp(skill, amount) - SkillService.java - Updated
addXp()to use fractional accumulator pattern - Calculates exact XP with bonus multiplier
- Adds accumulated fractional remainder from previous gains
- Awards integer portion, saves decimal remainder for next time
- SkillConfig.java - Added
combatXpEntityBlacklistfield with pattern matching -
isEntityBlacklisted(entityType)- check if entity matches any blacklist pattern -
addEntityBlacklist(pattern)/removeEntityBlacklist(pattern)- manage entries - CombatDamageEventSystem.java - Added entity blacklist check before awarding combat XP
-
getEntityType()- extracts entity archetype name from chunk - Blacklisted entities still take damage/trigger effects, just no XP awarded
- MMOConfigCommand.java - Added
/mmoconfig blacklistsubcommand - ConfigVersionUtil.java - Bumped to version 10
v0.4.8
New Features
- Placed Block XP Exploit Prevention - Players can no longer gain XP by breaking blocks they placed
- Tracks block positions when players place blocks
- Breaking a self-placed block awards no XP and removes tracking
- Other players CAN still get XP from blocks you placed (fair multiplayer)
- Applies to Mining, Woodcutting, Excavation block breaks (not item pickups)
- Configurable via
trackPlacedBlocks(default: true) - Optional expiration via
placedBlockExpireMinutes(0 = never expire)
Balance Changes
- Building XP Simplified - Changed from dynamic 50% of gathering values to flat 4 XP per block
- All placed blocks now give 4 XP regardless of block type
- Previously calculated as 50% of the mining/woodcutting/excavation value
- Simpler and more predictable progression
- Mushroom XP Reduced - Reduced mushroom harvesting XP from higher values to 1 XP
-
Plant_Crop_Mushroom_andPlant_Mushroom_now give 1 XP - Prevents easy XP farming from abundant mushrooms
Localization
- Italian Language Support - Full translation of all UI, notifications, skills, and rewards
- New language file:
messages-it.json - 8 languages now supported: English, Spanish, French, Portuguese, Hungarian, Turkish, German, Italian
Configuration
trackPlacedBlocks- Enable/disable placed block tracking (default: true)- When enabled, players cannot gain XP from breaking blocks they placed
- Set to false to allow the old behavior
placedBlockExpireMinutes- Minutes until placed blocks "become natural" (default: 0)- Set to 0 for blocks to never expire (only removed when broken)
- Set to a positive value (e.g., 30) to allow XP after that many minutes
Technical
- PlacedBlockTracker.java - New singleton service for tracking player-placed blocks
- Uses
ConcurrentHashMapwith player hash -> Set of tracked blocks -
BlockPositionrecord stores x, y, z coordinates -
TrackedBlockrecord stores position and timestamp for expiration - Methods:
trackPlacement(),wasPlacedByPlayer(),removeTracking(),cleanupExpired() - PlaceBlockEventSystem.java - Now calls
PlacedBlockTracker.trackPlacement()on block place - BreakBlockEventSystem.java - Checks
PlacedBlockTracker.wasPlacedByPlayer()before awarding XP - PickupItemEventSystem.java - Same check for interactive pickups (harvesting)
- SkillConfig.java - Added
trackPlacedBlocksandplacedBlockExpireMinutesfields
v0.4.7
Bug Fixes
- Fixed leaderboard crash on multi-world servers - Leaderboard no longer crashes when players are in different world instances (e.g., dungeons, temples)
- Error was:
IllegalStateException: Assert not in thread!when accessing player data across world threads - Now uses cached leaderboard data instead of accessing live data from other world threads
- Online/offline indicator still works correctly (based on player connection status)
- Viewer's own data is still live (same thread)
Technical
- LeaderboardPage.java - Refactored
gatherPlayerData()to avoid cross-thread Store access - Only calls
playerRef.getUsername()on other players (thread-safe) - Uses
LeaderboardDataStorecache for all player XP/level data - Gets live data only for the viewer (who is on the current thread)
v0.4.6
Bug Fixes
- Fixed command permissions blocking all players - Commands like
/mmoxp,/skilltree, and/xpdisplaywere requiring permission nodes even when permission system was disabled - Added
enableCommandPermissionsconfig option (default:false) - When disabled, all players can use all commands without needing
mmoskilltree.command.*permissions - Enable via config file or future admin command to require permission nodes for commands
- Matches the existing behavior of skill permissions (
enableSkillPermissions)
Technical
- SkillConfig.java - Added
enableCommandPermissionsfield with getter/setter and JSON serialization - PermissionUtil.java - Updated
hasCommandPermission()to checkisCommandPermissionsEnabled()before requiring permissions - Added wildcard permission support (
mmoskilltree.command.*) for command permissions
v0.4.5
New Features
- Keb's Katanas Mod Support - Added default XP values for katanas from the Keb's Katanas mod
- Legendary: Keb_Katana_Prisma (55 XP)
- Epic: Keb_Katana_Mithril (48 XP), Keb_Katana_Onyxium (52 XP)
- Rare: Keb_Katana_Cobalt (23 XP), Keb_Katana_Adamantite (28 XP), Keb_Katana_Thorium (28 XP)
- Uncommon: Keb_Katana_Copper (8 XP), Keb_Katana_Steel (8 XP)
- Configurable Permission System - Opt-in permission nodes for skill XP gain and commands
- Permission checking is disabled by default for backwards compatibility
- Enable via
/mmoconfig permissions --args=true - When enabled, players need
mmoskilltree.skill.ormmoskilltree.skill.*to gain XP
- Skill Permissions - Control which players can gain XP in specific skills
-
mmoskilltree.skill.*- Wildcard for all skills (default: granted) -
mmoskilltree.skill.mining- Individual skill permissions - All 16 skills have their own permission node
- Silent deny - players without permission simply don't gain XP (no error message)
- Command Permissions - Control access to plugin commands
-
mmoskilltree.command.xp- /mmoxp command (default: granted) -
mmoskilltree.command.skilltree- /skilltree command (default: granted) -
mmoskilltree.command.xpdisplay- /xpdisplay command (default: granted) -
mmoskilltree.command.setxp- /setmmoxp command (default: op) -
mmoskilltree.command.config- /mmoconfig command (default: op)
- New Admin Command -
/mmoconfig permissions --args= - Toggle skill permission checking on/off
- Displays current state and usage help
Upgrade Notes
- Config version bumped to 8 - Existing configs will be backed up automatically
- Your old config is saved as
skill-config_backup.jsoninmods/mmoskilltree/ - Restore any custom XP values from the backup file after upgrading
Technical
- ConfigVersionUtil.java - Bumped
CURRENT_CONFIG_VERSIONto 8 - LongswordDefaults.java - Added 8 Keb's Katanas with tiered XP values
- manifest.json - Added Permissions array with all skill and command permission nodes
- SkillConfig.java - Added
enableSkillPermissionsfield with getter/setter - PermissionUtil.java - New utility class for permission checking
-
canGainSkillXp(player, skill)- Checks skill permission (respects config toggle) -
hasCommandPermission(player, command)- Checks command permission - SkillService.java - Added permission check in
processTrigger()before awarding XP - Command classes - All 5 commands now check
mmoskilltree.command.permission - GetXpCommand, SkillTreeCommand, XpDisplayCommand, SetXpCommand, MMOConfigCommand
- LocalizationConfig.java - Added
cmd.no_permissionkey to all 7 languages
v0.4.4
New Features
- Offline Player Leaderboard - Leaderboard now displays both online and offline players
- Player data is cached when XP is gained and on server shutdown
- Online players show live data; offline players show cached data
- Visual indicator distinguishes online (green dot) from offline (gray dot) players
- Player count now shows "X online / Y total" format
Technical
- LeaderboardDataStore.java - New singleton service for persistent leaderboard caching
- Stores player skill data in
mods/mmoskilltree/leaderboard-cache.json - Thread-safe with
ConcurrentHashMap - Automatically saves on JVM shutdown via shutdown hook
- SkillService.java - Added leaderboard cache update on XP gain
- LeaderboardPage.java - Merged online player data with cached offline data
- Added
isOnlinefield toPlayerLeaderboardEntry - Added
createEntryFromCache()method for offline player entries - Online/offline visual styling in leaderboard rows
- LeaderboardRow.ui - Added
#OnlineStatusindicator element - MMOSkillTreePlugin.java - Added leaderboard cache initialization and shutdown hook
- LocalizationConfig.java - Added
ui.leaderboard.player_count_onlinekey for all 7 languages
v0.4.3
New Features
- Turkish Localization - Added Turkish (Türkçe) as a supported language
- Full translation of all UI text, notifications, skill names, and reward types
- German Localization - Added German (Deutsch) as a supported language
- Full translation of all UI text, notifications, skill names, and reward types
- Expanded Language Selector - Settings page now supports up to 15 languages
- Increased from 2 rows of 3 buttons to 3 rows of 5 buttons
- Ready for community-contributed translations
Bug Fixes
- Fixed Wan's Wonder Weapons Battleaxe - Corrected typo in item ID
-
WanMine_God_Slayer_Batlleaxe→WanMine_God_Slayer_Battleaxe
Technical
- LocalizationConfig.java - Added
getTurkishDefaults()andgetGermanDefaults()with full translations - LocalizationConfig.java - Added
ensureTurkishDefaults()andensureGermanDefaults()methods - SettingsPage.java - Increased
maxLangButtonsfrom 6 to 15 - SettingsPage.ui - Added
#LangBtn3through#LangBtn14buttons in 3 rows of 5 - SettingsPage.ui - Increased
#LanguageSettingheight from 160 to 204 - SettingsPage.ui - Increased
#LanguageButtonsheight from 80 to 124
v0.4.2
Bug Fixes
- Disabled Luck for Crafting - Removed luck bonus from crafting due to critical item duplication bug
- Luck bonuses were incorrectly duplicating crafted items
- Will be re-enabled in a future update once the underlying issue is resolved
Technical
- CraftRecipeEventSystem.java - Removed
LuckUtil.tryLuckBonus()call
v0.4.1
New Features
- Mighty Staffs Mod Support - Added default XP values for staffs from the Mighty Staffs mod
- Weapon_Staff_Bo_Wood (6 XP)
- Wan's Wonder Weapons Mod Support - Added default XP values for weapons from the Wan's Wonder Weapons mod
- Swords: WanMine_Lethal_Leftovers_Sword, WanMine_Gaia's_Wrath_Sword, WanMine_Quasar_Cosmic_Sword (65-70 XP)
- Longswords: WanMine_Soulblight_Longsword, WanMine_Soulblight_Longsword_v1 (65 XP)
- Daggers: WanMine_Chromatic_Cleaver_Dagger, WanMine_Daybreak_Dagger, WanMine_Heartroot_Dagger, WanMine_Ashthorn_Dagger, WanMine_Nightshade_Dagger, WanMine_Frostburn_Dagger, WanMine_Nightfall_Dagger (58-68 XP)
- Battleaxe: WanMine_God_Slayer_Battleaxe (70 XP)
- Maces: WanMine_Helioram_Mace, WanMine_Maelstrom_Mace, WanMine_Mjollnir_Mace (65-70 XP)
Technical
- StaffDefaults.java - Added Weapon_Staff_Bo_Wood
- SwordDefaults.java - Added Legendary tier with 3 WanMine swords
- LongswordDefaults.java - Added 2 WanMine longswords to Legendary tier
- DaggerDefaults.java - Added Legendary tier with 7 WanMine daggers
- BattleaxeDefaults.java - Added Legendary tier with WanMine_God_Slayer_Battleaxe
- MaceDefaults.java - Added Legendary tier with 3 WanMine maces
v0.4.0
New Features
- Skill Tree Scrolling - Skill tree UI now scrolls when there are many tiers
- XP Progress Bar - Skill tree header shows progress bar to next level with "current / needed" XP format
- STAT_MANA Reward - New reward type for increasing maximum mana
- Complete 10-Tier Reward Redesign - All 16 skills now have 10 tiers of rewards (Lv 5, 10, 15, 20, 30, 40, 50, 65, 75, 100)
Mana System Design
Players start with 0 mana - it must be earned through skill tree choices. Each skill class has a distinct mana identity:
- Staves - THE mana class (360+ max mana potential). Bo staff masters channel mystical energy
- Archery - High mana (290+) for wand and spellbook users
- Crafting - Moderate mana (305) for enchanting and magical item creation
- Block/Harvesting - Low mana (65-180) for survivability or nature attunement
- Physical Combat (Swords, Daggers, Axes, Blunt, Polearms) - Minimal mana (15-38). Warriors rely on steel, not sorcery
- Gathering (Mining, Woodcutting, Excavation) - Minimal mana (23-38). Laborers focus on physical prowess
- Acrobatics/Building - Zero mana. Pure physical disciplines focused on stamina
Tier Structure
| Tier | Level | Choices | Required |
| ---- | ----- | ------- | -------- |
| 0-1 | 5-10 | 2 | 1 |
| 2-3 | 15-20 | 3 | 1 |
| 4-5 | 30-40 | 4 | 2 |
| 6-7 | 50-65 | 5 | 2 |
| 8 | 80 | 5 | 3 |
| 9 | 100 | 6 | 3 |
Technical
- SkillTreePage.ui:
#TiersListLayoutMode changed to TopScrolling - SkillTreePage.ui: Added
#SkillProgressProgressBar in header - SkillTreePage.java: Added progress calculation using getLevelProgress()
- SkillTreePage.java: Fixed ProgressBar.Value to use double instead of String
- SkillTreePage.java: MAX_CHOICES increased from 5 to 6 for tier 9
- RewardType.java: Added STAT_MANA enum
- StatModifierUtil.java: Added applyManaBonus/removeManaBonus methods
- StatManaEffect.java: New effect handler
- RewardEffectRegistry.java: Registered StatManaEffect
- LocalizationConfig.java: Added reward.stat_mana to all 5 languages
- SkillTreeDefaults.java: Complete rewrite with 10 tiers for all 16 skills
v0.3.5
Bug Fixes
- Leaderboard Combat Skill Filters - Fixed combat category only showing 6 of 10 skills in the filter bar
- Added SkillFilter6-9 buttons to support all combat skills
- All 10 combat skills now visible: Swords, Daggers, Polearms, Staves, Axes, Blunt, Archery, Unarmed, Block, Acrobatics
- Archery XP Defaults - Removed magic staves from Archery skill
- Staves (magic/ranged) were incorrectly included in Archery defaults
- Staves now correctly belong only to the STAVES skill (melee staves)
Technical
- LeaderboardPage.ui - Added
#SkillFilter6through#SkillFilter9buttons - LeaderboardPage.java:217 - Changed skill filter loop from
i < 6toi < 10 - ArcheryDefaults.java - Removed
StaffDefaults.getDefaults()call
v0.3.4
New Features
- Leaderboard Page - View player rankings accessible from ViewXpPage
- Shows all players ranked by total level and XP
- Sort by category: Total Level, Gathering, Combat, Production
- Drill down by individual skill within each category
- Your rank displayed at the bottom with highlighting
- Scrollable player list for large servers
Balance Changes
- Leveling Curve Rebalanced - Adjusted
baseXpPerLevelandlevelScaleMultiplierdefaults - Level 120 now requires approximately 1 million XP
- Default
levelScaleMultiplierchanged from 1.0 to 1.1 - Creates more meaningful late-game progression
- Existing servers can adjust via
/mmoconfig basexpand/mmoconfig scalecommands
Technical
- Scrollable UI Pattern -
LayoutMode: TopScrollingenables vertical scrolling in UI containers - New Files:
-
LeaderboardPage.java- Leaderboard page logic with player data gathering and sorting -
LeaderboardPage.ui- Layout with scrolling player list and filter buttons -
LeaderboardRow.ui- Template for individual player rank rows - ViewXpPage.java - Added Leaderboard tab navigation
- ViewXpPage.ui - Added Leaderboard tab button
- LocalizationConfig.java - Added leaderboard localization keys for all 5 languages
v0.3.3
New Features
- Hungarian Localization - Added Hungarian (Magyar) as a supported language
- Full translation of all UI text, notifications, skill names, and reward types
- More Crossbow Tiers Mod Support - Added default XP values for crossbows from the More Crossbow Tiers mod
Bug Fixes
- Config Reload Command - Fixed
/mmoconfig reloadoverwriting user config edits - Manual file edits are now preserved when using reload command
- Version check only runs on server startup, not manual reloads
Technical
- Config Version System - Replaced hash-based version tracking with simple version numbers
-
ConfigVersionUtil.CURRENT_CONFIG_VERSION- Bump this constant when defaults change - Configs now store
configVersionfield instead of computed hash - More reliable: user edits don't trigger false "defaults changed" detection
- SkillConfig.java / SkillTreeConfig.java -
load(false)skips version check for manual reloads
v0.3.2
New Features
- Portuguese Localization - Added Portuguese (Português) as a supported language
- Full translation of all UI text, notifications, skill names, and reward types
- Select Portuguese in Settings page
Bug Fixes
- Settings Language Selector - Fixed backup language files appearing in the language selector
- Files like
messages-en_backup.jsonno longer show as selectable languages - Only valid language files (
messages-XX.json) are displayed
Technical
- LocalizationConfig.java - Added filter to exclude
_backupfiles from language scanning - LocalizationConfig.java - Added
getPortugueseDefaults()with full Portuguese translations
v0.3.1
New Features
- Tier 7 Rewards - Added Level 65 milestone rewards for all 16 skills
- Master-tier bonuses: higher luck, health, damage, defense values
- Each skill gets 5 new reward choices at this tier
Balance Changes
- Stamina Rewards Rebalanced - Base stamina is 10, so rewards were scaled down
- Early tiers (5-15): +1 stamina
- Mid tiers (30-50): +2 stamina
- Master tier (65): +3 stamina
- Maximum stamina from all skill trees is now ~15-20 (was 100+)
Bug Fixes
- Config Backup System - Fixed bug creating nested
_backup_backup_backup...files - Backup files are now skipped when creating backups
- Added automatic cleanup of malformed backup files on startup
- Config Auto-Update - Configs now regenerate from new defaults when mod updates
- Old configs are backed up to
*_backup.jsonbefore regeneration - Ensures players get new tiers and balance changes automatically
Technical
- ConfigVersionUtil.java - Added
cleanupMalformedBackups()method - SkillConfig.java - Regenerates from defaults when hash changes instead of merging
- SkillTreeConfig.java - Same auto-regeneration behavior
- SkillTreeDefaults.java - Added tier 7 for all skills, fixed stamina values
- MMOSkillTreePlugin.java - Calls cleanup on startup for both config dirs
v0.3.0
New Features
- Luck System (renamed from Double Drop) - Complete overhaul of bonus drop mechanics
- Renamed
RewardType.DOUBLE_DROP→RewardType.LUCKthroughout codebase - New configurable
luckLootTable- maps block patterns to specific item drops - Block breaks (Mining, Excavation): Only gives luck bonus if loot table match exists
- Item pickups (Harvesting): Defaults to same item as bonus
- Default loot table includes all ore tiers (Copper → Onyxium) → raw ore items
- Default loot table includes all gems (Diamond, Emerald, Ruby, etc.) → gem items
- Creative Mode XP Disable - XP gains are now disabled by default in creative mode
- New config option
disableXpInCreative(default: true) - Toggle via
/mmoconfig creative --args= - Players in creative mode will not gain XP unless this setting is changed
UI/Text Improvements
- Luck Reward Text - Now specifies which skill the luck bonus applies to
- Before: "+5% Double Drop" (generic)
- After: "+5% Mining Luck" (skill-specific)
- Uses localization system with new
reward.luck_suffixkey - Defense renamed to Block - Clarifies that this stat is different from Hytale's built-in defense
- All reward text now shows "Block" instead of "Defense"
- Updated in all three languages (EN: "Block", ES: "Bloqueo", FR: "Blocage")
Breaking Changes
- Config field
doubleDropLootTablerenamed toluckLootTable - Existing configs will need to rename this field manually
Technical
- New Files:
-
LuckUtil.java- Luck bonus logic (renamed from DoubleDropUtil) -
LuckDefaults.java- Default loot table mappings (renamed from DoubleDropDefaults) - Renamed Methods:
-
SkillTreeService.getDoubleDropChance()→getLuckChance() -
SkillConfig.getDoubleDropLootTable()→getLuckLootTable() -
SkillConfig.getDoubleDropItem()→getLuckItem() - SkillConfig.java - Added
disableXpInCreativefield, renamed loot table fields - SkillService.java - Added creative mode check in
processTrigger() - MMOConfigCommand.java - Added
creativesubcommand for toggling the setting - Messages.java - Added LUCK handling in
getRewardDisplayText()to include skill name - LocalizationConfig.java - Added
reward.luck_suffix, renamed fromreward.double_drop_suffix - SkillTreeDefaults.java - All DOUBLE_DROP rewards renamed to LUCK
v0.2.9
Bugfixes
- Fixed critical hit notification - Now shows multiplier (x1.5) instead of total damage amount
- Fixed lifesteal notification - Removed duplicate
+sign that was appearing in heal messages
Technical
- DoubleDropUtil.java - Extracted double drop logic to shared utility class for DRY
- PickupItemEventSystem - Refactored to use
DoubleDropUtil.tryDoubleDrop()instead of inline implementation
v0.2.8
New Features
- 4 New Melee Combat Skills - Weapon types split from Swords/Axes for more focused progression:
- DAGGERS - Daggers, claws, kunai (high crit chance, fast attacks)
- POLEARMS - Spears, halberds (reach weapons, defensive bonuses)
- STAVES - Bo staffs, melee staves (disciplined, stamina-focused)
- BLUNT - Clubs, maces (heavy hitters, high damage and health)
Bugfixes
- Combat Effect Notifications - Added missing notifications for critical hits, defense blocks, and fall damage reduction (previously only lifesteal had notifications)
Combat Skill Changes
- SWORDS now covers only longswords and one-handed swords (bladed slashing weapons)
- AXES now covers only axes and battleaxes (chopping weapons)
- Daggers, spears, and melee staves moved to their own dedicated skills
- Clubs and maces moved to new BLUNT skill
Skill Tree Additions
- 4 New Skill Trees with thematic reward progressions:
- DAGGERS: High crit scaling (up to +15%), strong lifesteal
- POLEARMS: Balanced offense/defense, high defense bonuses
- STAVES: Stamina-focused, good defense and fall reduction
- BLUNT: Highest raw damage (+14%), highest health (+28%)
Technical
- Config Version Hashing - Config files now track a
defaultsHashto detect when mod defaults change - Automatically creates backup (e.g.,
skill-config_backup.json) before loading if defaults changed - Protects user customizations when updating the mod
- New config files:
DaggersDefaults.java,PolearmsDefaults.java,StavesDefaults.java,BluntDefaults.java - New utility:
ConfigVersionUtil.javafor hash generation and backup management - Updated
SkillType.javawith 4 new enum values - Updated
SkillTreeDefaults.javawith 4 new skill tree methods - Total implemented skills: 16 (was 12)
v0.2.7
Bugfixes
- Fixed Archery skill not gaining XP - Archery now correctly awards XP when dealing damage with bows, crossbows, wands, and other ranged weapons
- Changed Archery trigger type from
DEAL_DAMAGE_PROJECTILEtoDEAL_DAMAGE_PHYSICAL - Removed unused
DEAL_DAMAGE_PROJECTILEtrigger type
v0.2.6
New Features
/setmmoxpCommand - Admin command to set player XP directly-
/setmmoxp- Set XP for a specific skill -
/setmmoxp all- Set XP for all skills at once - Alias:
/setxp - Configurable permission via
setXpRequiredGameModein config
Bugfixes
- Fixed torches giving Woodcutting XP - Torches no longer incorrectly award Woodcutting experience when broken
Configuration
setXpRequiredGameMode- New config option inconfig.json- Controls who can use
/setmmoxpcommand - Values:
"OP"(default),"Adventure","Creative", etc. - Example: Set to
"Creative"to allow creative mode players to use the command
v0.2.5
Bugfixes
- Fixed BONUS_XP reward display - Now correctly shows the target skill name
- Before: "+5% Bonus XP" (generic)
- After: "+5% Mining XP" or "+5% Excavation XP" (skill-specific)
- Extracts skill from reward ID (e.g.,
excavation_xp_1→ Excavation) - Works correctly when skill trees offer XP bonuses for related skills
Technical
Messages.extractSkillFromRewardId()- Parses reward ID to determine target skill- BONUS_XP rewards now display localized skill name + "XP" suffix
v0.2.4
New Features
- Localization System - Full i18n support for all UI text
- Players can select their preferred language in Settings
- Language preference saved per-player in SkillComponent
- Auto-generated English, Spanish, and French language files
- Users can add custom
messages-{lang}.jsonfiles for additional languages
- Language Selector - New section in Settings page
- 6 language button slots in 2 rows of 3
- Currently selected language highlighted in green
- Disabled style for unavailable language slots
- Instant language switching without restart
- Auto-Generated Language Files - Located in
mods/mmoskilltree/localization/
-
messages-en.json- English (auto-generated) -
messages-es.json- Spanish (auto-generated) -
messages-fr.json- French (auto-generated) - Files auto-update with new keys on server start
- Localized Reward Text - Skill tree rewards fully localized
- Reward type names (e.g., "Max Health" → "Santé Max" in French)
- Formatted values with localized patterns (+5%, +10, +3 blocks)
- Reward claimed notifications use localized text
- Hot-Reload Support -
/mmoconfig reloadlangcommand - Reload language files without server restart
- New translations take effect immediately
Technical
- New
i18n/package:
-
LocalizationConfig.java- Singleton config manager for language files - Loads/saves JSON message files using GSON
- Scans for available
messages-*.jsonfiles - Placeholder substitution with
{0},{1}format - Merges file contents with defaults (preserves user customizations while adding new keys)
- Always falls back to English defaults (never shows raw keys)
-
Messages.java- Static helper class for message access -
get(skills, key)/get(skills, key, args...)- Get translated message -
getSkillName(skills, skillType)- Get localized skill name -
getRewardTypeName(skills, rewardType)- Get localized reward type name -
getRewardFormattedValue(skills, reward)- Get localized formatted value -
getRewardDisplayText(skills, reward)- Get full localized reward text
- SkillComponent.java - Added
languagefield (default: "en")
- Codec entry for persistence
-
getLanguage()/setLanguage()methods
- UI Localization - All UI text now uses Messages helper
- ViewXpPage - Panel titles, tabs, labels, buttons
- SkillTreePage - Tier labels, status text, buttons
- SettingsPage - All setting labels and descriptions
- SkillRow/TierRow templates - Dynamic text via indexed selectors
- SettingsPage.ui - Added element IDs for dynamic localization
-
#SettingsTitle,#XpNotificationsLabel,#XpNotificationsDesc -
#CombatEffectsLabel,#CombatEffectsDesc -
#XpThresholdLabel,#XpThresholdDesc -
#LanguageLabel,#LanguageDesc -
#LangBtn0through#LangBtn5with@DisabledButtonStyle
- ViewXpPage.ui - Added
#SkillsHeaderID for localization
- MMOConfigCommand.java - Added
reloadlangsubcommand
Message Key Structure
{
"language.name": "English",
"ui.button.close": "CLOSE",
"ui.button.back": "< BACK",
"ui.viewxp.title": "Skill Overview",
"ui.viewxp.level_prefix": "Lv. {0}",
"ui.settings.language": "Language",
"notify.xp_gain": "+{0} {1} XP",
"notify.level_up": "LEVEL UP! {0} is now Level {1}!",
"skill.mining": "Mining",
"reward.stat_health": "Max Health",
"reward.bonus_xp": "Bonus XP",
"reward.format.percent": "+{0}%",
"reward.format.flat": "+{0}"
}Adding Custom Languages
1. Create mods/mmoskilltree/localization/messages-{code}.json
2. Copy structure from messages-en.json
3. Translate values (keys must remain the same)
4. Run /mmoconfig reloadlang or restart server
5. Language appears in Settings selector
v0.2.3
New Features
- Dynamic Skill Tree Choices - Configurable number of choices per tier (up to 5)
- New
choicesRequiredfield onSkillTreeNode- how many rewards to pick per tier - Global
defaultChoicesRequiredin config (default: 1) - UI shows "Pick X of Y" when tier has multiple selections
- Tier status shows completion progress (e.g., "1/2" when partially complete)
- Multi-Select Rewards - Players can now pick multiple rewards per tier
- Higher tiers offer more choices with multi-select (Pick 2 of 4)
- Prevents claiming same reward twice
- Tier only marked complete when all required choices made
- Dynamic UI Rendering - Choice buttons rendered dynamically
- New
ChoiceButton.uitemplate appended per choice - Buttons use FlexWeight for automatic sizing
- Supports 2-5 choices per tier
- Removed hardcoded button limits
- Expanded Reward Variety - Updated skill tree defaults
- Tiers 0-1: 2 choices, pick 1
- Tiers 2-3: 3 choices, pick 1
- Tiers 4-5: 4 choices, pick 2
- Tier 6 (Level 50): 5 choices, pick 2 - Elite rewards for all 12 skills
- More diverse reward options per tier
- Tier 6 Elite Rewards (Level 50) - All 12 skills now have elite tier rewards
- Gathering: +20% XP, +15% Double Drops, +20 Health, +15 Stamina, synergy bonuses
- Combat: +10% Damage, +12-15% Crit, +8-10% Lifesteal, +20 Health
- Defense: +12% Defense, +30 Health, -40% Fall Damage, +5% Lifesteal
- Acrobatics: -50% Fall Damage, +20 Health, +8% Defense, +20 Stamina
- Crafting/Building: +20% XP, +20 Health/Stamina, synergy bonuses
Technical
- SkillTreeNode.java - Added
choicesRequiredfield with new constructor - SkillComponent.java - Multi-claim storage using comma-separated reward IDs per tier
-
getClaimedRewardIds(skill, tier)returns list of claimed IDs -
getClaimedCount(skill, tier)returns count for tier -
hasClaimedSpecificReward(skill, tier, rewardId)checks specific claim - SkillTreeConfig.java - Added
defaultChoicesRequiredglobal setting and JSON codec - SkillTreePage.java - Dynamic button rendering with FlexWeight layout
- SkillTreeService.java - Updated claiming logic for multi-select validation
- TierRow.ui - Removed hardcoded buttons, added
#SelectionInfolabel - ChoiceButton.ui - New template for dynamic choice buttons
Config Schema Update
{
"defaultChoicesRequired": 1,
"skillTrees": {
"MINING": [{
"tier": 4,
"levelRequired": 30,
"choicesRequired": 2,
"choices": [...]
}]
}
}Upgrade Notes
Run /mmoconfig reloaddefaults to load the new multi-choice tier defaults.
v0.2.2
Bugfixes
- Fixed double drops not working - Double drops now actually add bonus items to inventory
- Moved double drop logic from
BreakBlockEventSystemtoPickupItemEventSystem - When items are picked up, checks DOUBLE_DROP reward chance for the relevant skill
- Bonus items are added directly to inventory (hotbar prioritized) via
getCombinedHotbarFirst().addItemStack() - Notification now shows item count: "Double Drop! (+3)"
v0.2.1
Bugfixes
- Fixed duplicate component error - Resolved
IllegalArgumentException: Entity contains component typecrash when multiple XP events fired in the same tick (e.g., breaking multiple blocks rapidly or combat with multiple hits) - Added pending component cache to prevent duplicate
addComponentcalls before CommandBuffer consumption
v0.2.0
New Features
- Reward Effect Framework - Pluggable system for reward effects with handler registry
-
RewardEffectHandlerinterface for all effect types -
RewardEffectRegistrysingleton maps RewardType to handlers -
RewardEffectContextprovides store, ref, playerRef, skills to handlers
- 9 Implemented Reward Types - Beyond XP bonuses:
- STAT_HEALTH - Increases max health via Hytale's EntityStatMap
- STAT_STAMINA - Increases max stamina via EntityStatMap
- STAT_DAMAGE - Multiplies outgoing damage
- STAT_DEFENSE - Reduces incoming damage (capped at 90%)
- CRITICAL_CHANCE - Chance for 1.5x damage on attacks
- LIFESTEAL - Heals attacker for percentage of damage dealt
- DOUBLE_DROP - Chance for bonus resources when gathering
- FALL_DAMAGE_REDUCTION - Reduces fall damage (capped at 90%)
- BONUS_XP - Multiplies XP gain (existing)
- Combat Effect Notifications - New settings toggle for lifesteal/crit feedback
- Toggle via Settings > Combat Effect Notifications
- Lifesteal shows green "+X.X" heal notification
- Saved per-player in SkillComponent
- Hytale EntityStatMap Integration - Direct stat modification
-
StatModifierUtilhelper class for health/stamina modifiers - Uses
DefaultEntityStatTypes.getHealth()andgetStamina() -
StaticModifierwithModifier.ModifierTarget.MAXfor permanent bonuses -
healEntity()for lifesteal healing
- Expanded Skill Tree Defaults - 10 tiers with varied reward types
- Gathering skills: BONUS_XP, DOUBLE_DROP, STAT_STAMINA, STAT_DEFENSE, STAT_HEALTH
- Combat skills: BONUS_XP, STAT_DAMAGE, CRITICAL_CHANCE, LIFESTEAL, STAT_HEALTH
- Defense: STAT_DEFENSE, STAT_HEALTH, FALL_DAMAGE_REDUCTION
- Acrobatics: FALL_DAMAGE_REDUCTION (10%→40%), STAT_STAMINA, STAT_DEFENSE
- Debug Starting Level - Config option for testing high-level gameplay
- Set
debugStartingLevelin config.json to initialize new players at that level - Set to 0 (default) for production
Technical
- New
reward/package:
-
RewardEffectHandler.java- Interface for effect handlers -
RewardEffectRegistry.java- Singleton registry -
RewardEffectContext.java- Context holder -
StatModifierUtil.java- Hytale stat integration -
effects/PassiveEffect.java- Base for passive effects -
effects/StatHealthEffect.java- STAT_HEALTH handler -
effects/StatStaminaEffect.java- STAT_STAMINA handler -
effects/NoOpEffect.java- Placeholder for deferred types
- CombatDamageEventSystem enhancements:
- Uses
DamageModule.get().getFilterDamageGroup()to modify damage before health loss -
applyAttackerEffects()- Damage bonus + critical chance -
applyDefenseReduction()- Defense damage reduction -
applyFallDamageReduction()- Fall damage reduction -
applyLifesteal()- Heal on damage with optional notification
- BreakBlockEventSystem enhancements:
-
checkDoubleDrops()- Random check for double drops - Notification when double drop triggers
- SkillComponent additions:
-
showCombatEffectsboolean for combat notification preference - Codec entry for persistence
- SkillTreeService additions:
-
getTotalLifesteal(),getTotalHealthBonus(),getTotalStaminaBonus() - Effect application on claim/reset
Upgrade Notes
Run /mmoconfig reloaddefaults after upgrading to load the new 10-tier skill trees with varied reward types.
v0.1.1
New Features
- Settings Page - New in-game settings UI accessible via the Settings tab on ViewXpPage
- Toggle XP notifications on/off with a single click
- Choose XP gain threshold from preset values (1, 5, 10, 25, 50, 100)
- Visual feedback for current settings state
- Back button to return to skill overview
- Expanded Skill Tree Defaults - All 12 implemented skills now have 6 tiers of rewards
- Previously some skills had only 2-4 tiers
- Consistent progression across Mining, Woodcutting, Excavation, Harvesting, Swords, Axes, Archery, Unarmed, Defense, Acrobatics, Crafting, and Building
- Updated skill pairings for meaningful choices (Harvesting now pairs with Excavation/Crafting instead of unimplemented Cooking/Alchemy)
- Dynamic Skill Tree UI - Skill tree page now uses template-based dynamic rendering
- Automatically scales to display any number of tiers
- Improved visual styling with larger fonts and clearer status indicators
Upgrade Notes
Important: After upgrading to v0.1.1, run /mmoconfig reloaddefaults to load the new 6-tier defaults for all skills. This will reset your skill tree configuration to the new defaults.
v0.1.0
New Features
- Skill Tree System - Unlock XP bonus rewards at level milestones
- 6 tiers per skill, unlocked at levels 5, 10, 15, 20, 30, 40
- Each tier offers a choice between XP bonus for the main skill OR a related skill
- Example: Mining tier lets you choose +5% Mining XP OR +5% Excavation XP
- Rewards persist across sessions via ECS component
- Meaningful Reward Choices - Each skill pairs with thematically related skills:
_(TEMPORARY UNTIL MORE COMPLEX IMPLEMENTATION)_
- Mining ↔ Excavation, Smithing
- Woodcutting ↔ Building, Crafting
- Excavation ↔ Mining, Harvesting
- Harvesting ↔ Cooking, Alchemy
- Swords ↔ Defense, Axes
- Axes ↔ Swords, Woodcutting
- Archery ↔ Crafting, Acrobatics
- Unarmed ↔ Acrobatics, Defense
- Defense ↔ Acrobatics, Swords
- Acrobatics ↔ Defense, Unarmed
- Crafting ↔ Mining, Smithing
- Building ↔ Woodcutting, Crafting
- Skill Tree UI (
SkillTreePage)
- Navigate between skills with arrow buttons
- Shows tier status: LOCKED (gray), AVAILABLE (white), or CLAIMED (green)
- Color-coded buttons show selection state
- Reset button to clear all selections for current skill
- Opens via
/skilltreecommand or Tree button on ViewXpPage
- ViewXpPage Enhancement - Added "Tree" button next to each skill to jump directly to its skill tree
- Bonus XP System - Claimed BONUS_XP rewards apply multiplicatively to XP gains
- Stack bonuses from multiple tiers for compounding effect
- Milestone Notifications - Players notified when new tiers unlock
- Automatic detection when leveling up
- Prompts players to use
/skilltreeto claim
New Commands
/skilltree [skill]- Open the skill tree UI- Aliases:
/st,/rewards
Configuration
- New config file:
config/mmoskilltree/skilltree.json - Configurable level thresholds per tier
- Per-skill reward choices with custom values
- Currently supports BONUS_XP reward type (other types defined but not yet implemented)
Technical
- New data classes:
-
RewardType.java- Enum of reward types (BONUS_XP implemented, others placeholder) -
SkillReward.java- Configurable reward with id, type, value, display name -
SkillTreeNode.java- Milestone with level threshold and reward choices
- New config classes:
-
SkillTreeConfig.java- Singleton config for skill trees with JSON persistence -
SkillTreeDefaults.java- Default rewards with meaningful skill pairings
- New service:
-
SkillTreeService.java- Milestone checking, reward claiming, XP multiplier calculations
- New UI:
-
SkillTreePage.java- Interactive page with claim/reset/navigate actions -
SkillTreePage.ui- Layout with 6 tiers, 2 choices each, navigation and reset buttons
- Extended SkillComponent:
-
claimedRewards- Map of skill → claimed reward IDs per tier -
pendingRewards- Map of skill → pending milestone notifications -
resetRewards(skill)/resetAllRewards()- Clear claimed rewards - String serialization for codec compatibility
- Extended MMOSkillTreeAPI:
-
updateSkillComponent(PlayerRef, SkillComponent)- Explicit update hook
- Modified SkillService:
- Integrated milestone checks on XP gain
- Applies bonus XP multiplier from claimed rewards
v0.0.11
New Features
- Combat XP System - Full implementation of combat skills via
CombatDamageEventSystem
- SWORDS - XP for melee damage with longswords, daggers, spears, claws, staves
- AXES - XP for melee damage with axes, clubs, maces, battleaxes
- ARCHERY - XP for ranged damage with bows, crossbows, wands, spellbooks, guns, bombs
- UNARMED - XP for dealing damage without weapons
- DEFENSE - XP for taking damage from any source
- ACROBATICS - XP for surviving fall damage
- Material-Tiered XP Values - Rare weapons give more XP than common ones
- Common (Crude, Copper, Tribal): 5-6 XP
- Uncommon (Iron, Bronze, Scrap): 7-9 XP
- Rare (Cobalt, Adamantite, Thorium): 20-32 XP
- Epic (Mithril, Onyxium): 45-55 XP
- Legendary (Flame, Void, Spectral): 55-70 XP
- New Skills (in SkillType enum, not yet implemented)
- FISHING, TAMING, REPAIR, ALCHEMY, ENCHANTING, COOKING, SMITHING
Technical
- Separate defaults files for maintainability:
-
SwordsDefaults.java- Longswords, daggers, spears, staves (melee) -
AxesDefaults.java- Axes, clubs, maces (blunt weapons) -
ArcheryDefaults.java- Bows, crossbows, wands, spellbooks, guns, bombs - SkillDefaults.java now delegates to per-skill defaults files
- New TriggerTypes:
DEAL_DAMAGE_PHYSICAL,DEAL_DAMAGE_PROJECTILE(replacedDEAL_DAMAGE) - Weapon detection uses
inventory.getActiveHotbarItem().getItemId()for pattern matching - Patterns match actual item IDs (e.g.,
Weapon_Longsword_Mithril,Weapon_Axe_Iron)
v0.0.10
New Features
- Crafting XP - New
CraftRecipeEventSystemawards XP when crafting items - Building XP - New
PlaceBlockEventSystemawards XP when placing blocks - CRAFTING skill - New general crafting skill type (15 XP per craft by default)
- BUILDING skill defaults - Placing blocks now awards XP at 50% of the breaking value (e.g., placing stone = 2 XP vs mining stone = 3 XP)
Technical
- New
event/CraftRecipeEventSystem.java- HandlesCraftRecipeEventfor crafting XP - New
event/PlaceBlockEventSystem.java- HandlesPlaceBlockEventfor building XP SkillDefaults.getBuildingDefaults()dynamically computes values from mining/woodcutting/excavation at 50%
v0.0.9
New Features
- Quick threshold commands - Set XP display threshold with
/xpd 10,/xpd 20,/xpd 0, etc.
Bugfixes
- Progress bar display - Fixed progress bar rendering in skill UI
v0.0.8
Bugfixes
/mmoconfig reloaddefaults- Now properly saves config after reloading defaults (previously only applied in-memory)
Changes
- Enhanced notifications - XP gain and level up messages now use the notification system instead of chat messages for better visibility
v0.0.7
New Features
- ViewXpPage - New custom UI page for viewing skill stats
- Shows total level, total XP, and number of active skills
- Displays individual skill progress for all configured skills
- Only shows skills with XP configuration data (unconfigured skills hidden)
- Open via
player.getPageManager().setPage(ref, store, new ViewXpPage(playerRef))
Technical
- New
pages/package for custom UI pages - UI files in
src/main/resources/Common/UI/Custom/Pages/ - Updated CLAUDE.md with UI page documentation and naming conventions
v0.0.6
New Features
/mmoconfig reloaddefaults- New admin command to reset all XP values to built-in defaults fromSkillDefaults.java
v0.0.5
New Features
- Comprehensive default XP values for all Hytale blocks based on official item database
- Mining: 10+ rock types, 9 ore tiers (Copper→Onyxium), 7 gem types
- Woodcutting: 30+ tree types across 5 tiers (Common→Special)
- Excavation: All soil types including grass, sand, mud, snow, ash
- Harvesting: Grass, ferns, bushes, flowers, crops, fruits, coral, vines
- PickupItemEventSystem - Awards XP when picking up items
Changes
- Professional package structure - Reorganized codebase:
-
api/- Public API for other plugins -
command/- Command handlers -
config/- Configuration and defaults -
data/- Data models (SkillComponent, SkillType) -
event/- Event system handlers -
service/- Business logic -
util/- Utility classes - SkillDefaults.java - Centralized default XP values (separated from config)
- Updated import paths:
com.ziggfreed.mmoskilltree.api.MMOSkillTreeAPI,com.ziggfreed.mmoskilltree.data.SkillType
v0.0.4
Bugfixes
- Fixed issue with "harvesting" blocks not rewarding xp
v0.0.3
New Skills
New skills added for developers to access via API
New Features
- Public API (
MMOSkillTreeAPI) for other plugins to access player skill data -
getSkillComponent(PlayerRef)- Get full skill data -
getXp(PlayerRef, SkillType)/getLevel(PlayerRef, SkillType)- Get specific skill data -
getTotalXp(PlayerRef)/getTotalLevel(PlayerRef)- Get combined stats -
getAllXp(PlayerRef)/getAllLevels(PlayerRef)- Get all skills as maps -
addXp(PlayerRef, SkillType, long)- Add XP (silent) -
removeXp(PlayerRef, SkillType, long)/setXp(PlayerRef, SkillType, long)- Modify XP -
getLevelProgress(PlayerRef, SkillType)- Get progress to next level (0.0-1.0) -
calculateLevelFromXp(long)/getXpRequiredForLevel(int)- Utility methods -
hasSkillData(PlayerRef)/getSkillTypes()- Helper methods - New skill category: Misc (for Building and future utility skills)
- New trigger types:
PLACE_BLOCK,FALL_DAMAGE,TAME_ENTITY,CATCH_FISH
Changes
/mmoxpnow only shows skills with XP > 0 (cleaner display)- Plugin instance now accessed via
MMOSkillTreePlugin.getInstance()(singleton pattern) - Commands now extend
AbstractPlayerCommandwithPlayerRefparameter - Internal services use API for cache access (DRY principle)
- Consistent cache keying via
PlayerRef.hashCode()
v0.0.2
Changes
- Changed default XP gain threshold for message display from 0 to 10
- Fixed permission issue with
/xpdisplaycommand
v0.0.1 - Initial Release
Skills
- Mining - Gain XP from breaking stone and ores
- Woodcutting - Gain XP from chopping trees and logs
- Excavation - Gain XP from digging soil and dirt
- Harvesting - Gain XP from harvesting plants and crops
Features
- Quadratic XP scaling formula for balanced progression
- Per-player XP display preferences (toggle on/off, set minimum threshold)
- Persistent player data via ECS component codec
- JSON config file with hot-reload support
- Pattern-based block matching for flexible XP configuration
Commands
/mmoxp- View your skill levels and XP progress/xpdisplay- Configure personal XP notification settings-
on/off- Toggle XP gain messages -
threshold --value=- Only show gains >= n XP -
status- View current settings /mmoconfig(OP only) - Server configuration-
list --args=- View XP configurations -
- Set XP for block pattern--args= -
basexp --args=- Set base XP per level -
scale --args=- Set leveling speed -
remove --args=- Remove block pattern -
reload- Reload config from file
Technical
- Dynamic skill system - adding new skills only requires enum entry + defaults
- TriggerType system for future event handlers (combat, crafting, etc.)
- Thread-safe skill caching for command access
- Auto-generates config on first launch