DEADSOFTWARE

New saving fromat and rewrite in kotlin
authorfredboy <fredboy@protonmail.com>
Tue, 21 May 2024 19:42:50 +0000 (02:42 +0700)
committerfredboy <fredboy@protonmail.com>
Tue, 21 May 2024 19:42:50 +0000 (02:42 +0700)
23 files changed:
core/build.gradle
core/src/ru/deadsoftware/cavedroid/game/GameModule.java
core/src/ru/deadsoftware/cavedroid/game/GameSaver.java [deleted file]
core/src/ru/deadsoftware/cavedroid/game/input/handler/keyboard/PauseGameKeyboardInputHandler.kt
core/src/ru/deadsoftware/cavedroid/game/mobs/FallingBlock.kt
core/src/ru/deadsoftware/cavedroid/game/mobs/Mob.java
core/src/ru/deadsoftware/cavedroid/game/mobs/MobSaveDataMapper.kt [new file with mode: 0644]
core/src/ru/deadsoftware/cavedroid/game/mobs/MobsController.kt
core/src/ru/deadsoftware/cavedroid/game/mobs/Pig.kt
core/src/ru/deadsoftware/cavedroid/game/mobs/player/Inventory.kt
core/src/ru/deadsoftware/cavedroid/game/mobs/player/Player.java
core/src/ru/deadsoftware/cavedroid/game/model/dto/SaveDataDto.kt [new file with mode: 0644]
core/src/ru/deadsoftware/cavedroid/game/model/item/InventoryItem.kt
core/src/ru/deadsoftware/cavedroid/game/objects/container/Chest.kt
core/src/ru/deadsoftware/cavedroid/game/objects/container/Container.kt
core/src/ru/deadsoftware/cavedroid/game/objects/container/ContainerController.kt
core/src/ru/deadsoftware/cavedroid/game/objects/container/Furnace.kt
core/src/ru/deadsoftware/cavedroid/game/objects/drop/Drop.kt
core/src/ru/deadsoftware/cavedroid/game/objects/drop/DropController.java
core/src/ru/deadsoftware/cavedroid/game/save/GameSaveData.kt [new file with mode: 0644]
core/src/ru/deadsoftware/cavedroid/game/save/GameSaveLoader.kt [new file with mode: 0644]
core/src/ru/deadsoftware/cavedroid/menu/submenus/MenuMain.java
core/src/ru/deadsoftware/cavedroid/misc/Saveable.kt [new file with mode: 0644]

index 3b061a7b46e305818b0cc302409847fc593d016a..ac46757aece221bb9eea06e31a2a7c99b5d44c08 100644 (file)
@@ -22,5 +22,6 @@ dependencies {
     implementation 'org.jetbrains:annotations:23.1.0'
     implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
     implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion"
+    implementation "org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion"
     ksp "com.google.dagger:dagger-compiler:$daggerVersion"
-}
\ No newline at end of file
+}
index d57ab337e9e49c0cbc872ee037cb42b407908d24..29aac7df7e0f42be5364e1df21a5327fe3888697 100644 (file)
@@ -7,6 +7,8 @@ import ru.deadsoftware.cavedroid.game.mobs.MobsController;
 import ru.deadsoftware.cavedroid.game.model.block.Block;
 import ru.deadsoftware.cavedroid.game.objects.drop.DropController;
 import ru.deadsoftware.cavedroid.game.objects.container.ContainerController;
+import ru.deadsoftware.cavedroid.game.save.GameSaveData;
+import ru.deadsoftware.cavedroid.game.save.GameSaveLoader;
 import ru.deadsoftware.cavedroid.game.ui.TooltipManager;
 import ru.deadsoftware.cavedroid.game.world.GameWorld;
 
@@ -16,15 +18,15 @@ import javax.annotation.CheckForNull;
 public class GameModule {
 
     @CheckForNull
-    private static GameSaver.Data data;
+    private static GameSaveData data;
 
     public static boolean loaded = false;
 
-    private static void load(MainConfig mainConfig, GameItemsHolder gameItemsHolder) {
+    private static void load(MainConfig mainConfig, GameItemsHolder gameItemsHolder, TooltipManager tooltipManager) {
         if (loaded) {
             return;
         }
-        data = GameSaver.load(mainConfig, gameItemsHolder);
+        data = GameSaveLoader.INSTANCE.load(mainConfig, gameItemsHolder, tooltipManager);
         loaded = true;
     }
 
@@ -36,8 +38,10 @@ public class GameModule {
 
     @Provides
     @GameScope
-    public static DropController provideDropController(MainConfig mainConfig, GameItemsHolder gameItemsHolder) {
-        load(mainConfig, gameItemsHolder);
+    public static DropController provideDropController(MainConfig mainConfig,
+                                                       GameItemsHolder gameItemsHolder,
+                                                       TooltipManager tooltipManager) {
+        load(mainConfig, gameItemsHolder, tooltipManager);
         DropController controller = data != null ? data.retrieveDropController() : new DropController();
         makeDataNullIfEmpty();
         controller.initDrops(gameItemsHolder);
@@ -46,9 +50,14 @@ public class GameModule {
 
     @Provides
     @GameScope
-    public static ContainerController provideFurnaceController(MainConfig mainConfig, DropController dropController, GameItemsHolder gameItemsHolder) {
-        load(mainConfig, gameItemsHolder);
-        ContainerController controller = data != null ? data.retrieveFurnaceController() : new ContainerController(dropController, gameItemsHolder);
+    public static ContainerController provideFurnaceController(MainConfig mainConfig,
+                                                               DropController dropController,
+                                                               GameItemsHolder gameItemsHolder,
+                                                               TooltipManager tooltipManager) {
+        load(mainConfig, gameItemsHolder, tooltipManager);
+        ContainerController controller = data != null
+                ? data.retrieveContainerController()
+                : new ContainerController(dropController, gameItemsHolder);
         makeDataNullIfEmpty();
         controller.init(dropController, gameItemsHolder);
         return controller;
@@ -59,7 +68,7 @@ public class GameModule {
     public static MobsController provideMobsController(MainConfig mainConfig,
                                                        GameItemsHolder gameItemsHolder,
                                                        TooltipManager tooltipManager) {
-        load(mainConfig, gameItemsHolder);
+        load(mainConfig, gameItemsHolder, tooltipManager);
         MobsController controller = data != null
                 ? data.retrieveMobsController()
                 : new MobsController(gameItemsHolder, tooltipManager);
@@ -74,8 +83,9 @@ public class GameModule {
                                              DropController dropController,
                                              MobsController mobsController,
                                              GameItemsHolder gameItemsHolder,
-                                             ContainerController containerController) {
-        load(mainConfig, gameItemsHolder);
+                                             ContainerController containerController,
+                                             TooltipManager tooltipManager) {
+        load(mainConfig, gameItemsHolder, tooltipManager);
         Block[][] fm = data != null ? data.retrieveForeMap() : null;
         Block[][] bm = data != null ? data.retrieveBackMap() : null;
         makeDataNullIfEmpty();
diff --git a/core/src/ru/deadsoftware/cavedroid/game/GameSaver.java b/core/src/ru/deadsoftware/cavedroid/game/GameSaver.java
deleted file mode 100644 (file)
index 39d2278..0000000
+++ /dev/null
@@ -1,271 +0,0 @@
-package ru.deadsoftware.cavedroid.game;
-
-import com.badlogic.gdx.Gdx;
-import com.badlogic.gdx.files.FileHandle;
-import ru.deadsoftware.cavedroid.MainConfig;
-import ru.deadsoftware.cavedroid.game.mobs.MobsController;
-import ru.deadsoftware.cavedroid.game.model.block.Block;
-import ru.deadsoftware.cavedroid.game.objects.drop.DropController;
-import ru.deadsoftware.cavedroid.game.objects.container.ContainerController;
-import ru.deadsoftware.cavedroid.game.world.GameWorld;
-
-import javax.annotation.CheckForNull;
-import java.io.*;
-import java.nio.ByteBuffer;
-import java.util.HashMap;
-import java.util.Map;
-
-public class GameSaver {
-
-    private static final String TAG = "GameSaver";
-
-    public static class Data {
-        @CheckForNull
-        private MobsController mMobsController;
-        @CheckForNull
-        private DropController mDropController;
-        @CheckForNull
-        private ContainerController mContainerController;
-        @CheckForNull
-        private Block[][] mForeMap, mBackMap;
-
-        public Data(MobsController mobsController,
-                    DropController dropController,
-                    ContainerController containerController,
-                    Block[][] foreMap,
-                    Block[][] backMap) {
-            mMobsController = mobsController;
-            mDropController = dropController;
-            mContainerController = containerController;
-            mForeMap = foreMap;
-            mBackMap = backMap;
-        }
-
-        public MobsController retrieveMobsController() {
-            assert mMobsController != null;
-            MobsController mobsController = mMobsController;
-            mMobsController = null;
-            return mobsController;
-        }
-
-        public DropController retrieveDropController() {
-            assert mDropController != null;
-            DropController dropController = mDropController;
-            mDropController = null;
-            return dropController;
-        }
-
-        public ContainerController retrieveFurnaceController() {
-            assert mContainerController != null;
-            ContainerController containerController = mContainerController;
-            mContainerController = null;
-            return containerController;
-        }
-
-        public Block[][] retrieveForeMap() {
-            assert mForeMap != null;
-            Block[][] foreMap = mForeMap;
-            mForeMap = null;
-            return foreMap;
-        }
-
-        public Block[][] retrieveBackMap() {
-            assert mBackMap != null;
-            Block[][] backMap = mBackMap;
-            mBackMap = null;
-            return backMap;
-        }
-
-        public boolean isEmpty() {
-            return mMobsController == null &&
-                    mDropController == null &&
-                    mContainerController == null &&
-                    mForeMap == null &&
-                    mBackMap == null;
-        }
-    }
-
-    private static final int SAVE_VERSION = 1;
-
-    private static byte[] intToBytes(int i) {
-        return ByteBuffer.allocate(4).putInt(i).array();
-    }
-
-    private static Map<String, Integer> buildBlocksDictionary(Block[][] foreMap, Block[][] backMap) {
-        final HashMap<String, Integer> dict = new HashMap<>();
-
-        int id = 0;
-        for (int i = 0; i < foreMap.length; i++) {
-            for (int j = 0; j < foreMap[i].length; j++) {
-                for (int k = 0; k < 2; k++) {
-                    final Block block = k == 0 ? foreMap[i][j] : backMap[i][j];
-                    final String key = block.getParams().getKey();
-                    if (!dict.containsKey(key)) {
-                        dict.put(key, id++);
-                    }
-                }
-            }
-        }
-
-        return dict;
-    }
-
-    private static void saveDict(FileHandle file, Map<String, Integer> dict) {
-        final String[] arr = new String[dict.size()];
-
-        for (Map.Entry<String, Integer> entry : dict.entrySet()) {
-            arr[entry.getValue()] = entry.getKey();
-        }
-
-        final StringBuilder builder = new StringBuilder();
-        for (String key : arr) {
-            builder.append(key);
-            builder.append('\n');
-        }
-
-        file.writeString(builder.toString(), false);
-    }
-
-    private static String[] loadDict(FileHandle file) {
-        return file.readString().split("\n");
-    }
-
-    private static void saveMap(FileHandle file, Block[][] map, Map<String, Integer> dict) throws IOException {
-        int run, block;
-        int width = map.length;
-        int height = map[0].length;
-
-        BufferedOutputStream out = new BufferedOutputStream(file.write(false));
-
-        out.write(SAVE_VERSION);
-        out.write(intToBytes(width));
-        out.write(intToBytes(height));
-
-        for (int y = 0; y < height; y++) {
-            block = dict.get(map[0][y].getParams().getKey());
-            run = 0;
-            for (Block[] blocks : map) {
-                int newValue = dict.get(blocks[y].getParams().getKey());
-                if (run >= 0xFF || newValue != block) {
-                    out.write(run);
-                    out.write(block);
-                    run = 0;
-                    block = dict.get(blocks[y].getParams().getKey());
-                }
-                run++;
-            }
-            out.write(run);
-            out.write(block);
-        }
-
-        out.flush();
-        out.close();
-    }
-
-    private static Block[][] loadMap(GameItemsHolder gameItemsHolder, FileHandle file, String[] dict) throws Exception {
-        Block[][] map;
-        int version, width, height;
-        int run, block;
-
-        DataInputStream in = new DataInputStream(file.read());
-
-        version = in.readByte();
-
-        if (SAVE_VERSION == version) {
-            width = in.readInt();
-            height = in.readInt();
-            map = new Block[width][height];
-            for (int y = 0; y < height; y++) {
-                for (int x = 0; x < width; x += run) {
-                    run = in.readUnsignedByte();
-                    block = in.readUnsignedByte();
-                    for (int i = x; i < x + run; i++) {
-                        map[i][y] = gameItemsHolder.getBlock(dict[block]);
-                    }
-                }
-            }
-        } else {
-            throw new Exception("version mismatch");
-        }
-
-        in.close();
-        return map;
-    }
-
-    @CheckForNull
-    public static Data load(MainConfig mainConfig, GameItemsHolder gameItemsHolder) {
-        String folder = mainConfig.getGameFolder();
-        FileHandle file = Gdx.files.absolute(folder + "/saves/game.sav");
-
-        try {
-            ObjectInputStream in = new ObjectInputStream(file.read());
-            int version = in.readInt();
-            DropController dropController;
-            MobsController mobsController;
-            ContainerController containerController;
-
-            if (SAVE_VERSION == version) {
-                dropController = (DropController) in.readObject();
-                mobsController = (MobsController) in.readObject();
-                containerController = (ContainerController) in.readObject();
-            } else {
-                throw new Exception("version mismatch");
-            }
-
-            in.close();
-
-            final String[] dict = loadDict(Gdx.files.absolute(mainConfig.getGameFolder() + "/saves/dict"));
-            Block[][] foreMap = loadMap(gameItemsHolder, Gdx.files.absolute(mainConfig.getGameFolder() + "/saves/foremap.sav"), dict);
-            Block[][] backMap = loadMap(gameItemsHolder, Gdx.files.absolute(mainConfig.getGameFolder() + "/saves/backmap.sav"), dict);
-
-            if (dropController == null || mobsController == null) {
-                throw new Exception("couldn't load");
-            }
-
-            return new Data(mobsController, dropController, containerController, foreMap, backMap);
-        } catch (Exception e) {
-            Gdx.app.error("GameSaver", e.getMessage());
-        }
-
-        return null;
-    }
-
-    public static void save(MainConfig mainConfig,
-                            DropController dropController,
-                            MobsController mobsController,
-                            ContainerController containerController,
-                            GameWorld gameWorld) {
-        String folder = mainConfig.getGameFolder();
-        FileHandle file = Gdx.files.absolute(folder + "/saves/");
-        file.mkdirs();
-        file = Gdx.files.absolute(folder + "/saves/game.sav");
-
-        final Block[][] foreMap, backMap;
-        foreMap = gameWorld.getFullForeMap();
-        backMap = gameWorld.getFullBackMap();
-
-        final Map<String, Integer> dict = buildBlocksDictionary(foreMap, backMap);
-
-        try {
-            ObjectOutputStream out = new ObjectOutputStream(file.write(false));
-            out.writeInt(SAVE_VERSION);
-            out.writeObject(dropController);
-            out.writeObject(mobsController);
-            out.writeObject(containerController);
-            out.close();
-
-            saveDict(Gdx.files.absolute(folder + "/saves/dict"), dict);
-            saveMap(Gdx.files.absolute(folder + "/saves/foremap.sav"), gameWorld.getFullForeMap(), dict);
-            saveMap(Gdx.files.absolute(folder + "/saves/backmap.sav"), gameWorld.getFullBackMap(), dict);
-        } catch (Exception e) {
-            e.printStackTrace();
-        }
-    }
-
-    public static boolean exists(MainConfig mainConfig) {
-        String folder = mainConfig.getGameFolder();
-        return (Gdx.files.absolute(folder + "/saves/game.sav").exists() &&
-                Gdx.files.absolute(folder + "/saves/foremap.sav").exists() &&
-                Gdx.files.absolute(folder + "/saves/backmap.sav").exists());
-    }
-}
index cedc78a7fc128e95ed99757e6bd3243a2893e934..0311c8831fb22dd753d08db23564e9a8f5b19235 100644 (file)
@@ -1,7 +1,6 @@
 package ru.deadsoftware.cavedroid.game.input.handler.keyboard
 
 import ru.deadsoftware.cavedroid.MainConfig
-import ru.deadsoftware.cavedroid.game.GameSaver
 import ru.deadsoftware.cavedroid.game.GameScope
 import ru.deadsoftware.cavedroid.game.GameUiWindow
 import ru.deadsoftware.cavedroid.game.input.IKeyboardInputHandler
@@ -10,6 +9,7 @@ import ru.deadsoftware.cavedroid.game.input.action.keys.KeyboardInputActionKey
 import ru.deadsoftware.cavedroid.game.mobs.MobsController
 import ru.deadsoftware.cavedroid.game.objects.drop.DropController
 import ru.deadsoftware.cavedroid.game.objects.container.ContainerController
+import ru.deadsoftware.cavedroid.game.save.GameSaveLoader
 import ru.deadsoftware.cavedroid.game.ui.windows.GameWindowsManager
 import ru.deadsoftware.cavedroid.game.world.GameWorld
 import ru.deadsoftware.cavedroid.misc.annotations.multibinding.BindKeyboardInputHandler
@@ -36,7 +36,7 @@ class PauseGameKeyboardInputHandler @Inject constructor(
             return
         }
 
-        GameSaver.save(mainConfig, dropController, mobsController, containerController, gameWorld)
+        GameSaveLoader.save(mainConfig, dropController, mobsController, containerController, gameWorld)
         mainConfig.caveGame.quitGame()
     }
 }
\ No newline at end of file
index f2735044aee27572ed2045f7f0978712aabeb4a9..77e51a1b189ecc2a4757a5b79f0be7dcff331117 100644 (file)
@@ -3,6 +3,7 @@ package ru.deadsoftware.cavedroid.game.mobs
 import com.badlogic.gdx.graphics.g2d.SpriteBatch
 import ru.deadsoftware.cavedroid.game.GameItemsHolder
 import ru.deadsoftware.cavedroid.game.model.block.Block
+import ru.deadsoftware.cavedroid.game.model.dto.SaveDataDto
 import ru.deadsoftware.cavedroid.game.world.GameWorld
 import ru.deadsoftware.cavedroid.misc.utils.bl
 import ru.deadsoftware.cavedroid.misc.utils.px
@@ -50,4 +51,47 @@ class FallingBlock(
     ) {
         _block?.draw(spriteBatch, x, y)
     }
+
+    override fun getSaveData(): SaveDataDto.FallingBlockSaveData {
+        return SaveDataDto.FallingBlockSaveData(
+            version = SAVE_DATA_VERSION,
+            x = x,
+            y = y,
+            width = width,
+            height = height,
+            velocityX = velocity.x,
+            velocityY = velocity.y,
+            type = mType,
+            animDelta = mAnimDelta,
+            anim = mAnim,
+            direction = mDirection,
+            dead = mDead,
+            canJump = mCanJump,
+            flyMode = mFlyMode,
+            maxHealth = mMaxHealth,
+            health = mHealth,
+            blockKey = blockKey,
+        )
+    }
+
+    companion object {
+        private const val SAVE_DATA_VERSION = 1
+
+        fun fromSaveData(saveData: SaveDataDto.FallingBlockSaveData): FallingBlock {
+            saveData.verifyVersion(SAVE_DATA_VERSION)
+
+            return FallingBlock(saveData.blockKey, saveData.x, saveData.y).apply {
+                velocity.x = saveData.velocityX
+                velocity.y = saveData.velocityY
+                mAnimDelta = saveData.animDelta
+                mAnim = saveData.anim
+                mDirection = saveData.direction
+                mDead = saveData.dead
+                mCanJump = saveData.canJump
+                mFlyMode = saveData.flyMode
+                mMaxHealth = saveData.maxHealth
+                mHealth = saveData.health
+            }
+        }
+    }
 }
\ No newline at end of file
index e4ca679cb43c5a68430d521889459c1958e6c439..0f3610b61c36d0d516c744d7158abfa348a5fbd5 100644 (file)
@@ -8,9 +8,11 @@ import com.badlogic.gdx.math.Rectangle;
 import com.badlogic.gdx.math.Vector2;
 import com.badlogic.gdx.utils.Timer;
 import ru.deadsoftware.cavedroid.game.GameItemsHolder;
+import ru.deadsoftware.cavedroid.game.model.dto.SaveDataDto;
 import ru.deadsoftware.cavedroid.game.model.item.InventoryItem;
 import ru.deadsoftware.cavedroid.game.model.item.Item;
 import ru.deadsoftware.cavedroid.game.world.GameWorld;
+import ru.deadsoftware.cavedroid.misc.Saveable;
 
 import javax.annotation.CheckForNull;
 import java.io.Serializable;
@@ -20,7 +22,7 @@ import java.util.List;
 /**
  * Mob class.
  */
-public abstract class Mob extends Rectangle implements Serializable {
+public abstract class Mob extends Rectangle implements Serializable, Saveable {
 
     private static final float DAMAGE_TINT_TIMEOUT_S = 0.5f;
     private static final Color DAMAGE_TINT_COLOR = new Color(0xff8080ff);
@@ -75,13 +77,13 @@ public abstract class Mob extends Rectangle implements Serializable {
     protected int mAnimDelta = ANIMATION_SPEED;
     protected float mAnim;
 
-    private Direction mDirection;
+    protected Direction mDirection;
     protected boolean mDead;
-    private boolean mCanJump;
-    private boolean mFlyMode;
+    protected boolean mCanJump;
+    protected boolean mFlyMode;
 
-    private final int mMaxHealth;
-    private int mHealth;
+    protected int mMaxHealth;
+    protected int mHealth;
 
     private transient boolean mTakingDamage = false;
     @CheckForNull private transient ResetTakeDamageTask mResetTakeDamageTask = null;
@@ -344,4 +346,11 @@ public abstract class Mob extends Rectangle implements Serializable {
     public abstract float getSpeed();
 
     public abstract void jump();
+
+    @Override
+    public abstract SaveDataDto.MobSaveDataDto getSaveData();
+
+    public static Mob fromSaveData(SaveDataDto.MobSaveDataDto saveData) {
+        return MobSaveDataMapperKt.fromSaveData(saveData);
+    }
 }
diff --git a/core/src/ru/deadsoftware/cavedroid/game/mobs/MobSaveDataMapper.kt b/core/src/ru/deadsoftware/cavedroid/game/mobs/MobSaveDataMapper.kt
new file mode 100644 (file)
index 0000000..49adee3
--- /dev/null
@@ -0,0 +1,12 @@
+package ru.deadsoftware.cavedroid.game.mobs
+
+import ru.deadsoftware.cavedroid.game.model.dto.SaveDataDto
+
+fun fromSaveData(saveData: SaveDataDto.MobSaveDataDto): Mob {
+    return when (saveData) {
+        is SaveDataDto.PigSaveData -> Pig.fromSaveData(saveData)
+        is SaveDataDto.FallingBlockSaveData -> FallingBlock.fromSaveData(saveData)
+
+        is SaveDataDto.PlayerSaveData -> throw IllegalArgumentException("Cannot load player as regular Mob")
+    }
+}
\ No newline at end of file
index 158d22a9e55445ae1056e680c4fb3621f03a03e0..dc01e1e8e238db085a53a467edd52c6801bd634c 100644 (file)
@@ -3,7 +3,9 @@ package ru.deadsoftware.cavedroid.game.mobs
 import ru.deadsoftware.cavedroid.game.GameItemsHolder
 import ru.deadsoftware.cavedroid.game.GameScope
 import ru.deadsoftware.cavedroid.game.mobs.player.Player
+import ru.deadsoftware.cavedroid.game.model.dto.SaveDataDto
 import ru.deadsoftware.cavedroid.game.ui.TooltipManager
+import ru.deadsoftware.cavedroid.misc.Saveable
 import java.io.Serializable
 import java.util.*
 import javax.inject.Inject
@@ -12,12 +14,12 @@ import javax.inject.Inject
 class MobsController @Inject constructor(
     gameItemsHolder: GameItemsHolder,
     tooltipManager: TooltipManager,
-) : Serializable {
+) : Serializable, Saveable {
 
     private val _mobs = LinkedList<Mob>()
 
-    val player: Player =
-        Player(gameItemsHolder, tooltipManager)
+    var player: Player = Player(gameItemsHolder, tooltipManager)
+        private set
 
     val mobs: List<Mob>
         get() = _mobs
@@ -26,7 +28,31 @@ class MobsController @Inject constructor(
         _mobs.add(mob)
     }
 
+    override fun getSaveData(): SaveDataDto.MobsControllerSaveData {
+        return SaveDataDto.MobsControllerSaveData(
+            version = SAVE_DATA_VERSION,
+            mobs = _mobs.map(Mob::getSaveData),
+            player = player.getSaveData(),
+        )
+    }
+
     companion object {
+        private const val SAVE_DATA_VERSION = 1
+
         private const val TAG = "MobsController"
+
+        fun fromSaveData(
+            saveData: SaveDataDto.MobsControllerSaveData,
+            gameItemsHolder: GameItemsHolder,
+            tooltipManager: TooltipManager
+        ): MobsController {
+            saveData.verifyVersion(SAVE_DATA_VERSION)
+
+            return MobsController(gameItemsHolder, tooltipManager)
+                .apply {
+                    _mobs.addAll(saveData.mobs.map { mob -> Mob.fromSaveData(mob) })
+                    player = Player.fromSaveData(saveData.player, gameItemsHolder, tooltipManager)
+                }
+        }
     }
 }
\ No newline at end of file
index b3d647d328aae2fd0a65f89f66b92ff3c5b0e832..b080e4078730713b897e8a45f1e1ba3655b81239 100644 (file)
@@ -3,6 +3,7 @@ package ru.deadsoftware.cavedroid.game.mobs
 import com.badlogic.gdx.graphics.g2d.SpriteBatch
 import com.badlogic.gdx.math.Vector2
 import ru.deadsoftware.cavedroid.game.GameItemsHolder
+import ru.deadsoftware.cavedroid.game.model.dto.SaveDataDto
 import ru.deadsoftware.cavedroid.game.model.item.InventoryItem
 import ru.deadsoftware.cavedroid.misc.utils.drawSprite
 import ru.deadsoftware.cavedroid.misc.utils.mobs.MobSprites.Pig.getBackgroundLeg
@@ -54,13 +55,52 @@ class Pig(x: Float, y: Float) : PeacefulMob(x, y, WIDTH, HEIGHT, randomDir(), MA
         spriteBatch.drawSprite(getForegroundLeg(), leftLegX, legY, anim, tint = tintColor)
         spriteBatch.drawSprite(getForegroundLeg(), rightLegX, legY, anim, tint = tintColor)
     }
+
+    override fun getSaveData(): SaveDataDto.PigSaveData {
+        return SaveDataDto.PigSaveData(
+            version = SAVE_DATA_VERSION,
+            x = x,
+            y = y,
+            width = width,
+            height = height,
+            velocityX = velocity.x,
+            velocityY = velocity.y,
+            type = mType,
+            animDelta = mAnimDelta,
+            anim = mAnim,
+            direction = mDirection,
+            dead = mDead,
+            canJump = mCanJump,
+            flyMode = mFlyMode,
+            maxHealth = mMaxHealth,
+            health = mHealth
+        )
+    }
     
-    
-    private companion object {
+    companion object {
+        private const val SAVE_DATA_VERSION = 1
+
         private const val WIDTH = 25f
         private const val HEIGHT = 18f
         private const val SPEED =  48f
         private const val JUMP_VELOCITY = -133.332f
         private const val MAX_HEALTH = 10
+
+        fun fromSaveData(saveData: SaveDataDto.PigSaveData): Pig {
+            saveData.verifyVersion(SAVE_DATA_VERSION)
+
+            return Pig(saveData.x, saveData.y).apply {
+                velocity.x = saveData.velocityX
+                velocity.y = saveData.velocityY
+                mAnimDelta = saveData.animDelta
+                mAnim = saveData.anim
+                mDirection = saveData.direction
+                mDead = saveData.dead
+                mCanJump = saveData.canJump
+                mFlyMode = saveData.flyMode
+                mMaxHealth = saveData.maxHealth
+                mHealth = saveData.health
+            }
+        }
     }
 }
\ No newline at end of file
index 43f298c876e9702fb5e675e5b429602b22fbf13d..8f0b90d721a709f3b5c2015d16d82c943ae1e779 100644 (file)
@@ -1,18 +1,21 @@
 package ru.deadsoftware.cavedroid.game.mobs.player
 
 import ru.deadsoftware.cavedroid.game.GameItemsHolder
+import ru.deadsoftware.cavedroid.game.model.dto.SaveDataDto
 import ru.deadsoftware.cavedroid.game.model.item.InventoryItem
 import ru.deadsoftware.cavedroid.game.model.item.Item
 import ru.deadsoftware.cavedroid.game.objects.drop.Drop
 import ru.deadsoftware.cavedroid.game.ui.TooltipManager
+import ru.deadsoftware.cavedroid.misc.Saveable
 import java.io.Serializable
 
-class Inventory(
+class Inventory @JvmOverloads constructor(
     val size: Int,
     val hotbarSize: Int,
     gameItemsHolder: GameItemsHolder,
     tooltipManager: TooltipManager,
-) : Serializable {
+    initialItems: List<InventoryItem>? = null
+) : Serializable, Saveable {
 
     @Suppress("UNNECESSARY_LATEINIT")
     @Transient
@@ -22,6 +25,8 @@ class Inventory(
     @Transient
     private lateinit var fallbackItem: InventoryItem
 
+    private val _items: Array<InventoryItem>
+
     init {
         fallbackItem = gameItemsHolder.fallbackItem.toInventoryItem()
         this.tooltipManager = tooltipManager
@@ -29,18 +34,21 @@ class Inventory(
         if (size < 0 || hotbarSize < 0 || hotbarSize > size) {
             throw IllegalArgumentException("Invalid inventory sizes: hotbarSize=$hotbarSize; size=$size")
         }
-    }
 
-    private val _items = Array(size) { InventoryItem(gameItemsHolder.fallbackItem) }
+        _items = Array(size) { index -> initialItems?.getOrNull(index) ?: InventoryItem(gameItemsHolder.fallbackItem) }
+    }
 
     val items get() = _items.asList() as MutableList<InventoryItem>
 
     val hotbarItems get() = items.subList(0, hotbarSize)
 
-    var activeSlot = 0
+    private var _activeSlot = 0
+
+    var activeSlot
+        get() = _activeSlot
         set(value) {
             if (value in 0 ..< hotbarSize) {
-                field = value
+                _activeSlot = value
                 showCurrentItemTooltip()
             }
         }
@@ -139,4 +147,36 @@ class Inventory(
             _items[i] = fallbackItem
         }
     }
+
+    override fun getSaveData(): SaveDataDto.InventorySaveData {
+        return SaveDataDto.InventorySaveData(
+            version = SAVE_DATA_VERSION,
+            size = size,
+            hotbarSize = hotbarSize,
+            activeSlot = _activeSlot,
+            items = items.map(InventoryItem::getSaveData)
+        )
+    }
+
+    companion object {
+        private const val SAVE_DATA_VERSION = 1
+
+        fun fromSaveData(
+            saveData: SaveDataDto.InventorySaveData,
+            gameItemsHolder: GameItemsHolder,
+            tooltipManager: TooltipManager,
+        ): Inventory {
+            saveData.verifyVersion(SAVE_DATA_VERSION)
+
+            return Inventory(
+                size = saveData.size,
+                hotbarSize = saveData.hotbarSize,
+                gameItemsHolder = gameItemsHolder,
+                tooltipManager = tooltipManager,
+                initialItems = saveData.items.map { item -> InventoryItem.fromSaveData(item, gameItemsHolder) }
+            ).apply {
+                _activeSlot = saveData.activeSlot
+            }
+        }
+    }
 }
\ No newline at end of file
index 8ea87c11d8b42868a4dc393c9f100689496b4788..f454b0c4d05a978e3a88d4387710d7535905e718 100644 (file)
@@ -4,10 +4,12 @@ import com.badlogic.gdx.graphics.g2d.Sprite;
 import com.badlogic.gdx.graphics.g2d.SpriteBatch;
 import com.badlogic.gdx.math.MathUtils;
 import com.badlogic.gdx.math.Vector2;
+import org.jetbrains.annotations.NotNull;
 import ru.deadsoftware.cavedroid.game.GameItemsHolder;
 import ru.deadsoftware.cavedroid.game.mobs.Mob;
 import ru.deadsoftware.cavedroid.game.mobs.MobsController;
 import ru.deadsoftware.cavedroid.game.model.block.Block;
+import ru.deadsoftware.cavedroid.game.model.dto.SaveDataDto;
 import ru.deadsoftware.cavedroid.game.model.item.InventoryItem;
 import ru.deadsoftware.cavedroid.game.model.item.Item;
 import ru.deadsoftware.cavedroid.game.objects.drop.Drop;
@@ -22,6 +24,8 @@ import javax.annotation.CheckForNull;
 
 public class Player extends Mob {
 
+    private static final int SAVE_DATA_VERSION = 1;
+
     private static final float SPEED = 69.072f;
     private static final float JUMP_VELOCITY = -133.332f;
     private static final int SURVIVAL_CURSOR_RANGE = 4;
@@ -34,7 +38,7 @@ public class Player extends Mob {
     private float hitAnim = 0f;
     private float hitAnimDelta = ANIMATION_SPEED;
 
-    public final Inventory inventory;
+    public Inventory inventory;
 
     public int gameMode;
     public boolean swim;
@@ -439,4 +443,79 @@ public class Player extends Mob {
         SpriteUtilsKt.drawSprite(spriteBatch, frontHand, x + 2, y + 8, frontHandAnim);
     }
 
+    @NotNull
+    @Override
+    public SaveDataDto.PlayerSaveData getSaveData() {
+        return new SaveDataDto.PlayerSaveData(
+                SAVE_DATA_VERSION,
+                mType,
+                mAnimDelta,
+                mAnim,
+                mDirection,
+                mDead,
+                mCanJump,
+                mFlyMode,
+                mMaxHealth,
+                mHealth,
+                x,
+                y,
+                width,
+                height,
+                getVelocity().x,
+                getVelocity().y,
+                hitting,
+                hittingWithDamage,
+                hitAnim,
+                hitAnimDelta,
+                inventory.getSaveData(),
+                gameMode,
+                swim,
+                headRotation,
+                blockDamage,
+                cursorX,
+                cursorY,
+                spawnPoint != null ? spawnPoint.x : 0f,
+                spawnPoint != null ? spawnPoint.y : 0f,
+                controlMode
+        );
+    }
+
+    public static Player fromSaveData(
+            SaveDataDto.PlayerSaveData saveData,
+            GameItemsHolder gameItemsHolder,
+            TooltipManager tooltipManager
+    ) {
+        saveData.verifyVersion(SAVE_DATA_VERSION);
+
+        Player player = new Player(gameItemsHolder, tooltipManager);
+
+        player.mType = saveData.getType();
+        player.mAnimDelta = saveData.getAnimDelta();
+        player.mAnim = saveData.getAnim();
+        player.mDirection = saveData.getDirection();
+        player.mDead = saveData.getDead();
+        player.mCanJump = saveData.getCanJump();
+        player.mFlyMode = saveData.getFlyMode();
+        player.mMaxHealth = saveData.getMaxHealth();
+        player.mHealth = saveData.getHealth();
+        player.x = saveData.getX();
+        player.y = saveData.getY();
+        player.width = saveData.getWidth();
+        player.height = saveData.getHeight();
+        player.hitting = saveData.getHitting();
+        player.hittingWithDamage = saveData.getHittingWithDamage();
+        player.hitAnim = saveData.getHitAnim();
+        player.hitAnimDelta = saveData.getHitAnimDelta();
+        player.inventory = Inventory.Companion.fromSaveData(saveData.getInventory(), gameItemsHolder, tooltipManager);
+        player.gameMode = saveData.getGameMode();
+        player.swim = saveData.getSwim();
+        player.headRotation = saveData.getHeadRotation();
+        player.blockDamage = saveData.getBlockDamage();
+        player.cursorX = saveData.getCursorX();
+        player.cursorY = saveData.getCursorY();
+        player.spawnPoint = new Vector2(saveData.getSpawnPointX(), saveData.getSpawnPointY());
+        player.controlMode = saveData.getControlMode();
+
+        return player;
+    }
 }
diff --git a/core/src/ru/deadsoftware/cavedroid/game/model/dto/SaveDataDto.kt b/core/src/ru/deadsoftware/cavedroid/game/model/dto/SaveDataDto.kt
new file mode 100644 (file)
index 0000000..e6ad828
--- /dev/null
@@ -0,0 +1,191 @@
+package ru.deadsoftware.cavedroid.game.model.dto
+
+import kotlinx.serialization.Contextual
+import kotlinx.serialization.Serializable
+import ru.deadsoftware.cavedroid.game.mobs.Mob
+import ru.deadsoftware.cavedroid.game.mobs.player.Player.ControlMode
+
+@Serializable
+sealed class SaveDataDto {
+
+    abstract val version: Int
+
+    fun verifyVersion(expectedVersion: Int) {
+        require(version == expectedVersion) {
+            "${this::class.simpleName} version mismatch ($version != $expectedVersion)"
+        }
+    }
+
+    @Serializable
+    sealed class ContainerSaveDataDto : SaveDataDto() {
+        abstract val size: Int
+        abstract val items: List<InventoryItemSaveData>
+    }
+
+    @Serializable
+    sealed class RectangleObjectSaveDataDto : SaveDataDto() {
+        abstract val x: Float
+        abstract val y: Float
+        abstract val width: Float
+        abstract val height: Float
+        abstract val velocityX: Float
+        abstract val velocityY: Float
+    }
+
+    @Serializable
+    sealed class MobSaveDataDto : RectangleObjectSaveDataDto() {
+        abstract val type: Mob.Type
+        abstract val animDelta: Int
+        abstract val anim: Float
+        abstract val direction: Mob.Direction
+        abstract val dead: Boolean
+        abstract val canJump: Boolean
+        abstract val flyMode: Boolean
+        abstract val maxHealth: Int
+        abstract val health: Int
+    }
+
+    @Serializable
+    data class InventoryItemSaveData(
+        override val version: Int,
+        val itemKey: String,
+        val amount: Int,
+    ) : SaveDataDto()
+
+    @Serializable
+    data class InventorySaveData(
+        override val version: Int,
+        override val size: Int,
+        val hotbarSize: Int,
+        val activeSlot: Int,
+        override val items: List<InventoryItemSaveData>,
+    ) : ContainerSaveDataDto()
+
+    @Serializable
+    data class FurnaceSaveData(
+        override val version: Int,
+        override val size: Int,
+        val currentFuelItemKey: String?,
+        override val items: List<InventoryItemSaveData>,
+        val startBurnTimeMs: Long,
+        val startSmeltTimeMs: Long,
+        val burnProgress: Float,
+        val smeltProgress: Float,
+    ) : ContainerSaveDataDto()
+
+    @Serializable
+    data class ChestSaveData(
+        override val version: Int,
+        override val size: Int,
+        override val items: List<InventoryItemSaveData>
+    ) : ContainerSaveDataDto()
+
+    @Serializable
+    data class ContainerControllerSaveData(
+        override val version: Int,
+        val containerMap: Map<String, @Contextual ContainerSaveDataDto>,
+    ): SaveDataDto()
+
+    @Serializable
+    data class DropSaveData(
+        override val version: Int,
+        override val x: Float,
+        override val y: Float,
+        override val width: Float,
+        override val height: Float,
+        override val velocityX: Float,
+        override val velocityY: Float,
+        val itemKey: String,
+        val amount: Int,
+        val pickedUp: Boolean
+    ) : RectangleObjectSaveDataDto()
+    
+    @Serializable
+    data class DropControllerSaveData(
+        override val version: Int,
+        val drops: List<DropSaveData>
+    ) : SaveDataDto()
+    
+    @Serializable
+    data class PigSaveData(
+        override val version: Int,
+        override val x: Float,
+        override val y: Float,
+        override val width: Float,
+        override val height: Float,
+        override val velocityX: Float,
+        override val velocityY: Float,
+        override val type: Mob.Type,
+        override val animDelta: Int,
+        override val anim: Float,
+        override val direction: Mob.Direction,
+        override val dead: Boolean,
+        override val canJump: Boolean,
+        override val flyMode: Boolean,
+        override val maxHealth: Int,
+        override val health: Int,
+    ) : MobSaveDataDto()
+
+    @Serializable
+    data class FallingBlockSaveData(
+        override val version: Int,
+        override val x: Float,
+        override val y: Float,
+        override val width: Float,
+        override val height: Float,
+        override val velocityX: Float,
+        override val velocityY: Float,
+        override val type: Mob.Type,
+        override val animDelta: Int,
+        override val anim: Float,
+        override val direction: Mob.Direction,
+        override val dead: Boolean,
+        override val canJump: Boolean,
+        override val flyMode: Boolean,
+        override val maxHealth: Int,
+        override val health: Int,
+        val blockKey: String,
+    ) : MobSaveDataDto()
+
+    @Serializable
+    data class PlayerSaveData(
+        override val version: Int,
+        override val type: Mob.Type,
+        override val animDelta: Int,
+        override val anim: Float,
+        override val direction: Mob.Direction,
+        override val dead: Boolean,
+        override val canJump: Boolean,
+        override val flyMode: Boolean,
+        override val maxHealth: Int,
+        override val health: Int,
+        override val x: Float,
+        override val y: Float,
+        override val width: Float,
+        override val height: Float,
+        override val velocityX: Float,
+        override val velocityY: Float,
+        val hitting: Boolean,
+        val hittingWithDamage: Boolean,
+        val hitAnim: Float,
+        val hitAnimDelta: Float,
+        val inventory: InventorySaveData,
+        val gameMode: Int,
+        val swim: Boolean,
+        val headRotation: Float,
+        val blockDamage: Float,
+        val cursorX: Int,
+        val cursorY: Int,
+        val spawnPointX: Float,
+        val spawnPointY: Float,
+        val controlMode: ControlMode,
+    ) : MobSaveDataDto()
+
+    @Serializable
+    data class MobsControllerSaveData(
+        override val version: Int,
+        val mobs: List<@Contextual MobSaveDataDto>,
+        val player: PlayerSaveData,
+    ) : SaveDataDto()
+    
+}
\ No newline at end of file
index 3cd406b731f5e2e0dc8a128926a4baef24d5a246..71909880fde87171235d4e665c39ca1f4837fad2 100644 (file)
@@ -4,7 +4,9 @@ import com.badlogic.gdx.graphics.Color
 import com.badlogic.gdx.graphics.g2d.SpriteBatch
 import com.badlogic.gdx.graphics.glutils.ShapeRenderer
 import ru.deadsoftware.cavedroid.game.GameItemsHolder
+import ru.deadsoftware.cavedroid.game.model.dto.SaveDataDto
 import ru.deadsoftware.cavedroid.misc.Assets
+import ru.deadsoftware.cavedroid.misc.Saveable
 import ru.deadsoftware.cavedroid.misc.utils.drawSprite
 import ru.deadsoftware.cavedroid.misc.utils.drawString
 import ru.deadsoftware.cavedroid.misc.utils.px
@@ -15,7 +17,7 @@ import kotlin.contracts.contract
 class InventoryItem @JvmOverloads constructor(
     val itemKey: String,
     _amount: Int = 1,
-) : Serializable {
+) : Serializable, Saveable {
 
     var amount = _amount
         set(value) {
@@ -131,12 +133,35 @@ class InventoryItem @JvmOverloads constructor(
         }
     }
 
+    override fun getSaveData(): SaveDataDto.InventoryItemSaveData {
+        return SaveDataDto.InventoryItemSaveData(
+            version = SAVE_DATA_VERSION,
+            itemKey = itemKey,
+            amount = amount,
+        )
+    }
+
     companion object {
+        private const val SAVE_DATA_VERSION = 1
 
         @OptIn(ExperimentalContracts::class)
         fun InventoryItem?.isNoneOrNull(): Boolean {
             contract { returns(false) implies(this@isNoneOrNull != null) }
             return this?.item == null || this.item.isNone()
         }
+
+
+        fun fromSaveData(
+            saveData: SaveDataDto.InventoryItemSaveData,
+            gameItemsHolder: GameItemsHolder? = null
+        ): InventoryItem {
+            saveData.verifyVersion(SAVE_DATA_VERSION)
+
+            val inventoryItem = InventoryItem(saveData.itemKey, saveData.amount)
+            gameItemsHolder?.let(inventoryItem::init)
+
+            return inventoryItem
+        }
     }
+
 }
index be820bc3e2a19a73cf70b35ded134bd6683b82ae..a9e893be3cbad21c09b3eb81c9c40167c95b92e2 100644 (file)
@@ -1,14 +1,38 @@
 package ru.deadsoftware.cavedroid.game.objects.container
 
 import ru.deadsoftware.cavedroid.game.GameItemsHolder
+import ru.deadsoftware.cavedroid.game.model.dto.SaveDataDto
+import ru.deadsoftware.cavedroid.game.model.item.InventoryItem
+import ru.deadsoftware.cavedroid.misc.Saveable
 
-class Chest(gameItemsHolder: GameItemsHolder) : Container(SIZE, gameItemsHolder) {
+class Chest @JvmOverloads constructor(
+    gameItemsHolder: GameItemsHolder,
+    initialItems: List<InventoryItem>? = null
+) : Container(SIZE, gameItemsHolder, initialItems), Saveable {
 
     override fun update(gameItemsHolder: GameItemsHolder) {
         // no-op
     }
 
+    override fun getSaveData(): SaveDataDto.ChestSaveData {
+        return SaveDataDto.ChestSaveData(
+            version = SAVE_DATA_VERSION,
+            size = size,
+            items = items.map(InventoryItem::getSaveData)
+        )
+    }
+
     companion object {
+        private const val SAVE_DATA_VERSION = 1
         private const val SIZE = 27
+
+        fun fromSaveData(saveData: SaveDataDto.ChestSaveData, gameItemsHolder: GameItemsHolder): Chest {
+            saveData.verifyVersion(SAVE_DATA_VERSION)
+
+            return Chest(
+                gameItemsHolder = gameItemsHolder,
+                initialItems = saveData.items.map { item -> InventoryItem.fromSaveData(item, gameItemsHolder) }
+            )
+        }
     }
 }
\ No newline at end of file
index 1e13c640cda5f1cdd8e64dc816e81881b85a6504..05ec7444fd79243e9ebbd35d129de5d668dd2f85 100644 (file)
@@ -1,16 +1,21 @@
 package ru.deadsoftware.cavedroid.game.objects.container
 
 import ru.deadsoftware.cavedroid.game.GameItemsHolder
+import ru.deadsoftware.cavedroid.game.model.dto.SaveDataDto
 import ru.deadsoftware.cavedroid.game.model.item.InventoryItem
+import ru.deadsoftware.cavedroid.misc.Saveable
 import java.io.Serializable
 import javax.annotation.OverridingMethodsMustInvokeSuper
 
-abstract class Container(
+abstract class Container @JvmOverloads constructor(
     val size: Int,
-    gameItemsHolder: GameItemsHolder
-) : Serializable {
+    gameItemsHolder: GameItemsHolder,
+    initialItems: List<InventoryItem>? = null,
+) : Serializable, Saveable {
 
-    private val _items = Array(size) { gameItemsHolder.fallbackItem.toInventoryItem() }
+    private val _items = Array(size) { index ->
+        initialItems?.getOrNull(index) ?: gameItemsHolder.fallbackItem.toInventoryItem()
+    }
 
     val items get() = _items.asList() as MutableList<InventoryItem>
 
@@ -21,4 +26,18 @@ abstract class Container(
 
     abstract fun update(gameItemsHolder: GameItemsHolder)
 
+    abstract override fun getSaveData(): SaveDataDto.ContainerSaveDataDto
+
+    companion object {
+        fun fromSaveData(saveData: SaveDataDto.ContainerSaveDataDto, gameItemsHolder: GameItemsHolder): Container {
+            return when (saveData) {
+                is SaveDataDto.FurnaceSaveData -> Furnace.fromSaveData(saveData, gameItemsHolder)
+                is SaveDataDto.ChestSaveData -> Chest.fromSaveData(saveData, gameItemsHolder)
+
+                is SaveDataDto.InventorySaveData -> {
+                    throw IllegalArgumentException("Cannot load Container from InventorySaveData")
+                }
+            }
+        }
+    }
 }
\ No newline at end of file
index 3facb6f542e980806b3cc03e4e58e7280de7b98f..bffb846d6116ef42439a8032e156ea19fb393e23 100644 (file)
@@ -4,8 +4,10 @@ import com.badlogic.gdx.Gdx
 import ru.deadsoftware.cavedroid.game.GameItemsHolder
 import ru.deadsoftware.cavedroid.game.GameScope
 import ru.deadsoftware.cavedroid.game.model.block.Block
+import ru.deadsoftware.cavedroid.game.model.dto.SaveDataDto
 import ru.deadsoftware.cavedroid.game.model.item.InventoryItem.Companion.isNoneOrNull
 import ru.deadsoftware.cavedroid.game.objects.drop.DropController
+import ru.deadsoftware.cavedroid.misc.Saveable
 import ru.deadsoftware.cavedroid.misc.utils.px
 import java.io.Serializable
 import javax.inject.Inject
@@ -14,7 +16,7 @@ import javax.inject.Inject
 class ContainerController @Inject constructor(
     _dropController: DropController,
     _gameItemsHolder: GameItemsHolder
-) : Serializable {
+) : Serializable, Saveable {
 
     @Suppress("UNNECESSARY_LATEINIT")
     @Transient
@@ -79,8 +81,35 @@ class ContainerController @Inject constructor(
         }
     }
 
+    override fun getSaveData(): SaveDataDto.ContainerControllerSaveData {
+        return SaveDataDto.ContainerControllerSaveData(
+            version = SAVE_DATA_VERSION,
+            containerMap = containerMap.mapValues { (_, container) -> container.getSaveData() },
+        )
+    }
+
     companion object {
+        private const val SAVE_DATA_VERSION = 1
         private const val TAG = "ContainerController"
+
+        fun fromSaveData(
+            saveData: SaveDataDto.ContainerControllerSaveData,
+            dropController: DropController,
+            gameItemsHolder: GameItemsHolder
+        ): ContainerController {
+            saveData.verifyVersion(SAVE_DATA_VERSION)
+
+            return ContainerController(
+                dropController,
+                gameItemsHolder
+            ).apply {
+                containerMap.putAll(
+                    saveData.containerMap.mapValues { (_, containerSaveData) ->
+                        Container.fromSaveData(containerSaveData, gameItemsHolder)
+                    }
+                )
+            }
+        }
     }
 
 }
\ No newline at end of file
index 1a75e89f83657b8222c2a7ba5e2e54f17bcf0508..92eeab9eae5dcb239746a19c66e7139fd0e54ca9 100644 (file)
@@ -4,12 +4,17 @@ import com.badlogic.gdx.Gdx
 import com.badlogic.gdx.math.MathUtils
 import com.badlogic.gdx.utils.TimeUtils
 import ru.deadsoftware.cavedroid.game.GameItemsHolder
+import ru.deadsoftware.cavedroid.game.model.dto.SaveDataDto
 import ru.deadsoftware.cavedroid.game.model.item.InventoryItem
 import ru.deadsoftware.cavedroid.game.model.item.InventoryItem.Companion.isNoneOrNull
 import ru.deadsoftware.cavedroid.game.model.item.Item
+import ru.deadsoftware.cavedroid.misc.Saveable
+
+class Furnace @JvmOverloads constructor(
+    gameItemsHolder: GameItemsHolder,
+    initialItems: List<InventoryItem>? = null
+) : Container(SIZE, gameItemsHolder, initialItems), Saveable {
 
-class Furnace(gameItemsHolder: GameItemsHolder) : Container(SIZE, gameItemsHolder) {
-    
     var fuel: InventoryItem
         get() = items[FUEL_INDEX]
         set(value) {
@@ -57,7 +62,7 @@ class Furnace(gameItemsHolder: GameItemsHolder) : Container(SIZE, gameItemsHolde
     }
 
     fun canSmelt(): Boolean {
-        return (result.isNoneOrNull() || (result.item.params.key == input.item.params.smeltProductKey) )&&
+        return (result.isNoneOrNull() || (result.item.params.key == input.item.params.smeltProductKey)&&
                 !input.isNoneOrNull() && input.item.params.smeltProductKey != null &&
                 (!fuel.isNoneOrNull() || burnProgress > 0f)
     }
@@ -118,7 +123,7 @@ class Furnace(gameItemsHolder: GameItemsHolder) : Container(SIZE, gameItemsHolde
         if (isActive && smeltProgress >= 1f) {
             val productKey = requireNotNull(input.item.params.smeltProductKey)
             val res = gameItemsHolder.getItem(productKey)
-            if (result.isNoneOrNull())  {
+            if (result.isNoneOrNull()) {
                 result = res.toInventoryItem()
             } else {
                 result.add()
@@ -132,7 +137,21 @@ class Furnace(gameItemsHolder: GameItemsHolder) : Container(SIZE, gameItemsHolde
         }
     }
 
+    override fun getSaveData(): SaveDataDto.FurnaceSaveData {
+        return SaveDataDto.FurnaceSaveData(
+            version = SAVE_DATA_VERSION,
+            size = size,
+            currentFuelItemKey = currentFuelKey,
+            items = items.map(InventoryItem::getSaveData),
+            startBurnTimeMs = startBurnTimeMs,
+            startSmeltTimeMs = smeltStarTimeMs,
+            burnProgress = burnProgress,
+            smeltProgress = smeltProgress,
+        )
+    }
+
     companion object {
+        private const val SAVE_DATA_VERSION = 1
         private const val SIZE = 3
         private const val TAG = "Furnace"
 
@@ -141,6 +160,23 @@ class Furnace(gameItemsHolder: GameItemsHolder) : Container(SIZE, gameItemsHolde
         const val RESULT_INDEX = 2
 
         const val SMELTING_TIME_MS = 10000L
+
+        fun fromSaveData(saveData: SaveDataDto.FurnaceSaveData, gameItemsHolder: GameItemsHolder): Furnace {
+            saveData.verifyVersion(SAVE_DATA_VERSION)
+
+            return Furnace(
+                gameItemsHolder = gameItemsHolder,
+                initialItems = saveData.items.map { item ->
+                    InventoryItem.fromSaveData(item, gameItemsHolder)
+                }
+            ).apply {
+                currentFuelKey = saveData.currentFuelItemKey
+                startBurnTimeMs = saveData.startSmeltTimeMs
+                smeltStarTimeMs = saveData.startSmeltTimeMs
+                burnProgress = saveData.burnProgress
+                smeltProgress = saveData.smeltProgress
+            }
+        }
     }
 
 }
\ No newline at end of file
index edb3c81ff657a63fd41b865e4dbaea9a7dc01230..548c44a5bbfb7a12fa0237dfa56d94cc2402d8a4 100644 (file)
@@ -4,14 +4,16 @@ import com.badlogic.gdx.math.Intersector
 import com.badlogic.gdx.math.Rectangle
 import com.badlogic.gdx.math.Vector2
 import ru.deadsoftware.cavedroid.game.GameItemsHolder
+import ru.deadsoftware.cavedroid.game.model.dto.SaveDataDto
 import ru.deadsoftware.cavedroid.game.model.item.Item
+import ru.deadsoftware.cavedroid.misc.Saveable
 
 class Drop @JvmOverloads constructor(
     x: Float,
     y: Float,
     _item: Item,
     _amount: Int = 1,
-) : Rectangle(x, y, DROP_SIZE, DROP_SIZE) {
+) : Rectangle(x, y, DROP_SIZE, DROP_SIZE), Saveable {
 
     var amount: Int = _amount
         private set
@@ -60,12 +62,44 @@ class Drop @JvmOverloads constructor(
         )
     }
 
+    override fun getSaveData(): SaveDataDto.DropSaveData {
+        return SaveDataDto.DropSaveData(
+            version = SAVE_DATA_VERSION,
+            x = x,
+            y = y,
+            width = width,
+            height = height,
+            velocityX = velocity.x,
+            velocityY = velocity.y,
+            itemKey = itemKey,
+            amount = amount,
+            pickedUp = pickedUp
+        )
+    }
+
     companion object {
+        private const val SAVE_DATA_VERSION = 1
+
         private const val MAGNET_DISTANCE = 8f
 
         const val MAGNET_VELOCITY = 256f
         const val DROP_SIZE = 8f
 
         private fun getInitialVelocity(): Vector2 = Vector2(0f, -1f)
+
+        fun fromSaveData(saveData: SaveDataDto.DropSaveData, gameItemsHolder: GameItemsHolder): Drop {
+            saveData.verifyVersion(SAVE_DATA_VERSION)
+
+            return Drop(
+                x = saveData.x,
+                y = saveData.y,
+                _item = gameItemsHolder.getItem(saveData.itemKey),
+                _amount = saveData.amount,
+            ).apply {
+                velocity.x = saveData.velocityX
+                velocity.y = saveData.velocityY
+                pickedUp = saveData.pickedUp
+            }
+        }
     }
 }
index 0213c971cbed124b6787a9fec6c570636ff8e47b..f8299139a2723cf9565b56670b74f4fef0ad7e82 100644 (file)
@@ -3,8 +3,10 @@ package ru.deadsoftware.cavedroid.game.objects.drop;
 import org.jetbrains.annotations.NotNull;
 import ru.deadsoftware.cavedroid.game.GameItemsHolder;
 import ru.deadsoftware.cavedroid.game.GameScope;
+import ru.deadsoftware.cavedroid.game.model.dto.SaveDataDto;
 import ru.deadsoftware.cavedroid.game.model.item.InventoryItem;
 import ru.deadsoftware.cavedroid.game.model.item.Item;
+import ru.deadsoftware.cavedroid.misc.Saveable;
 
 import javax.inject.Inject;
 import java.io.Serializable;
@@ -12,7 +14,9 @@ import java.util.Iterator;
 import java.util.LinkedList;
 
 @GameScope
-public class DropController implements Serializable {
+public class DropController implements Serializable, Saveable {
+
+    private static final int SAVE_DATA_VERSION = 1;
 
     public interface Callback {
         void run(Drop drop);
@@ -59,4 +63,27 @@ public class DropController implements Serializable {
         return mDrops.iterator();
     }
 
+    @Override
+    @NotNull
+    public SaveDataDto.DropControllerSaveData getSaveData() {
+        final LinkedList<SaveDataDto.DropSaveData> dropSaveData = new LinkedList<>();
+        for (Drop drop : mDrops) {
+            dropSaveData.add(drop.getSaveData());
+        }
+        return new SaveDataDto.DropControllerSaveData(SAVE_DATA_VERSION, dropSaveData);
+    }
+
+    public static DropController fromSaveData(
+            SaveDataDto.DropControllerSaveData saveData, GameItemsHolder gameItemsHolder) {
+        saveData.verifyVersion(SAVE_DATA_VERSION);
+
+        DropController controller = new DropController();
+
+        for (SaveDataDto.DropSaveData dropSaveData : saveData.getDrops()) {
+            controller.mDrops.add(Drop.Companion.fromSaveData(dropSaveData, gameItemsHolder));
+        }
+
+        return controller;
+    }
+
 }
diff --git a/core/src/ru/deadsoftware/cavedroid/game/save/GameSaveData.kt b/core/src/ru/deadsoftware/cavedroid/game/save/GameSaveData.kt
new file mode 100644 (file)
index 0000000..e0d64e7
--- /dev/null
@@ -0,0 +1,55 @@
+package ru.deadsoftware.cavedroid.game.save
+
+import ru.deadsoftware.cavedroid.game.mobs.MobsController
+import ru.deadsoftware.cavedroid.game.model.block.Block
+import ru.deadsoftware.cavedroid.game.objects.container.ContainerController
+import ru.deadsoftware.cavedroid.game.objects.drop.DropController
+
+class GameSaveData(
+    private var mobsController: MobsController?,
+    private var dropController: DropController?,
+    private var containerController: ContainerController?,
+    private var foreMap: Array<Array<Block>>?,
+    private var backMap: Array<Array<Block>>?
+) {
+
+    fun retrieveMobsController(): MobsController {
+        val value = requireNotNull(mobsController)
+        mobsController = null
+        return value
+    }
+
+    fun retrieveDropController(): DropController {
+        val value = requireNotNull(dropController)
+        dropController = null
+        return value
+    }
+
+    fun retrieveContainerController(): ContainerController {
+        val value = requireNotNull(containerController)
+        containerController = null
+        return value
+    }
+
+    fun retrieveForeMap(): Array<Array<Block>> {
+        val value = requireNotNull(foreMap)
+        foreMap = null
+        return value
+    }
+
+    fun retrieveBackMap(): Array<Array<Block>> {
+        val value = requireNotNull(backMap)
+        backMap = null
+        return value
+    }
+
+    fun isEmpty(): Boolean {
+        return mobsController == null &&
+                dropController == null &&
+                containerController == null &&
+                foreMap == null &&
+                backMap == null
+    }
+
+
+}
\ No newline at end of file
diff --git a/core/src/ru/deadsoftware/cavedroid/game/save/GameSaveLoader.kt b/core/src/ru/deadsoftware/cavedroid/game/save/GameSaveLoader.kt
new file mode 100644 (file)
index 0000000..bd8c3d6
--- /dev/null
@@ -0,0 +1,255 @@
+package ru.deadsoftware.cavedroid.game.save
+
+import com.badlogic.gdx.Gdx
+import com.badlogic.gdx.files.FileHandle
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.decodeFromByteArray
+import kotlinx.serialization.encodeToByteArray
+import kotlinx.serialization.protobuf.ProtoBuf
+import ru.deadsoftware.cavedroid.MainConfig
+import ru.deadsoftware.cavedroid.game.GameItemsHolder
+import ru.deadsoftware.cavedroid.game.mobs.MobsController
+import ru.deadsoftware.cavedroid.game.model.block.Block
+import ru.deadsoftware.cavedroid.game.model.dto.SaveDataDto
+import ru.deadsoftware.cavedroid.game.objects.container.ContainerController
+import ru.deadsoftware.cavedroid.game.objects.drop.DropController
+import ru.deadsoftware.cavedroid.game.ui.TooltipManager
+import ru.deadsoftware.cavedroid.game.world.GameWorld
+import java.nio.ByteBuffer
+import java.util.zip.GZIPInputStream
+import java.util.zip.GZIPOutputStream
+
+@OptIn(ExperimentalSerializationApi::class)
+object GameSaveLoader {
+
+    private const val MAP_SAVE_VERSION: UByte = 2u
+
+    private const val SAVES_DIR = "/saves"
+    private const val DROP_FILE = "/drop.dat"
+    private const val MOBS_FILE = "/mobs.dat"
+    private const val CONTAINERS_FILE = "/containers.dat"
+    private const val DICT_FILE = "/dict"
+    private const val FOREMAP_FILE = "/foremap.dat.gz"
+    private const val BACKMAP_FILE = "/backmap.dat.gz"
+
+    private fun Int.toByteArray(): ByteArray {
+        return ByteBuffer.allocate(Int.SIZE_BYTES)
+            .putInt(this)
+            .array()
+    }
+
+    private fun Short.toByteArray(): ByteArray {
+        return ByteBuffer.allocate(Short.SIZE_BYTES)
+            .putShort(this)
+            .array()
+    }
+
+    private fun buildBlocksDictionary(
+        foreMap: Array<Array<Block>>,
+        backMap: Array<Array<Block>>
+    ): Map<String, Int> {
+        val maps = sequenceOf(foreMap.asSequence(), backMap.asSequence())
+
+        return maps.flatten()
+            .flatMap(Array<Block>::asSequence)
+            .toSet()
+            .mapIndexed { index, block -> block.params.key to index }
+            .toMap()
+    }
+
+    private fun saveDict(file: FileHandle, dict: Map<String, Int>) {
+        val result = dict.asSequence()
+            .sortedBy { it.value }
+            .joinToString(separator = "\n") { it.key }
+            .encodeToByteArray()
+
+        file.writeBytes(result, false)
+    }
+
+    private fun compressMap(map: Array<Array<Block>>, dict: Map<String, Int>): ByteArray {
+        if (dict.size > 0xff) {
+            throw IllegalArgumentException("Cannot save this map as bytes")
+        }
+
+        val width = map.size
+        val height = map[0].size
+
+        val blocks = sequence {
+            for (y in 0 ..< height) {
+                for (x in 0 ..< width) {
+                    yield(map[x][y])
+                }
+            }
+        }
+
+        val result = sequence {
+            var run = 0
+            var runValue: UByte? = null
+
+            yield(MAP_SAVE_VERSION.toByte())
+            width.toByteArray().forEach { yield(it) }
+            height.toByteArray().forEach { yield(it) }
+
+            blocks.forEach { block ->
+                val key = block.params.key
+
+                val blockId = dict[key]?.toUByte()
+                    ?: throw IllegalArgumentException("Dictionary does not contain key $key")
+
+                if (blockId != runValue || run == Int.MAX_VALUE) {
+                    if (run > 0 && runValue != null) {
+                        run.toByteArray().forEach { yield(it) }
+                        yield(runValue!!.toByte())
+                    }
+                    run = 1
+                    runValue = blockId
+                } else {
+                    run++
+                }
+            }
+
+            run.toByteArray().forEach { yield(it) }
+            yield(runValue!!.toByte())
+        }
+
+        return result.toList().toByteArray()
+    }
+
+    private fun decompressMap(
+        bytes: ByteArray,
+        dict: List<String>,
+        gameItemsHolder: GameItemsHolder
+    ): Array<Array<Block>> {
+        val version = bytes.first().toUByte()
+        require(version == MAP_SAVE_VERSION)
+
+        val width = ByteBuffer.wrap(bytes, 1, Int.SIZE_BYTES).getInt()
+        val height = ByteBuffer.wrap(bytes, 1 + Int.SIZE_BYTES, Int.SIZE_BYTES).getInt()
+
+        val blocks = buildList {
+            for (i in 1 + (Int.SIZE_BYTES shl 1) .. bytes.lastIndex step Int.SIZE_BYTES + 1) {
+                val run = ByteBuffer.wrap(bytes, i, Int.SIZE_BYTES).getInt()
+                val blockId = bytes[i + Int.SIZE_BYTES].toUByte().toInt()
+
+                for (j in 0 ..< run) {
+                    add(gameItemsHolder.getBlock(dict[blockId]))
+                }
+            }
+        }
+
+        return Array(width) { x ->
+            Array(height) { y ->
+                blocks[x + y * width]
+            }
+        }
+    }
+
+    private fun loadMap(
+        gameItemsHolder: GameItemsHolder,
+        savesPath: String
+    ): Pair<Array<Array<Block>>, Array<Array<Block>>> {
+        val dict = Gdx.files.absolute("$savesPath$DICT_FILE").readString().split("\n")
+
+        val foreMap: Array<Array<Block>>
+        with(GZIPInputStream(Gdx.files.absolute("$savesPath$FOREMAP_FILE").read())) {
+            foreMap = decompressMap(readBytes(), dict, gameItemsHolder)
+            close()
+        }
+
+        val backMap: Array<Array<Block>>
+        with(GZIPInputStream(Gdx.files.absolute("$savesPath$BACKMAP_FILE").read())) {
+            backMap = decompressMap(readBytes(), dict, gameItemsHolder)
+            close()
+        }
+
+        return foreMap to backMap
+    }
+
+    private fun saveMap(gameWorld: GameWorld, savesPath: String) {
+        val fullForeMap = gameWorld.fullForeMap
+        val fullBackMap = gameWorld.fullBackMap
+
+        val dict = buildBlocksDictionary(fullForeMap, fullBackMap)
+
+        saveDict(Gdx.files.absolute("$savesPath$DICT_FILE"), dict)
+
+        with(GZIPOutputStream(Gdx.files.absolute("$savesPath$FOREMAP_FILE").write(false))) {
+            write(compressMap(fullForeMap, dict))
+            close()
+        }
+
+        with(GZIPOutputStream(Gdx.files.absolute("$savesPath$BACKMAP_FILE").write(false))) {
+            write(compressMap(fullBackMap, dict))
+            close()
+        }
+    }
+
+    fun load(
+        mainConfig: MainConfig,
+        gameItemsHolder: GameItemsHolder,
+        tooltipManager: TooltipManager
+    ): GameSaveData {
+        val gameFolder = mainConfig.gameFolder
+        val savesPath = "$gameFolder$SAVES_DIR"
+
+        val dropFile = Gdx.files.absolute("$savesPath$DROP_FILE")
+        val mobsFile = Gdx.files.absolute("$savesPath$MOBS_FILE")
+        val containersFile = Gdx.files.absolute("$savesPath$CONTAINERS_FILE")
+
+        val dropBytes = dropFile.readBytes()
+        val mobsBytes = mobsFile.readBytes()
+        val containersBytes = containersFile.readBytes()
+
+        val dropController = ProtoBuf.decodeFromByteArray<SaveDataDto.DropControllerSaveData>(dropBytes)
+            .let { saveData -> DropController.fromSaveData(saveData, gameItemsHolder) }
+        val mobsController = ProtoBuf.decodeFromByteArray<SaveDataDto.MobsControllerSaveData>(mobsBytes)
+            .let { saveData -> MobsController.fromSaveData(saveData, gameItemsHolder, tooltipManager) }
+        val containerController = ProtoBuf.decodeFromByteArray<SaveDataDto.ContainerControllerSaveData>(containersBytes)
+            .let { saveData -> ContainerController.fromSaveData(saveData, dropController, gameItemsHolder) }
+
+        val (foreMap, backMap) = loadMap(gameItemsHolder, savesPath)
+
+        return GameSaveData(mobsController, dropController, containerController, foreMap, backMap)
+    }
+
+    fun save(
+        mainConfig: MainConfig,
+        dropController: DropController,
+        mobsController: MobsController,
+        containerController: ContainerController,
+        gameWorld: GameWorld
+    ) {
+        val gameFolder = mainConfig.gameFolder
+        val savesPath = "$gameFolder$SAVES_DIR"
+
+        Gdx.files.absolute(savesPath).mkdirs()
+
+        val dropFile = Gdx.files.absolute("$savesPath$DROP_FILE")
+        val mobsFile = Gdx.files.absolute("$savesPath$MOBS_FILE")
+        val containersFile = Gdx.files.absolute("$savesPath$CONTAINERS_FILE")
+
+        val dropBytes = ProtoBuf.encodeToByteArray(dropController.getSaveData())
+        val mobsBytes = ProtoBuf.encodeToByteArray(mobsController.getSaveData())
+        val containersBytes = ProtoBuf.encodeToByteArray(containerController.getSaveData())
+
+        dropFile.writeBytes(dropBytes, false)
+        mobsFile.writeBytes(mobsBytes, false)
+        containersFile.writeBytes(containersBytes, false)
+
+        saveMap(gameWorld, savesPath)
+    }
+
+    fun exists(mainConfig: MainConfig): Boolean {
+        val gameFolder = mainConfig.gameFolder
+        val savesPath = "$gameFolder$SAVES_DIR"
+
+        return Gdx.files.absolute("$savesPath$DROP_FILE").exists() &&
+                Gdx.files.absolute("$savesPath$MOBS_FILE").exists() &&
+                Gdx.files.absolute("$savesPath$CONTAINERS_FILE").exists() &&
+                Gdx.files.absolute("$savesPath$DICT_FILE").exists() &&
+                Gdx.files.absolute("$savesPath$FOREMAP_FILE").exists() &&
+                Gdx.files.absolute("$savesPath$BACKMAP_FILE").exists()
+    }
+
+
+}
\ No newline at end of file
index 6a40fc534b1a5d04e5fb5d6d80c4ae94b8487777..29aeb8fb272b53f58101634c15a23e38573b48d6 100644 (file)
@@ -1,8 +1,7 @@
 package ru.deadsoftware.cavedroid.menu.submenus;
 
-import com.badlogic.gdx.Gdx;
 import ru.deadsoftware.cavedroid.MainConfig;
-import ru.deadsoftware.cavedroid.game.GameSaver;
+import ru.deadsoftware.cavedroid.game.save.GameSaveLoader;
 import ru.deadsoftware.cavedroid.menu.MenuProc;
 import ru.deadsoftware.cavedroid.menu.objects.Button;
 import ru.deadsoftware.cavedroid.menu.objects.ButtonEventListener;
@@ -36,7 +35,7 @@ public class MenuMain extends Menu {
     @Override
     protected void initButtons() {
         loadButtonsFromJson(mAssetLoader.getAssetHandle("json/menu_main_buttons.json"));
-        if (GameSaver.exists(mMainConfig)) {
+        if (GameSaveLoader.INSTANCE.exists(mMainConfig)) {
             getButtons().get("load_game").setType(Button.NORMAL);
         }
     }
diff --git a/core/src/ru/deadsoftware/cavedroid/misc/Saveable.kt b/core/src/ru/deadsoftware/cavedroid/misc/Saveable.kt
new file mode 100644 (file)
index 0000000..d350f1f
--- /dev/null
@@ -0,0 +1,7 @@
+package ru.deadsoftware.cavedroid.misc
+
+import ru.deadsoftware.cavedroid.game.model.dto.SaveDataDto
+
+interface Saveable {
+    fun getSaveData(): SaveDataDto
+}