NeoOrigins 2.0 — Change Document
Status: ready for 2.0.0 release. All phases landed; alpha.12 → alpha.37 polish + dedicated-server boot port complete on both branches. Last updated: 2026-05-02 Audience: contributors and future-you. This is not a pack-author doc — user-facing docs (POWER_TYPES.md, PACK_FORMAT.md) reflect the post-Phase 7 generic power types.
This document tracks what 2.0 changed, why, and the running state of each phase.
Mission
Collapse 88 PowerType classes → ~25 generic composable types so that future powers are authored as JSON configs against the composable types rather than as new Java classes.
Non-negotiables:
- Every legacy JSON payload keeps working for at least 2 major versions.
- Pack authors never see a hard break — old class IDs are aliased to new generic types.
- Dual-path dispatch during the deprecation window: legacy Java class and new generic type both run side-by-side at every migrated event site.
v2.0.20 — Universal power condition gate (2026-05-02)
All power types now support an optional top-level condition + condition_mode field, parsed in PowerDataManager.parsePower and stored on PowerHolder. The full condition DSL (and/or/not, biome, water, sun, health, etc.) can gate any power without per-type changes.
condition_mode: "deny"(default) — power disabled when condition is truecondition_mode: "allow"— power only operates when condition is true- Gates:
onTick,onActivated,onHit,onKill, capability checks - Ungated:
onGranted,onRevoked,onLogin,onRespawn
Files changed: PowerHolder.java (new fields + isConditionSatisfied), PowerDataManager.java (condition parsing + stripping before codec), ActiveOriginService.java (condition check in hasCapability).
Architecture additions (vs. 1.x)
EventPowerIndex — service/EventPowerIndex.java
O(1) event → handler dispatch, indexed per player + per event. Replaces linear ActiveOriginService.forEachOfType scans for event-carrying powers.
Eventenum — 35 entries: lifecycle/combat (ATTACK,HIT_TAKEN,KILL,DEATH,BLOCK_BREAK, …), Origins-Classes actions (CRAFT_ITEM,FOOD_EATEN,BONEMEAL, …), and modifier-style events (MOD_EXHAUSTION,MOD_KNOCKBACK,MOD_HARVEST_DROPS, …).Handler—BiConsumer<ServerPlayer, Object>; registered per player atPowerType.onGrantedand removed atonRevoked.ModifierHandler—(player, ctx, float base) -> float; multiple handlers chain in registration order. Used for theMOD_*events.- Context records —
HitTakenContext,KillContext,ProjectileHitContext,CraftContext,FoodContext,BlockInteractContext,EntityInteractContext,TradeContext,AdvancementContext. - Lifecycle —
invalidate(uuid)on logout clears both action and modifier indexes.
ActionContextHolder — service/ActionContextHolder.java
ThreadLocal that holds the event context currently being dispatched. Allows action verbs to reach event-specific data (attacker from HIT_TAKEN, cancellable event from FOOD_EATEN) without changing the EntityAction = Consumer<ServerPlayer> signature.
EventPowerIndex.dispatch/dispatchModifierset the context around their handler loop and restore it afterwards — nested dispatches are safe.- Context-aware action verbs read it:
damage_attacker,ignite_attacker,effect_on_attacker,cancel_event.
ActionOnEventPower — power/builtin/ActionOnEventPower.java
The generic event-hook power. Collapses 26 Origins-Classes hook powers into one.
{
"type": "neoorigins:action_on_event",
"event": "food_eaten",
"condition": { ... optional EntityCondition ... },
"entity_action": { ... optional EntityAction ... },
"modifier": { ... optional FloatModifier, or array of them ... }
}
- Custom
Codec<Config>parses JSON viaops.convertTo(JsonOps.INSTANCE, input). - Eager DSL compilation at power-load: condition/action/modifier become closed-over lambdas stored on the config.
onGrantedregisters action and/or modifier handlers separately (a single power can do both) and stores the tokens per-player-per-config so revoke is clean.
LegacyPowerTypeAliases — power/registry/LegacyPowerTypeAliases.java
JSON rewrite table applied by PowerDataManager before codec parsing. Key guard:
if (PowerTypes.get(typeId) != null) return typeId; // stay dormant while Java class exists
All aliases are registered up-front but don’t steal traffic until the corresponding legacy Java class is deleted in Phase 7. One deprecation warning per unique old type per boot (WARNED set).
Phase status
Phase 1 — Foundation (done)
- Client-side power sync infrastructure, capability-tag system
- Flight capability + dimension-change sync
- Wall-climb via
onClimbablemixin - Generic types landed:
ActiveAbilityPower,PersistentEffectPower,ConditionPassivePower,ActionOnEventPower
Phase 2 — Action/Condition DSL (done)
ActionParserwith ~30 verbs (Origins + Apace + NeoOrigins namespaces)ConditionParserwith broad Apace compatModifierParsersupportingadd_base/multiplication/multiply_total_additive/set_total/min_total/max_total
Phase 3 — Native upgrades (done)
- Conditioned
modify_damage_takenvia Route B - Native
invulnerability(replaces lossy FIRE-only translation) - Native
stacking_status_effects area_of_effectiterates nearby players with shape + condition
Phase 4 — Tooling (done)
- In-game origin editor and power tester GUI
- Debug screens for active powers / condition evaluation
- Pehkui scale bridge for sized origins
- Epic Fight compat mixins for sized origins
Phase 5 — Dormant alias tables
Active ability (done): active_launch, active_dash, repulse, active_aoe_effect, active_swap, active_fireball, active_bolt, healing_mist → active_ability with DSL action trees (8 of 15 eligible legacy types — active_phase is deliberately out of scope as a movement-state toggle, not an active ability). The remaining 7 stay standalone because the DSL can’t express their runtime model: active_teleport / active_recall / active_place_block / shadow_orb / ground_slam / tidal_wave. Phase 7 may add raycast/cone/mob-AoE verbs and shrink that set. Lossiness: active_fireball alias fires a single projectile where legacy fired 3–4 with spread; active_swap alias targets the nearest entity in radius where legacy targeted the look-direction pick.
Persistent effect (done): status_effect, stacking_status_effects, night_vision, glow, water_breathing → persistent_effect. Field remaps build the effect-spec array from legacy shapes; stacking_status_effects forces toggleable: false, water_breathing adds condition: origins:in_water and hides the HUD icon.
Attribute modifier (done): less_item_use_slowdown → attribute_modifier with condition: origins:using_item. Phase 3 architectural work (condition + edge-triggered apply/remove) was already complete; six of the originally-scoped ten classes moved to action_on_event under Phase 6 (hunger_drain_modifier, natural_regen_modifier, knockback_modifier, longer_potions, teleport_range_modifier, food_restriction); two more (break_speed_modifier, underwater_mining_speed) are deliberately skipped because NeoForge’s PlayerEvent.BreakSpeed only fires client-side; no_slowdown stays bespoke pending a slowdown-source DSL. less_item_use_slowdown alias is lossy for item_type != "any" (the item-type filter drops).
Condition passive (done): Six legacy environmental passives aliased to condition_passive by composing existing ConditionParser verbs (origins:biome tag, origins:exposed_to_sun, origins:relative_health, origins:submerged_in, combined via origins:and / origins:or / origins:not) with ActionParser verbs (origins:apply_effect, origins:damage, origins:set_on_fire, origins:heal):
biome_buff,damage_in_biome,damage_in_daylight,damage_in_water,burn_at_health_threshold, andregen_in_fluid(reassigned from Phase 2). Four legacy types stay standalone because they don’t fit a tick-condition model:mobs_ignore_player+no_mob_spawns_nearbyare event interceptors;item_magnetismneeds apull_itemsDSL verb;breath_in_fluidneeds adrain_airverb.
Scope note: three legacy types originally lumped under Phase 2 don’t semantically fit persistent_effect and have been reassigned:
breath_in_fluid/regen_in_fluid→ Phase 4condition_passive(tick-driven fluid checks, not MobEffect applications)effect_immunity→ Phase 6action_on_eventwith acancel_evententity_action (event canceler, not a persistent effect)
Phase 6 — Origins-Classes hook consolidation (in progress)
Collapses 26 modifier/action hook powers into action_on_event.
Migrated (14 powers — dormant alias + dual-path dispatch):
| Legacy type | Event | Dispatch site |
|---|---|---|
hunger_drain_modifier | MOD_EXHAUSTION | mixin/FoodDataMixin.neoorigins$modifyExhaustion |
natural_regen_modifier | MOD_NATURAL_REGEN | event/WorldPowerEvents.onLivingHeal |
knockback_modifier | MOD_KNOCKBACK | event/CombatPowerEvents.onLivingKnockBack |
longer_potions | MOD_POTION_DURATION | event/CombatPowerEvents.onMobEffectAdded |
more_animal_loot | MOD_HARVEST_DROPS | event/CombatPowerEvents.onLivingDrops |
efficient_repairs | MOD_ANVIL_COST | event/CraftingPowerEvents.onAnvilUpdate |
better_enchanting | MOD_ENCHANT_LEVEL (additive) | event/CraftingPowerEvents.onEnchantmentLevelSet |
better_crafted_food | MOD_CRAFTED_FOOD_SATURATION (additive) | event/CraftingPowerEvents.boostFoodIfCook |
better_bone_meal | MOD_BONEMEAL_EXTRA (additive) | event/CraftingPowerEvents.onBonemeal |
teleport_range_modifier | MOD_TELEPORT_RANGE | power/builtin/ActiveTeleportPower.execute |
action_on_kill | KILL (action) | event/CombatPowerEvents.onLivingDeath |
action_on_hit_taken | HIT_TAKEN (action) | event/CombatPowerEvents.onIncomingDamage |
thorns_aura | HIT_TAKEN (action, lossy alias) | same |
food_restriction | FOOD_EATEN (cancel, lossy alias) | event/MovementPowerEvents.onItemUseStart |
Phase 6.5 — context-aware verbs (done): ActionContextHolder + action verbs damage_attacker, ignite_attacker, effect_on_attacker, random_teleport, cancel_event. Enabled the last three migrations above.
Not migrated (blocked):
scare_entities— TICK-based iteration over nearby mobs; shape mismatch withEntityAction. Belongs inConditionPassivePower(Phase 7).crop_harvest_bonus— partlyMOD_HARVEST_DROPS(drop count), partly a regrow-block action that needsBlockInteractContext. Needs aneoorigins:regrow_cropaction verb or ConditionPassive migration.
Skipped (deliberately — no viable event):
break_speed_modifier,underwater_mining_speed— attribute-based;PlayerEvent.BreakSpeedis client-side only in current NeoForge— resolved in v2.0.31: now usescraft_amount_bonusPlayerEvent.ItemCraftedEventdirectlymore_smoker_xp— no furnace XP event in NeoForge 21.11.38
Known alias lossiness — all three fixed via context-aware DSL extensions:
thorns_aura: theneoorigins:damage_attackeraction now accepts anamount_ratiofield that readsHitTakenContext.amountand applies the ratio faithfully (min 0.5). Alias mapsreturn_ratio→amount_ratio.action_on_hit_taken:min_damagenow wraps the inner action inorigins:if_elsegated by a newneoorigins:hit_taken_amountcontext-aware condition.food_restriction: item-tag filter is expressed via a newneoorigins:food_item_in_tagcontext-aware condition that readsFoodContext.stack. Whitelist mode wraps it inorigins:not.
Phase 7 — Legacy class retirement (in progress)
First cut (done): deleted 14 loss-free classes — Phase 1 (active_launch, active_aoe_effect, healing_mist, repulse), Phase 2 (status_effect, stacking_status_effects, night_vision, glow), Phase 4 (biome_buff, damage_in_biome, damage_in_daylight, damage_in_water, burn_at_health_threshold, regen_in_fluid). Matching type IDs now route through LegacyPowerTypeAliases at load time.
Second cut (done): retired the 14 Phase 6 dual-path classes — pruned the forEachOfType(player, XxxPower.class, ...) legacy scan from each handler site and relied on the existing EventPowerIndex.dispatchModifier / .dispatch call that was already chaining. Classes deleted: HungerDrainModifierPower, NaturalRegenModifierPower, KnockbackModifierPower, LongerPotionsPower, MoreAnimalLootPower, EfficientRepairsPower, BetterEnchantingPower, BetterCraftedFoodPower, BetterBoneMealPower, TeleportRangeModifierPower, ActionOnKillPower, ActionOnHitTakenPower, ThornsAuraPower, FoodRestrictionPower. FoodContext gained an optional event field so food_item_in_tag conditions and cancel_event actions share a single dispatch context.
Total Phase 7 deletions: 28 classes — 88 → 60.
Deferred:
- Generalise
ConditionalPowerinto the fullConditionPassivePowerpipeline. Current shape is a wrapper around aninner_powerlookup — semantically different from the tick-passive model; needs careful rework. - Collapse
tick_actionintocondition_passive. CurrentTickActionPowerhas externally-dispatched behavior (TELEPORT_ON_DAMAGEaction-type resolved inOriginEventHandler), not a DSL action — aliasing requires that handler to move to event dispatch first. - Remove the migration gradle task (still useful as a diagnostic; retire when no legacy IDs remain in internal JSON).
- User-facing
POWER_TYPES.mdrewrite to documentpersistent_effect/condition_passive/action_on_event/active_abilityas the canonical types and drop retired entries.
Phase 8 — Regression pass (not started)
- Requires runtime; deferred until after Phase 7.
- Full playthrough of all shipped origins with a pack mix of legacy + new JSON.
- Validate dormant aliases activate correctly when legacy classes retire.
Dispatch sites still unwired in Event enum
Reserved for when the first consuming power lands. No current power depends on them.
| Enum entry | Status |
|---|---|
ATTACK | wired, but no action/modifier power uses it yet |
CLIMB | no NeoForge event; belongs in ConditionPassivePower, deferred |
MOD_FALL_DAMAGE | unused |
MOD_TRADE_PRICE, TRADE_COMPLETED, VILLAGER_INTERACT | unused |
ADVANCEMENT_EARNED | PlayerLifecycleEvents.onAdvancementEarned handles origin upgrades but doesn’t call EventPowerIndex.dispatch — wire when first action-on-advancement power lands |
Compat guarantees
All Phase 6 migrations run three paths in parallel so nothing breaks during the deprecation window:
- Legacy JSON + live Java class — e.g. a pack still declaring
neoorigins:hunger_drain_modifier. The Java class (HungerDrainModifierPower) is still registered inPowerTypes, so the JSON parses normally and the dispatch site still runsActiveOriginService.forEachOfType(sp, HungerDrainModifierPower.class, ...)against it. - Dormant alias — same legacy JSON;
LegacyPowerTypeAliases.apply()sees the old type ID but the dormancy guard (if (PowerTypes.get(typeId) != null) return typeId;) short-circuits so the alias never rewrites. Activates automatically the day the legacy Java class is deleted. - New JSON — a pack using
neoorigins:action_on_eventgoes throughActionOnEventPower.onGrantedwhich callsEventPowerIndex.registerModifierorregister. The dispatch site callsEventPowerIndex.dispatchModifier(...)and the new chain runs alongside path (1).
Multipliers fold multiplicatively across paths (1) + (3) at every migrated site — a pack mixing old and new modifiers for the same semantic produces the combined multiplier, not either-or.
Files of note
New (2.0)
service/EventPowerIndex.java— event dispatch coreservice/ActionContextHolder.java— ThreadLocal context bridgepower/builtin/ActionOnEventPower.java— the Phase 6 generic typepower/builtin/ActiveAbilityPower.java— Phase 1 active genericpower/builtin/ConditionPassivePower.java— Phase 1 passive genericpower/builtin/PersistentEffectPower.java— Phase 1 persistent effect genericpower/registry/LegacyPowerTypeAliases.java— the alias rewrite tablecompat/modifier/ModifierParser.java— FloatModifier DSL
Modified (2.0)
compat/action/ActionParser.java— +5 context-aware verbscompat/condition/ConditionParser.java— broadened Apace compatdata/PowerDataManager.java— callsLegacyPowerTypeAliases.applybefore codec parseevent/*.java— every event class gainedEventPowerIndex.dispatch(...)calls alongside legacy scansmixin/FoodDataMixin.java—MOD_EXHAUSTIONdispatchpower/registry/PowerTypes.java— registers the new generic typesservice/ActiveOriginService.java— cached per-player resolved power list
Launch checklist (Phase 7 → 2.0 release)
- Close the 3 known alias lossiness gaps (thorns ratio, food-tag condition, hit-taken min_damage)
- Delete migrated legacy PowerType classes
- Verify each dormant alias activates (JSON smoke tests)
- Merge
ConditionalPowerinto fullConditionPassivePower - Migrate
scare_entities+crop_harvest_bonus(last 2 holdouts) - Update
docs/POWER_TYPES.md— new generic types at top, legacy section marked deprecated with migration recipes - Update
docs/PACK_FORMAT.md— note alias behaviour, deprecation timeline - Wire remaining enum entries or remove them (
MOD_FALL_DAMAGE,MOD_TRADE_PRICE, etc.) - Phase 8 regression: full in-game playthrough with mixed legacy/new pack
- Dedicated-server boot validated on both 1.21.1 and 26.1 (alpha.34+)
- Merge
2.0-dev→master, tagv2.0.0(awaitingpublishcommand)
Phase 8 — alpha.12 → alpha.37 polish & dedicated-server port (2026-04-24/25)
The 2.0 line was singleplayer-tested through alpha.27. Pushing to a dedicated server surfaced classes of crash that singleplayer hides; alpha.28 → alpha.34 fixed those. alpha.35 → alpha.37 is post-boot polish (config audit, EnderMan re-target, mixin priority).
Aquatic-origin overhaul (alpha.11 → alpha.24)
- Dry-out mechanic rewritten as Post-tick handler in
BreathOutOfFluidPowerwith per-player virtual-air tracker, capability emission (dries_out_of_water), respawn cleanup, and direct drown damage on land. NewLivingEntityAirRefillMixincancels vanilla’s+4/tickair refill so the drain actually progresses on land. NewPlayerWaterVisionMixinshort-circuitsLocalPlayer.getWaterVisionto clear underwater fog.GuiHudBarsMixinhides the bubble row when an aquatic origin is submerged. - Master configs:
[ocean_origins].drain_rate_ticks(default 10) anddrown_damage_per_second(default 2.0). The per-powerdrain_rateJSON field on built-in*_dries_outpowers is no longer respected — config drives all four origins globally. - Spawn placement:
LocationConditiontries water passes (ocean floor, water surface) before the land-column pass whenallow_ocean_floor/allow_water_surfaceis set, and rejects land columns that lack sky access. Aquatic origins no longer spawn on tiny islands or in cave air-pockets in ocean biomes.NeoOriginsNetwork.allFillednow skips hidden / empty layers — disabling the class layer no longer strands the player in first-pick invulnerability. - Power list cleanup (abyssal / merling / kraken / siren): drop
daylight_damage(abyssal),scare_ocean(abyssal), andregen_in_water(abyssal / kraken / siren). Addabyssal_aquatic_speed(the only aquatic origin without one) and the sharedaquatic_fish_diet+aquatic_fish_diet_bonusPescivore set. - Fish-only diet: cancels non-fish eat at
food_eatenevent-START (cancellable), and applies cooked-equivalent food/saturation bonuses for raw cod / salmon at the newfood_finishedevent (post-eat, can’t be exploited by releasing right-click). Newfood_item_idcondition for per-item branches insideif_else_list. Newfish_foodsitem tag atdata/neoorigins/tags/item/fish_foods.json. scare_entitieswater-mob fix: was filtering byinstanceof PathfinderMob, which excludes everyWaterAnimal(cod, salmon, squid, dolphin). Loosen toMoband add a velocity-push branch for water mobs whoseWaterBoundPathNavigationsilently fails on unreachable flee targets.
New action: throw_target (alpha.25 → alpha.28)
Raycast the entity under the actor’s crosshair and push them away horizontally + upward with tunable force / vertical_lift / max_distance. Documented in ACTIONS.md and API.md; schema enum updated. 26.1’s ProjectileUtil.getEntityHitResult signature differs from 1.21.1 — added to reference_26_1_api_map.md.
Dedicated-server boot port (alpha.28 → alpha.34)
Six classes of crash that only surface on dedicated server (singleplayer always has client classes already loaded). Captured in feedback_dedicated_server_validation.md. Key fixes:
new ClientScreen(...)opcode in common-side class —NeoOriginsNetwork.handleOpenEditorScreenconstructedOriginEditorScreendirectly. RuntimeDistCleaner walks NEW opcodes during dist verification and tries to load the target; loadingOriginEditorScreentriggersScreenload, rejected on dedicated server. Routed throughClientOriginState.openEditorScreen()trampoline.- NeoForge event class moved between minor versions — 26.1.2.29-beta replaced inner
BlockEvent.BreakEventwith top-levelnet.neoforged.neoforge.event.level.block.BreakBlockEvent. Bumpeddeps.neoforgeto match server. - Mixin INVOKE owner mismatch on refactored vanilla method — 26.1’s
FoodData.ticktakesServerPlayerinstead ofPlayer; INVOKE owner inside the method body shifted accordingly. Updated theFoodDataNoRegenMixintarget. - PowerType class on disk but never registered —
natural_glide(26.1) andhide_hud_bar+cobweb_affinity(1.21.1) had Java classes but missed thereg(...)line inPowerTypes. Powers silently dropped withUnknown power typewarnings; matching origins (Elytrian / Hiveling / Phantom flight on 26.1, Automaton hide-bars + Arachnid cobweb on 1.21.1) had no-op kits. - Mixin target rename —
EnderMan.isLookingAtMe(Player)was renamed to privateisBeingStaredBy(Player)on 26.1 with a syncedDATA_STARED_ATfield. Re-targetedEnderManLookMixin(alpha.36).
Mod-compat mixin priority bumps (alpha.37)
LightTextureMixinandPlayerWaterVisionMixinpriority raised to 1500 (default 1000) so they apply after mods like Alex’s Caves that also mixin into the lightmap pipeline. Tester reportedenhanced_visionbroken under AC; this is the standard mitigation when two mods both write the lightmap.
Config audit (alpha.35)
- Removed dead
power_overridesentries for the four aquatic powers deleted earlier (abyssal_daylight_damage,abyssal_regen_in_water,kraken_regen_in_water,siren_regen_in_water). - Added
abyssal_aquatic_speedoverride. - Renamed
breeze_wind_dashfieldstrength→power(was silently broken — JSON has top-levelpower). - Removed
cinderborn_lava_regen/strider_lava_regen/umbral_active_dashamountoverrides — values are nested insideentity_actionand the shallow override system cannot reach them. Comments left explaining; pack authors who want to retune copy the JSON.
Validation status
- Both branches: dedicated server boots clean.
- Both branches: 1.21.1 + 26.1 commits land in
2.0-dev-1.21.1/2.0-dev(5–6 commits each since alpha.10). - Tester smoke-test of alpha.37 specific items pending: Enderian gaze immunity on 26.1, AC enhanced-vision compat, fish-diet bonus timing, master drain-rate config behaviour, ocean-floor spawn placement.
Carry-forward / non-blocking
- Sodium-specific enhanced-vision compat — separate workstream, not yet attempted.
- ~130 orphan power JSONs without
power_overridesentries — only add when a tester asks for a knob. - Schema files (
docs/schema/*.json) — session additions (throw_target,food_item_id) wired into the appropriate enums. Pre-existing drift remains: condition schema is short several pre-2.0 verbs, action schema is short a few, and power schema lists ~half of the 74 power types. Out of scope for 2.0 launch; track separately.