Game Progression Ore (GPO) is a NeoForge 1.21.1 addon for Game Progression Framework (GPF) that hides blocks from players until they unlock the required stage.
Instead of changing the real world state, GPO sends per-player replacement block states to the client. This lets you gate ores, plants, leaves, or any other block behind progression without actually replacing those blocks on the server.
- Hides blocks per player based on GPF stages.
- Sends fake replacement states only to players who do not have the required stage.
- Keeps the real block in the world unchanged on the server.
- Re-syncs nearby chunks when player stages change.
- Supports Java, CraftTweaker, and KubeJS registration.
- Hide ores until the player reaches a mining tier.
- Replace advanced machines with stone until a tech stage is unlocked.
- Hide biome resources, plants, or leaves until exploration progression is reached.
- Make late-game structures look ordinary until the player has the required stage.
GPO hooks into chunk and block update packets and swaps restricted block states before they are sent to the client.
That means:
- the server still stores the original block
- mining and interaction checks can still respect the hidden state for that player
- different players can see different versions of the same block
- Minecraft 1.21.1
- NeoForge 21.1.x
- Game Progression Framework or the published dependency used by this project
Main entry points:
E:\JavaProjects\GameProgressionOre\src\main\java\dev\sixik\gameprogressionore\api\OreStages.java- Main registration API for block restrictions.
- Use this to bind a stage to a block, block state, or raw block state id.
E:\JavaProjects\GameProgressionOre\src\main\java\dev\sixik\gameprogressionore\api\BlockRestrictionHelper.java- Helper API for resolving the visible replacement block for a player or packet context.
Compat layers:
- CraftTweaker:
E:\JavaProjects\GameProgressionOre\src\main\java\dev\sixik\gameprogressionore\impl\compat\crafttweaker\GPOCraftTweaker.java
- KubeJS:
E:\JavaProjects\GameProgressionOre\src\main\java\dev\sixik\gameprogressionore\impl\compat\kubejs\GPOKubeJS.javaE:\JavaProjects\GameProgressionOre\src\main\java\dev\sixik\gameprogressionore\impl\compat\kubejs\GPOKubeJSPlugin.java
Because GPO depends on finalized GPF stage ids, restrictions should be registered only after stage registration is complete.
Use StageRegisterEvent only to declare stage names.
Example:
bronze_ageore_tier_2my_mod:deep_mining
Do not register block restrictions here.
Use StageRegisterFinalizeEvent to register GPO restrictions.
At this point:
- stage names are already known
- ids are finalized
Stages.getStageId(...)is safe to use- GPO can build its internal restriction table
In short:
StageRegisterEvent= declare stage namesStageRegisterFinalizeEvent= register block hiding rules
If you do not specify a replacement block, GPO falls back to minecraft:stone.
That means these two forms are different:
OreStages.addStage("bronze_age", Blocks.DIAMOND_ORE)-> hidden as stoneOreStages.addStage("bronze_age", Blocks.BIRCH_LEAVES, Blocks.STONE.defaultBlockState())-> hidden as stone explicitly
When you register a Block, GPO applies the restriction to all possible block states of that block.
When you register a BlockState, GPO applies the restriction only to that exact state.
import dev.sixik.gpf.api.event.StageRegisterEvent;
import net.neoforged.bus.api.SubscribeEvent;
public class MyStages {
@SubscribeEvent
public static void registerStages(StageRegisterEvent event) {
event.registerStage("bronze_age");
event.registerStage("deep_mining");
}
}import dev.sixik.gameprogressionore.api.OreStages;
import dev.sixik.gpf.api.event.StageRegisterFinalizeEvent;
import net.minecraft.world.level.block.Blocks;
import net.neoforged.bus.api.SubscribeEvent;
public class MyRestrictions {
@SubscribeEvent
public static void registerRestrictions(StageRegisterFinalizeEvent event) {
OreStages.addStage("bronze_age", Blocks.DIAMOND_ORE);
OreStages.addStage("deep_mining", Blocks.DEEPSLATE_DIAMOND_ORE);
OreStages.addStage("deep_mining", Blocks.BIRCH_LEAVES, Blocks.STONE.defaultBlockState());
}
}OreStages.addStage(
"deep_mining",
Blocks.REDSTONE_ORE.defaultBlockState(),
Blocks.STONE.defaultBlockState()
);short stageId = Stages.getStageId("bronze_age");
OreStages.addStage(stageId, someBlockStateId, replacementBlockStateId);ZenCode class:
mods.gpo.api.OreStages
import mods.gpf.api.events.StageRegisterEvent;
import mods.gpf.api.events.StageRegisterFinalizeEvent;
import mods.gpo.api.OreStages;
events.register<StageRegisterEvent>(event => {
event.registerStage("bronze_age");
});
events.register<StageRegisterFinalizeEvent>(event => {
OreStages.addStage("bronze_age", <block:minecraft:diamond_ore>);
OreStages.addStage("bronze_age", <block:minecraft:birch_leaves>, <blockstate:minecraft:stone>);
});- Register stage names in
StageRegisterEvent. - Register GPO restrictions in
StageRegisterFinalizeEvent. - Use
Blockif you want all states of that block hidden. - Use
BlockStateif you want only one exact state hidden.
KubeJS binding name:
GPOre
GPFEvents.stageRegister(event => {
event.register('bronze_age')
})
GPFEvents.stageRegisterEnd(event => {
GPOre.addStage('bronze_age', Block.getBlock('minecraft:diamond_ore'))
})GPFEvents.stageRegisterEnd(event => {
GPOre.addStage(
'bronze_age',
Block.getBlock('minecraft:birch_leaves'),
Block.getBlock('minecraft:stone').defaultBlockState()
)
})When a player's stages change, GPO re-sends nearby chunks so the client immediately updates visible blocks.
This is handled in:
E:\JavaProjects\GameProgressionOre\src\main\java\dev\sixik\gameprogressionore\impl\events\GPOEvents.java
So after adding or removing a stage, hidden blocks around the player should refresh automatically.
- GPO is visual and packet-based. The original world block remains unchanged.
- Different players can see different block states for the same position.
- If you use leaves or blocks with frequent updates, GPO still re-applies hidden states during chunk and block sync packets.
- Register restrictions only after stage finalization to avoid invalid or missing stage ids.
Use GPO as a simple stage-driven visibility layer:
- Register stage names in GPF.
- Wait for
StageRegisterFinalizeEvent. - Register hidden block rules through
OreStages. - Let GPO send per-player replacement blocks automatically.
This keeps progression logic clean, preserves the real world state, and allows modpacks to hide content without expensive custom world mutation.