Skip to content

GameStateSavedData

GameStateSavedData<T> is a thin base over vanilla SavedData that packages the recurring four-piece boilerplate — SavedDataType field, computeIfAbsent getter, codec wiring, and a setDirty helper — into a single subclass-able shape. Use it for per-instance markers and simple replay state (“round started”, “round index”). Mid-game state restoration (eliminated set, target picks, timers) stays in the consumer mod’s own state object.

  • You need durable per-instance state (the slot’s saved data) without writing the vanilla boilerplate.
  • You want a one-call mutator that marks dirty automatically.
  • You want your state class to be loadable with a single static call.
package me.zlex.conduit.state;
public abstract class GameStateSavedData<T extends GameStateSavedData<T>> extends SavedData {
protected static <T extends GameStateSavedData<T>> SavedDataType<T> type(
String namespace, String path,
Supplier<T> factory, Codec<T> codec);
protected static <T extends GameStateSavedData<T>> T load(
ServerLevel level, SavedDataType<T> type);
protected T mutated(Function<T, T> mutator); // mutate + setDirty in one call
}
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import me.zlex.conduit.state.GameStateSavedData;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.saveddata.SavedDataType;
public final class MyGameState extends GameStateSavedData<MyGameState> {
public boolean started;
public int roundIndex;
private static final Codec<MyGameState> CODEC = RecordCodecBuilder.create(inst -> inst.group(
Codec.BOOL.optionalFieldOf("started", false).forGetter(s -> s.started),
Codec.INT .optionalFieldOf("round", 0) .forGetter(s -> s.roundIndex)
).apply(inst, MyGameState::reconstruct));
private static final SavedDataType<MyGameState> TYPE = type(
"my-game", "game-state", MyGameState::new, CODEC);
public static MyGameState get(ServerLevel level) {
return load(level, TYPE);
}
private static MyGameState reconstruct(boolean started, int round) {
MyGameState s = new MyGameState();
s.started = started;
s.roundIndex = round;
return s;
}
}

Mutate via mutated:

state.mutated(s -> { s.roundIndex++; return s; });
  • Saved data is per-ServerLevel. For per-instance state, call get(inst.level()) — each instance dimension has its own savefile.
  • Scope is the same as the original ShuffleGameState pattern — markers and replay state, not mid-game tactical state.
  • The codec lives on your subclass. Use RecordCodecBuilder for typed fields, or Codec.unboundedMap for KV blobs.