From: fredboy Date: Tue, 21 May 2024 19:42:50 +0000 (+0700) Subject: New saving fromat and rewrite in kotlin X-Git-Url: http://deadsoftware.ru/gitweb?a=commitdiff_plain;h=76a3b56287b1262f78f3b43a4aac5660338d67ad;p=cavedroid.git New saving fromat and rewrite in kotlin --- diff --git a/core/build.gradle b/core/build.gradle index 3b061a7..ac46757 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -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 +} diff --git a/core/src/ru/deadsoftware/cavedroid/game/GameModule.java b/core/src/ru/deadsoftware/cavedroid/game/GameModule.java index d57ab33..29aac7d 100644 --- a/core/src/ru/deadsoftware/cavedroid/game/GameModule.java +++ b/core/src/ru/deadsoftware/cavedroid/game/GameModule.java @@ -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 index 39d2278..0000000 --- a/core/src/ru/deadsoftware/cavedroid/game/GameSaver.java +++ /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 buildBlocksDictionary(Block[][] foreMap, Block[][] backMap) { - final HashMap 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 dict) { - final String[] arr = new String[dict.size()]; - - for (Map.Entry 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 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 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()); - } -} diff --git a/core/src/ru/deadsoftware/cavedroid/game/input/handler/keyboard/PauseGameKeyboardInputHandler.kt b/core/src/ru/deadsoftware/cavedroid/game/input/handler/keyboard/PauseGameKeyboardInputHandler.kt index cedc78a..0311c88 100644 --- a/core/src/ru/deadsoftware/cavedroid/game/input/handler/keyboard/PauseGameKeyboardInputHandler.kt +++ b/core/src/ru/deadsoftware/cavedroid/game/input/handler/keyboard/PauseGameKeyboardInputHandler.kt @@ -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 diff --git a/core/src/ru/deadsoftware/cavedroid/game/mobs/FallingBlock.kt b/core/src/ru/deadsoftware/cavedroid/game/mobs/FallingBlock.kt index f273504..77e51a1 100644 --- a/core/src/ru/deadsoftware/cavedroid/game/mobs/FallingBlock.kt +++ b/core/src/ru/deadsoftware/cavedroid/game/mobs/FallingBlock.kt @@ -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 diff --git a/core/src/ru/deadsoftware/cavedroid/game/mobs/Mob.java b/core/src/ru/deadsoftware/cavedroid/game/mobs/Mob.java index e4ca679..0f3610b 100644 --- a/core/src/ru/deadsoftware/cavedroid/game/mobs/Mob.java +++ b/core/src/ru/deadsoftware/cavedroid/game/mobs/Mob.java @@ -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 index 0000000..49adee3 --- /dev/null +++ b/core/src/ru/deadsoftware/cavedroid/game/mobs/MobSaveDataMapper.kt @@ -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 diff --git a/core/src/ru/deadsoftware/cavedroid/game/mobs/MobsController.kt b/core/src/ru/deadsoftware/cavedroid/game/mobs/MobsController.kt index 158d22a..dc01e1e 100644 --- a/core/src/ru/deadsoftware/cavedroid/game/mobs/MobsController.kt +++ b/core/src/ru/deadsoftware/cavedroid/game/mobs/MobsController.kt @@ -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() - val player: Player = - Player(gameItemsHolder, tooltipManager) + var player: Player = Player(gameItemsHolder, tooltipManager) + private set val mobs: List 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 diff --git a/core/src/ru/deadsoftware/cavedroid/game/mobs/Pig.kt b/core/src/ru/deadsoftware/cavedroid/game/mobs/Pig.kt index b3d647d..b080e40 100644 --- a/core/src/ru/deadsoftware/cavedroid/game/mobs/Pig.kt +++ b/core/src/ru/deadsoftware/cavedroid/game/mobs/Pig.kt @@ -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 diff --git a/core/src/ru/deadsoftware/cavedroid/game/mobs/player/Inventory.kt b/core/src/ru/deadsoftware/cavedroid/game/mobs/player/Inventory.kt index 43f298c..8f0b90d 100644 --- a/core/src/ru/deadsoftware/cavedroid/game/mobs/player/Inventory.kt +++ b/core/src/ru/deadsoftware/cavedroid/game/mobs/player/Inventory.kt @@ -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? = null +) : Serializable, Saveable { @Suppress("UNNECESSARY_LATEINIT") @Transient @@ -22,6 +25,8 @@ class Inventory( @Transient private lateinit var fallbackItem: InventoryItem + private val _items: Array + 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 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 diff --git a/core/src/ru/deadsoftware/cavedroid/game/mobs/player/Player.java b/core/src/ru/deadsoftware/cavedroid/game/mobs/player/Player.java index 8ea87c1..f454b0c 100644 --- a/core/src/ru/deadsoftware/cavedroid/game/mobs/player/Player.java +++ b/core/src/ru/deadsoftware/cavedroid/game/mobs/player/Player.java @@ -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 index 0000000..e6ad828 --- /dev/null +++ b/core/src/ru/deadsoftware/cavedroid/game/model/dto/SaveDataDto.kt @@ -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 + } + + @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, + ) : ContainerSaveDataDto() + + @Serializable + data class FurnaceSaveData( + override val version: Int, + override val size: Int, + val currentFuelItemKey: String?, + override val items: List, + 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 + ) : ContainerSaveDataDto() + + @Serializable + data class ContainerControllerSaveData( + override val version: Int, + val containerMap: Map, + ): 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 + ) : 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 diff --git a/core/src/ru/deadsoftware/cavedroid/game/model/item/InventoryItem.kt b/core/src/ru/deadsoftware/cavedroid/game/model/item/InventoryItem.kt index 3cd406b..7190988 100644 --- a/core/src/ru/deadsoftware/cavedroid/game/model/item/InventoryItem.kt +++ b/core/src/ru/deadsoftware/cavedroid/game/model/item/InventoryItem.kt @@ -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 + } } + } diff --git a/core/src/ru/deadsoftware/cavedroid/game/objects/container/Chest.kt b/core/src/ru/deadsoftware/cavedroid/game/objects/container/Chest.kt index be820bc..a9e893b 100644 --- a/core/src/ru/deadsoftware/cavedroid/game/objects/container/Chest.kt +++ b/core/src/ru/deadsoftware/cavedroid/game/objects/container/Chest.kt @@ -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? = 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 diff --git a/core/src/ru/deadsoftware/cavedroid/game/objects/container/Container.kt b/core/src/ru/deadsoftware/cavedroid/game/objects/container/Container.kt index 1e13c64..05ec744 100644 --- a/core/src/ru/deadsoftware/cavedroid/game/objects/container/Container.kt +++ b/core/src/ru/deadsoftware/cavedroid/game/objects/container/Container.kt @@ -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? = 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 @@ -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 diff --git a/core/src/ru/deadsoftware/cavedroid/game/objects/container/ContainerController.kt b/core/src/ru/deadsoftware/cavedroid/game/objects/container/ContainerController.kt index 3facb6f..bffb846 100644 --- a/core/src/ru/deadsoftware/cavedroid/game/objects/container/ContainerController.kt +++ b/core/src/ru/deadsoftware/cavedroid/game/objects/container/ContainerController.kt @@ -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 diff --git a/core/src/ru/deadsoftware/cavedroid/game/objects/container/Furnace.kt b/core/src/ru/deadsoftware/cavedroid/game/objects/container/Furnace.kt index 1a75e89..92eeab9 100644 --- a/core/src/ru/deadsoftware/cavedroid/game/objects/container/Furnace.kt +++ b/core/src/ru/deadsoftware/cavedroid/game/objects/container/Furnace.kt @@ -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? = 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 diff --git a/core/src/ru/deadsoftware/cavedroid/game/objects/drop/Drop.kt b/core/src/ru/deadsoftware/cavedroid/game/objects/drop/Drop.kt index edb3c81..548c44a 100644 --- a/core/src/ru/deadsoftware/cavedroid/game/objects/drop/Drop.kt +++ b/core/src/ru/deadsoftware/cavedroid/game/objects/drop/Drop.kt @@ -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 + } + } } } diff --git a/core/src/ru/deadsoftware/cavedroid/game/objects/drop/DropController.java b/core/src/ru/deadsoftware/cavedroid/game/objects/drop/DropController.java index 0213c97..f829913 100644 --- a/core/src/ru/deadsoftware/cavedroid/game/objects/drop/DropController.java +++ b/core/src/ru/deadsoftware/cavedroid/game/objects/drop/DropController.java @@ -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 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 index 0000000..e0d64e7 --- /dev/null +++ b/core/src/ru/deadsoftware/cavedroid/game/save/GameSaveData.kt @@ -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>?, + private var backMap: Array>? +) { + + 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> { + val value = requireNotNull(foreMap) + foreMap = null + return value + } + + fun retrieveBackMap(): Array> { + 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 index 0000000..bd8c3d6 --- /dev/null +++ b/core/src/ru/deadsoftware/cavedroid/game/save/GameSaveLoader.kt @@ -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>, + backMap: Array> + ): Map { + val maps = sequenceOf(foreMap.asSequence(), backMap.asSequence()) + + return maps.flatten() + .flatMap(Array::asSequence) + .toSet() + .mapIndexed { index, block -> block.params.key to index } + .toMap() + } + + private fun saveDict(file: FileHandle, dict: Map) { + val result = dict.asSequence() + .sortedBy { it.value } + .joinToString(separator = "\n") { it.key } + .encodeToByteArray() + + file.writeBytes(result, false) + } + + private fun compressMap(map: Array>, dict: Map): 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, + gameItemsHolder: GameItemsHolder + ): Array> { + 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>> { + val dict = Gdx.files.absolute("$savesPath$DICT_FILE").readString().split("\n") + + val foreMap: Array> + with(GZIPInputStream(Gdx.files.absolute("$savesPath$FOREMAP_FILE").read())) { + foreMap = decompressMap(readBytes(), dict, gameItemsHolder) + close() + } + + val backMap: Array> + 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(dropBytes) + .let { saveData -> DropController.fromSaveData(saveData, gameItemsHolder) } + val mobsController = ProtoBuf.decodeFromByteArray(mobsBytes) + .let { saveData -> MobsController.fromSaveData(saveData, gameItemsHolder, tooltipManager) } + val containerController = ProtoBuf.decodeFromByteArray(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 diff --git a/core/src/ru/deadsoftware/cavedroid/menu/submenus/MenuMain.java b/core/src/ru/deadsoftware/cavedroid/menu/submenus/MenuMain.java index 6a40fc5..29aeb8f 100644 --- a/core/src/ru/deadsoftware/cavedroid/menu/submenus/MenuMain.java +++ b/core/src/ru/deadsoftware/cavedroid/menu/submenus/MenuMain.java @@ -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 index 0000000..d350f1f --- /dev/null +++ b/core/src/ru/deadsoftware/cavedroid/misc/Saveable.kt @@ -0,0 +1,7 @@ +package ru.deadsoftware.cavedroid.misc + +import ru.deadsoftware.cavedroid.game.model.dto.SaveDataDto + +interface Saveable { + fun getSaveData(): SaveDataDto +}