Skip to content

EpochStages — KubeJS & Datapack Guide

How to drive EpochStages (stages, gates, ore-remaps, unlocks) without the in-game editor — from KubeJS scripts, datapack JSON, commands, or Java. The in-game editor just writes the same JSON this guide describes, so everything here is the "source of truth" format.

The KubeJS plugin auto-loads only when KubeJS is installed (registered via kubejs.plugins.txt). Nothing below runs on installs without KubeJS.


The 4 layers (and when to use each)

Layer Where Use it for
KubeJS scripts kubejs/server_scripts/*.js Declaring stages; dynamic grant/check/revoke (gameplay logic)
Datapack JSON kubejs/data/<ns>/<dir>/*.json The actual gating rules (mod locks, ore remaps, gates) — reloads with /reload
Commands chat / quest rewards / command blocks Operator actions; tying quests → stages
Java API another mod Addons calling StageManager / Unlocks

Mental model: rules are a blacklist — content assigned to a stage is locked until that stage. Anything not in any rule is available from the start (the "Dark Age"). There is no allow-list.


1. Declaring stages

A stage just needs to exist before you can gate things behind it. Two equivalent ways:

KubeJS (server_scripts)

// kubejs/server_scripts/stages.js — fires on server start
StageEvents.register(event => {
  event.add('dark_age')                       // id only -> display name = id
  event.add('medieval', 'Medieval')           // id + display name
  event.add('age_of_exploration', 'Age of Exploration')
})

Datapack JSON — kubejs/data/<ns>/epoch_stages/<id>.json

The file name is the stage id. Body fields are all optional:

{
  "display_name": "Medieval",
  "sort_index": 2,
  "description": "Iron, early magic, medieval life.",
  "requires": ["dark_age"]
}
- requires = prerequisite stage ids. /epochstages grant refuses to skip them (use force to override).


2. The Stages binding (runtime API)

Available in any server script. All calls go through the stage manager, so they respect the configured ownership mode (global / player / team).

Stages.has(player, 'medieval')     // boolean — does the player have it?
Stages.grant(player, 'medieval')   // boolean — true if newly granted
Stages.revoke(player, 'medieval')  // boolean
Stages.of(player)                  // Set<String> — every stage the player has
Stages.defined()                   // Set<String> — every declared stage id

player is the normal KubeJS event.player (a ServerPlayer). In global ownership, grant/revoke advance the shared world state (everyone), not just that player.

Example — grant a stage from a trigger:

// kubejs/server_scripts/progression.js
PlayerEvents.advancement(event => {
  if (event.advancement.id == 'minecraft:story/smelt_iron' && !Stages.has(event.player, 'medieval')) {
    Stages.grant(event.player, 'medieval')
    event.player.tell('You have entered the Medieval age!')
  }
})


3. Gating rules (datapack JSON)

Drop these under kubejs/data/<namespace>/<dir>/<anything>.json (any namespace works; the importer scans all of them). Apply changes with /reload.

Matcher tokens (used by items / recipes / mobs / dimensions / ores)

Form Example Matches
exact id minecraft:iron_pickaxe just that id
wildcard minecraft:iron_* * = any run, over the full namespace:path
tag #c:ingots/iron everything in the tag

In mod-stage rules, mods is a list of plain namespaces (e.g. create), not tokens.

3a. Lock whole mods — epoch_mod_stages/

{
  "stage": "age_of_exploration",
  "mods": ["twilightforest", "aether"],
  "except_items": ["*:*book*", "*:*guide*"],
  "except_recipes": [],
  "message": "Locked until the Age of Exploration."
}
- Locks all items/recipes/mobs of those mods until the stage. - except_items / except_recipes = carve-outs that stay available (and visible in JEI/EMI) even though the mod is locked — e.g. pull one starter recipe out of an otherwise-hidden mod.

3b. Ore substitution — epoch_ore_remaps/

While the stage is locked, the ore block is physically swapped (server-side, reversible) to the substitute, so Jade/WAILA/other mods all see the substitute too.

{
  "stage": "medieval",
  "ores": ["#c:ores/iron", "minecraft:iron_ore", "minecraft:deepslate_iron_ore"],
  "substitute_block": "minecraft:coal_ore",
  "substitute_drop": "minecraft:coal"
}
- Specific rules beat broad ones automatically (Exact > Tag > Wildcard), so a "*:*_ore" catch-all can hide every unknown ore while iron keeps its own stage. - coal_ore / deepslate_coal_ore are a hardcoded baseline and are never remapped.

3c. Gate items / recipes / mobs / dimensions — epoch_gates/

For piecemeal gating (not whole mods). Any category is optional.

{
  "stage": "age_of_exploration",
  "message": "You can't enter the Nether yet.",
  "items": ["minecraft:flint_and_steel", "#c:tools/shears"],
  "recipes": ["minecraft:iron_pickaxe"],
  "mobs": ["minecraft:zombie", "iceandfire:*"],
  "dimensions": ["minecraft:the_nether"]
}

3d. Recipe edits — epoch_recipe_swaps / epoch_recipe_removals / epoch_recipe_adds

These exist mainly for the editor. For scripting, prefer KubeJS's native recipe API — it's simpler and more powerful:

ServerEvents.recipes(event => {
  event.remove({ id: 'minecraft:iron_pickaxe' })
  event.shapeless('minecraft:stick', ['minecraft:oak_planks'])
})


4. Per-content unlocks (reveal one thing early)

Besides whole stages, you can unlock a single gated item/ore for a player — the unlock: system. This is how a quest reward can hand you iron_ore while everything else stays gated.

  • Command: /epochstages unlock <players> <token> (and lock to undo)
  • Java: Unlocks.unlock(player, "minecraft:iron_ore")
  • The token is matched against gated content ids — exact, * wildcard, or #tag.
/epochstages unlock @s minecraft:iron_ore
/epochstages unlock @a create:*

5. Commands (/epochstages, op level 2)

Command Effect
grant <players> <stage> grant (refuses if a prerequisite is missing)
force <players> <stage> grant, ignoring prerequisites
revoke <players> <stage> remove a stage
clear <players> remove all stages
list <player> show the player's stages + unlocks
unlock <players> <content> per-content unlock (see §4)
lock <players> <content> undo an unlock
editor open the in-game editor

6. Tying it to FTB Quests

Simplest path — no scripting needed: give the quest a Command reward:

/epochstages grant @s medieval
/epochstages unlock @s minecraft:iron_ore
(or do the same from KubeJS in whatever event you prefer with Stages.grant).


7. Java API (for an addon mod)

StageManager.grant(player, "medieval");            // grant / grantForced / revoke / clear
StageManager.hasStage(player, "medieval");         // boolean
StageManager.getStages(player);                    // Set<String>
StageManager.globalHasStage(server, "medieval");   // global-ownership query
StageManager.missingPrerequisite(player, "x");     // null if ok, else the missing stage id

Unlocks.unlock(player, "minecraft:iron_ore");      // per-content unlock: pseudo-stage
Unlocks.isUnlocked(player, ResourceLocation.parse("minecraft:iron_ore"));

8. Applying changes

You changed… To apply
kubejs/data/** rule JSON /reload (ore swaps re-apply to loaded chunks automatically)
StageEvents.register (declared stages) restart the server (it fires on server start)
Stages.* runtime calls live, no reload
ownership mode (config) restart / re-apply config

Quick reference — directories

kubejs/server_scripts/        Stages binding + StageEvents.register
kubejs/data/<ns>/
  epoch_stages/<id>.json          stage definitions (id = file name)
  epoch_mod_stages/*.json         lock whole mods
  epoch_ore_remaps/*.json         ore substitution
  epoch_gates/*.json              gate items/recipes/mobs/dimensions
  epoch_recipe_swaps|removals|adds/   (prefer KubeJS native recipes instead)