feat(saving): Use kryo for serialization

This will probably be more expanded in future. But for now this approach
works without some major issues. But ofc things like data migration etc
doesn't work.
This commit is contained in:
Jakub Žitník 2025-04-07 20:59:51 +02:00
parent 1d29972087
commit f09519773b
Signed by: jzitnik
GPG Key ID: C577A802A6AF4EF3
28 changed files with 64 additions and 218 deletions

View File

@ -128,11 +128,16 @@
<artifactId>logback-classic</artifactId>
<version>1.5.18</version> <!-- latest at the time -->
</dependency>
<dependency>
<dependency>
<groupId>com.github.trilarion</groupId>
<artifactId>java-vorbis-support</artifactId>
<version>1.2.1</version>
</dependency>
</dependency>
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>5.6.2</version>
</dependency>
</dependencies>
</project>

View File

@ -31,7 +31,8 @@ public class Main {
var spriteList = SpriteLoader.load();
var screenRenderer = new ScreenRenderer(spriteList, terminal);
var game = GameSaver.load();
var gameSaver = new GameSaver();
var game = gameSaver.load();
final boolean[] isRunning = { true };

View File

@ -11,7 +11,6 @@ import cz.jzitnik.game.handlers.place.CustomPlaceHandler;
import cz.jzitnik.game.mobs.EntitySpawnProvider;
import cz.jzitnik.game.sprites.Breaking;
import cz.jzitnik.game.sprites.Steve.SteveState;
import cz.jzitnik.game.annotations.AutoTransient;
import cz.jzitnik.game.annotations.WalkSound;
import cz.jzitnik.game.annotations.BreaksByPlace;
import cz.jzitnik.game.annotations.MineSound;
@ -20,9 +19,6 @@ import cz.jzitnik.game.annotations.PlaceSound;
import cz.jzitnik.game.blocks.Chest;
import cz.jzitnik.game.blocks.Furnace;
import cz.jzitnik.game.config.Configuration;
import cz.jzitnik.game.core.autotransient.AutoTransientSupport;
import cz.jzitnik.game.core.autotransient.initilizers.GameMiningInitializer;
import cz.jzitnik.game.core.autotransient.initilizers.GameWindowInitializer;
import cz.jzitnik.game.core.sound.SoundKey;
import cz.jzitnik.game.ui.Window;
import cz.jzitnik.game.ui.Inventory;
@ -39,19 +35,15 @@ import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
@Getter
public class Game extends AutoTransientSupport {
public class Game {
@SuppressWarnings("unchecked")
private final List<Block>[][] world = (List<Block>[][]) new CopyOnWriteArrayList[256][512];
private final Player player = new Player();
@AutoTransient(initializer = GameMiningInitializer.class)
private transient boolean mining = false;
@Setter
@AutoTransient(initializer = GameWindowInitializer.class)
private transient Window window = Window.WORLD;
private final Inventory inventory = new Inventory();
@AutoTransient
private transient EntitySpawnProvider entitySpawnProvider = new EntitySpawnProvider();
@AutoTransient
private transient GameStates gameStates = new GameStates(this);
@Setter
private int daytime = 0; // 0-600

View File

@ -1,46 +1,49 @@
package cz.jzitnik.game;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import lombok.extern.slf4j.Slf4j;
import java.io.*;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class GameSaver {
private static final String SAVE_FILE = "world.ser";
private final Kryo kryo;
public GameSaver() {
this.kryo = new Kryo();
kryo.setRegistrationRequired(false);
kryo.setReferences(true);
}
public void save(Game game) {
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("world.ser"))) {
out.writeObject(game);
try (Output output = new Output(new FileOutputStream(SAVE_FILE))) {
kryo.writeClassAndObject(output, game);
} catch (IOException e) {
e.printStackTrace();
log.error("Failed to save game", e);
}
}
// TODO: This will need rewrite
public static Game load() {
public Game load() {
log.info("Loading game");
File file = new File("world.ser");
File file = new File(SAVE_FILE);
if (!file.isFile()) {
log.info("No save file found, creating new game");
return new Game();
}
try {
try (Input input = new Input(new FileInputStream(SAVE_FILE))) {
log.info("Loading game from save file");
FileInputStream fileIn = new FileInputStream("world.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
// Read the object from the file
Object object = in.readObject();
Game game = (Game) object;
in.close();
fileIn.close();
return game;
} catch (IOException | ClassNotFoundException e) {
Object object = kryo.readClassAndObject(input);
return (Game) object;
} catch (IOException e) {
log.error("Failed to load game", e);
throw new RuntimeException(e);
}
}
}

View File

@ -1,16 +0,0 @@
package cz.jzitnik.game.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import cz.jzitnik.game.core.autotransient.AutoTransientInitializer;
import cz.jzitnik.game.core.autotransient.DefaultInitializer;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoTransient {
Class<? extends AutoTransientInitializer<?>> initializer() default DefaultInitializer.class;
}

View File

@ -1,12 +1,10 @@
package cz.jzitnik.game.config;
import java.io.Serializable;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class Configuration implements Serializable {
public class Configuration {
private int soundVolume = 100; // 0-100
}

View File

@ -1,5 +0,0 @@
package cz.jzitnik.game.core.autotransient;
public interface AutoTransientInitializer<T> {
T initialize(Object parent);
}

View File

@ -1,67 +0,0 @@
package cz.jzitnik.game.core.autotransient;
import cz.jzitnik.game.annotations.AutoTransient;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serial;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
public abstract class AutoTransientSupport implements Serializable {
@Serial
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
reinitializeAutoTransients();
}
protected void reinitializeAutoTransients() {
for (Field field : this.getClass().getDeclaredFields()) {
if (field.isAnnotationPresent(AutoTransient.class)) {
field.setAccessible(true);
AutoTransient annotation = field.getAnnotation(AutoTransient.class);
try {
Object value;
// Use initializer if provided
Class<? extends AutoTransientInitializer<?>> initializerClass = annotation.initializer();
if (!initializerClass.equals(DefaultInitializer.class)) {
AutoTransientInitializer<?> initializer = initializerClass.getDeclaredConstructor().newInstance();
value = initializer.initialize(this);
} else {
// Fallback to default instantiation
value = instantiateField(field.getType());
}
if (value != null) {
field.set(this, value);
}
} catch (Exception e) {
throw new RuntimeException("Failed to reinitialize @AutoTransient field: " + field.getName(), e);
}
}
}
}
protected Object instantiateField(Class<?> clazz) {
try {
// Try constructor with (this) reference first
Constructor<?> constructor = clazz.getDeclaredConstructor(this.getClass());
return constructor.newInstance(this);
} catch (NoSuchMethodException e) {
// Fallback to default constructor
try {
return clazz.getDeclaredConstructor().newInstance();
} catch (Exception ex) {
ex.printStackTrace();
return null;
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}

View File

@ -1,8 +0,0 @@
package cz.jzitnik.game.core.autotransient;
public class DefaultInitializer implements AutoTransientInitializer<Object> {
@Override
public Object initialize(Object parent) {
return null;
}
}

View File

@ -1,10 +0,0 @@
package cz.jzitnik.game.core.autotransient.initilizers;
import cz.jzitnik.game.core.autotransient.AutoTransientInitializer;
public class GameMiningInitializer implements AutoTransientInitializer<Boolean> {
@Override
public Boolean initialize(Object parent) {
return false;
}
}

View File

@ -1,11 +0,0 @@
package cz.jzitnik.game.core.autotransient.initilizers;
import cz.jzitnik.game.core.autotransient.AutoTransientInitializer;
import cz.jzitnik.game.ui.Window;
public class GameWindowInitializer implements AutoTransientInitializer<Window> {
@Override
public Window initialize(Object parent) {
return Window.WORLD;
}
}

View File

@ -6,15 +6,16 @@ import cz.jzitnik.game.entities.items.ItemType;
import cz.jzitnik.game.entities.items.ToolVariant;
import cz.jzitnik.game.ui.Inventory;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@Getter
@Setter
public class Block implements Serializable {
@NoArgsConstructor
public class Block {
private String blockId;
private SpriteLoader.SPRITES sprite;
private MyOptional<Enum> spriteState = MyOptional.empty();

View File

@ -1,10 +1,9 @@
package cz.jzitnik.game.entities;
import java.io.*;
import java.util.NoSuchElementException;
import java.util.function.*;
public class MyOptional<T> implements Serializable {
public class MyOptional<T> {
private T value;
private boolean isPresent;
@ -64,22 +63,6 @@ public class MyOptional<T> implements Serializable {
return this;
}
@Serial
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeBoolean(isPresent);
if (isPresent) {
out.writeObject(value);
}
}
@Serial
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
isPresent = in.readBoolean();
if (isPresent) {
value = (T) in.readObject();
}
}
@Override
public String toString() {
return isPresent ? "SerializableOptional[" + value + "]" : "SerializableOptional.empty";

View File

@ -3,7 +3,6 @@ package cz.jzitnik.game.entities;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
@ -15,7 +14,7 @@ import cz.jzitnik.tui.ScreenRenderer;
@Getter
@Setter
public class Player implements Serializable {
public class Player {
private int health = 10;
private int hunger = 10;
private int fallDistance = 0;

View File

@ -1,7 +1,5 @@
package cz.jzitnik.game.entities;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@ -9,6 +7,6 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class SteveData implements Serializable {
public class SteveData {
private boolean top = false;
}

View File

@ -3,13 +3,12 @@ package cz.jzitnik.game.entities.items;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@Getter
@AllArgsConstructor
public class InventoryItem implements Serializable {
public class InventoryItem {
private int amount;
private final List<Item> item;

View File

@ -7,12 +7,10 @@ import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
@Getter
@Setter
@AllArgsConstructor
public class Item implements Serializable {
public class Item {
private String id;
private String name;
private ItemType type;

View File

@ -1,13 +1,11 @@
package cz.jzitnik.game.logic.services.farmable;
import java.io.Serializable;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class FarmableData implements Serializable {
public class FarmableData {
private int age = 0;
private int state = 0;
}

View File

@ -3,11 +3,9 @@ package cz.jzitnik.game.logic.services.farmland;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
@Getter
@Setter
public class FarmlandData implements Serializable {
public class FarmlandData {
private int age = 0;
private int dryAge = 0;
private boolean watered = false;

View File

@ -3,10 +3,8 @@ package cz.jzitnik.game.logic.services.flowing;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
@Getter
@Setter
public class FlowingData implements Serializable {
public class FlowingData {
protected boolean isSource = true;
}

View File

@ -3,11 +3,9 @@ package cz.jzitnik.game.logic.services.grass;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
@Getter
@Setter
public class GrassDirtData implements Serializable {
public class GrassDirtData {
private int age = 0;
public void increaseAge() {

View File

@ -3,12 +3,11 @@ package cz.jzitnik.game.logic.services.saplings;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.util.Random;
@Getter
@Setter
public class SaplingData implements Serializable {
public class SaplingData {
private int growWait;
public SaplingData() {

View File

@ -3,8 +3,6 @@ package cz.jzitnik.game.mobs.services.cow;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import cz.jzitnik.game.Game;
import cz.jzitnik.game.annotations.RightClickLogic;
import cz.jzitnik.game.entities.items.ItemBlockSupplier;
@ -14,7 +12,7 @@ import cz.jzitnik.tui.ScreenRenderer;
@Getter
@Setter
@RightClickLogic
public class CowData implements Serializable, RightClickHandler {
public class CowData implements RightClickHandler {
private int lastDirection = 1; // 1 = right, -1 = left
private int movementCooldown = 0;
private int jumpAttempts = 0;

View File

@ -3,11 +3,9 @@ package cz.jzitnik.game.mobs.services.pig;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
@Getter
@Setter
public class PigData implements Serializable {
public class PigData {
private int lastDirection = 1; // 1 = right, -1 = left
private int movementCooldown = 0;
private int jumpAttempts = 0;

View File

@ -3,11 +3,9 @@ package cz.jzitnik.game.mobs.services.sheep;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
@Getter
@Setter
public class SheepData implements Serializable {
public class SheepData {
private int lastDirection = 1; // 1 = right, -1 = left
private int movementCooldown = 0;
private int jumpAttempts = 0;

View File

@ -0,0 +1,5 @@
package cz.jzitnik.game.ui;
public class HomeScreen {
}

View File

@ -1,8 +1,6 @@
package cz.jzitnik.game.ui;
import cz.jzitnik.game.Game;
import cz.jzitnik.game.annotations.AutoTransient;
import cz.jzitnik.game.core.autotransient.AutoTransientSupport;
import cz.jzitnik.game.entities.items.InventoryItem;
import cz.jzitnik.game.entities.items.Item;
import cz.jzitnik.tui.utils.SpriteCombiner;
@ -17,7 +15,7 @@ import java.util.List;
import java.util.Optional;
@Getter
public class Inventory extends AutoTransientSupport {
public class Inventory {
public static final int INVENTORY_SIZE_PX = 470;
public static final int COLUMN_AMOUNT = 5;
public static final int ROW_AMOUNT = 4;
@ -25,7 +23,6 @@ public class Inventory extends AutoTransientSupport {
private final InventoryItem[] items = new InventoryItem[20];
private final InventoryItem[] hotbar = new InventoryItem[9];
@AutoTransient
private transient SmallCraftingTable smallCraftingTable = new SmallCraftingTable(this);
@Setter

View File

@ -18,7 +18,6 @@ public class Options {
this.game = game;
}
public void render(StringBuilder buffer, Terminal terminal) {
var buf = new StringBuilder();
var font = game.getGameStates().dependencies.font;