Skip to main content
Version: Minecraft 26.2

Stored data and commands

Every time a player harvests, you record it in two places.

RecordAPIWhere it is savedSent to clients
Crystals the player has gainedEntity dataPlayer save data. Kept after respawningTo that player only
Harvest count of this worldWorld storageWorld folderTo all players

Declarations​

In CrystalGardenMod, add the storage ID, its options, and the entity data key.

CrystalGardenMod.java
/** Garden totals of the world, copied to every client so the HUD can show them. */
static final ResourceId GARDEN_STATS = ResourceId.of(MOD_ID, "garden_stats");

static final StorageOptions GARDEN_STATS_OPTIONS = StorageOptions.DEFAULT.syncedToClients();

/** Crystals each player harvested; saved with the player and sent to that player's client. */
static EntityDataKey harvested;

Create the key inside onInitializeCommon. true means it is sent to that player's own client.

CrystalGardenMod.java (onInitializeCommon)
harvested = mod.content().entityData("harvested", true);

Record the harvest​

Replace the temporary harvest from Growing block.

CrystalSprout.java
/** Gives the crystals of a ripe sprout to {@code player} and records the harvest. */
static void harvest(WorldView world, BlockPos pos, PlayerView player) {
world.setProperty(pos, STAGE, 0);
player.give(CrystalGardenMod.CRYSTAL, CRYSTALS_PER_HARVEST);

StoredData mine = player.data(CrystalGardenMod.harvested);
mine.data().putInt("count", mine.data().getInt("count", 0) + CRYSTALS_PER_HARVEST);
mine.markChanged();

StoredData stats = world.storage(CrystalGardenMod.GARDEN_STATS, CrystalGardenMod.GARDEN_STATS_OPTIONS);
stats.data().putInt("harvests", stats.data().getInt("harvests", 0) + 1);
stats.markChanged();

world.playSound(pos, ResourceId.minecraft("block.amethyst_block.chime"), 1.0f, 1.2f);
world.spawnParticles(
ResourceId.minecraft("happy_villager"), pos.x() + 0.5, pos.y() + 0.6, pos.z() + 0.5, 8, 0.3);
}
  • data() is a DataTree that holds integers, strings, subtrees, and more. After you change it, call markChanged() so it is saved and sent to clients.
  • The same storage ID is always opened with the same options, wherever you open it. If they differ, you get an IllegalArgumentException. That is why the options are kept in one constant.
  • Sounds and particles use vanilla IDs as they are.

Commands and permissions​

Create a new file, GardenCommands.java. Anyone can use /garden stats. Only people with the permission can use /garden grow <position>.

src/main/java/com/example/crystalgarden/GardenCommands.java
final class GardenCommands {

private GardenCommands() {}

static void register(CommandRegistrar commands) {
commands.register(CommandNode.literal("garden")
.then(CommandNode.literal("stats").executes(context -> {
StoredData stats = context.source()
.world()
.storage(CrystalGardenMod.GARDEN_STATS, CrystalGardenMod.GARDEN_STATS_OPTIONS);
int harvests = stats.data().getInt("harvests", 0);
String mine = context.source()
.player()
.map(player -> ", you harvested "
+ player.data(CrystalGardenMod.harvested)
.data()
.getInt("count", 0)
+ " crystal(s)")
.orElse("");
context.source().sendMessage("Crystal Garden: " + harvests + " harvest(s) in this world" + mine);
return harvests;
}))
.then(CommandNode.literal("grow")
.requiresPermission(CrystalGardenMod.growPermission)
.then(CommandNode.argument("pos", CommandArgumentType.blockPos())
.executes(context -> {
if (!CrystalSprout.ripen(context.source().world(), context.getBlockPos("pos"))) {
context.source().sendFailure("There is no crystal sprout there");
return 0;
}
context.source().sendMessage("The crystal sprout is ripe");
return 1;
}))));
}
}

Put ripen in CrystalSprout.

CrystalSprout.java
/** Ripens the sprout at {@code pos} at once; false when there is no sprout. */
static boolean ripen(WorldView world, BlockPos pos) {
if (!world.blockAt(pos).equals(CrystalGardenMod.CRYSTAL_SPROUT)) {
return false;
}
return world.setProperty(pos, STAGE, RIPE);
}

In the entrypoint, create the permission node and register the command. Create the permission node before the command.

CrystalGardenMod.java
/** Who may ripen sprouts with {@code /garden grow}: operators (level 2) unless a permission mod says otherwise. */
static PermissionNode growPermission;

// inside onInitializeCommon
growPermission = mod.permissions().node("grow", 2);
GardenCommands.register(mod.commands());
  • The node ID is crystal_garden:grow. The default is operator level 2. A permission provider (a permission management mod) can allow or deny it per player.
  • The integer a command's execute function returns is the result value that command blocks and /execute store read.

Check​

In game, place one sprout, ripen it at once with /garden grow <x> <y> <z> (the sprout's coordinates, shown as the targeted block on the F3 screen), then harvest it. /garden stats shows Crystal Garden: 1 harvest(s) in this world, you harvested 3 crystal(s). The numbers are still there after you close and reopen the world.

The command argument types and permission providers are in Commands and permissions. Version conversion of stored data is in Stored data and versions.

Next step​

Add the join check.

Buy me a coffee