DEADSOFTWARE

New saving fromat and rewrite in kotlin
[cavedroid.git] / core / src / ru / deadsoftware / cavedroid / game / save / GameSaveLoader.kt
1 package ru.deadsoftware.cavedroid.game.save
3 import com.badlogic.gdx.Gdx
4 import com.badlogic.gdx.files.FileHandle
5 import kotlinx.serialization.ExperimentalSerializationApi
6 import kotlinx.serialization.decodeFromByteArray
7 import kotlinx.serialization.encodeToByteArray
8 import kotlinx.serialization.protobuf.ProtoBuf
9 import ru.deadsoftware.cavedroid.MainConfig
10 import ru.deadsoftware.cavedroid.game.GameItemsHolder
11 import ru.deadsoftware.cavedroid.game.mobs.MobsController
12 import ru.deadsoftware.cavedroid.game.model.block.Block
13 import ru.deadsoftware.cavedroid.game.model.dto.SaveDataDto
14 import ru.deadsoftware.cavedroid.game.objects.container.ContainerController
15 import ru.deadsoftware.cavedroid.game.objects.drop.DropController
16 import ru.deadsoftware.cavedroid.game.ui.TooltipManager
17 import ru.deadsoftware.cavedroid.game.world.GameWorld
18 import java.nio.ByteBuffer
19 import java.util.zip.GZIPInputStream
20 import java.util.zip.GZIPOutputStream
22 @OptIn(ExperimentalSerializationApi::class)
23 object GameSaveLoader {
25 private const val MAP_SAVE_VERSION: UByte = 2u
27 private const val SAVES_DIR = "/saves"
28 private const val DROP_FILE = "/drop.dat"
29 private const val MOBS_FILE = "/mobs.dat"
30 private const val CONTAINERS_FILE = "/containers.dat"
31 private const val DICT_FILE = "/dict"
32 private const val FOREMAP_FILE = "/foremap.dat.gz"
33 private const val BACKMAP_FILE = "/backmap.dat.gz"
35 private fun Int.toByteArray(): ByteArray {
36 return ByteBuffer.allocate(Int.SIZE_BYTES)
37 .putInt(this)
38 .array()
39 }
41 private fun Short.toByteArray(): ByteArray {
42 return ByteBuffer.allocate(Short.SIZE_BYTES)
43 .putShort(this)
44 .array()
45 }
47 private fun buildBlocksDictionary(
48 foreMap: Array<Array<Block>>,
49 backMap: Array<Array<Block>>
50 ): Map<String, Int> {
51 val maps = sequenceOf(foreMap.asSequence(), backMap.asSequence())
53 return maps.flatten()
54 .flatMap(Array<Block>::asSequence)
55 .toSet()
56 .mapIndexed { index, block -> block.params.key to index }
57 .toMap()
58 }
60 private fun saveDict(file: FileHandle, dict: Map<String, Int>) {
61 val result = dict.asSequence()
62 .sortedBy { it.value }
63 .joinToString(separator = "\n") { it.key }
64 .encodeToByteArray()
66 file.writeBytes(result, false)
67 }
69 private fun compressMap(map: Array<Array<Block>>, dict: Map<String, Int>): ByteArray {
70 if (dict.size > 0xff) {
71 throw IllegalArgumentException("Cannot save this map as bytes")
72 }
74 val width = map.size
75 val height = map[0].size
77 val blocks = sequence {
78 for (y in 0 ..< height) {
79 for (x in 0 ..< width) {
80 yield(map[x][y])
81 }
82 }
83 }
85 val result = sequence {
86 var run = 0
87 var runValue: UByte? = null
89 yield(MAP_SAVE_VERSION.toByte())
90 width.toByteArray().forEach { yield(it) }
91 height.toByteArray().forEach { yield(it) }
93 blocks.forEach { block ->
94 val key = block.params.key
96 val blockId = dict[key]?.toUByte()
97 ?: throw IllegalArgumentException("Dictionary does not contain key $key")
99 if (blockId != runValue || run == Int.MAX_VALUE) {
100 if (run > 0 && runValue != null) {
101 run.toByteArray().forEach { yield(it) }
102 yield(runValue!!.toByte())
104 run = 1
105 runValue = blockId
106 } else {
107 run++
111 run.toByteArray().forEach { yield(it) }
112 yield(runValue!!.toByte())
115 return result.toList().toByteArray()
118 private fun decompressMap(
119 bytes: ByteArray,
120 dict: List<String>,
121 gameItemsHolder: GameItemsHolder
122 ): Array<Array<Block>> {
123 val version = bytes.first().toUByte()
124 require(version == MAP_SAVE_VERSION)
126 val width = ByteBuffer.wrap(bytes, 1, Int.SIZE_BYTES).getInt()
127 val height = ByteBuffer.wrap(bytes, 1 + Int.SIZE_BYTES, Int.SIZE_BYTES).getInt()
129 val blocks = buildList {
130 for (i in 1 + (Int.SIZE_BYTES shl 1) .. bytes.lastIndex step Int.SIZE_BYTES + 1) {
131 val run = ByteBuffer.wrap(bytes, i, Int.SIZE_BYTES).getInt()
132 val blockId = bytes[i + Int.SIZE_BYTES].toUByte().toInt()
134 for (j in 0 ..< run) {
135 add(gameItemsHolder.getBlock(dict[blockId]))
140 return Array(width) { x ->
141 Array(height) { y ->
142 blocks[x + y * width]
147 private fun loadMap(
148 gameItemsHolder: GameItemsHolder,
149 savesPath: String
150 ): Pair<Array<Array<Block>>, Array<Array<Block>>> {
151 val dict = Gdx.files.absolute("$savesPath$DICT_FILE").readString().split("\n")
153 val foreMap: Array<Array<Block>>
154 with(GZIPInputStream(Gdx.files.absolute("$savesPath$FOREMAP_FILE").read())) {
155 foreMap = decompressMap(readBytes(), dict, gameItemsHolder)
156 close()
159 val backMap: Array<Array<Block>>
160 with(GZIPInputStream(Gdx.files.absolute("$savesPath$BACKMAP_FILE").read())) {
161 backMap = decompressMap(readBytes(), dict, gameItemsHolder)
162 close()
165 return foreMap to backMap
168 private fun saveMap(gameWorld: GameWorld, savesPath: String) {
169 val fullForeMap = gameWorld.fullForeMap
170 val fullBackMap = gameWorld.fullBackMap
172 val dict = buildBlocksDictionary(fullForeMap, fullBackMap)
174 saveDict(Gdx.files.absolute("$savesPath$DICT_FILE"), dict)
176 with(GZIPOutputStream(Gdx.files.absolute("$savesPath$FOREMAP_FILE").write(false))) {
177 write(compressMap(fullForeMap, dict))
178 close()
181 with(GZIPOutputStream(Gdx.files.absolute("$savesPath$BACKMAP_FILE").write(false))) {
182 write(compressMap(fullBackMap, dict))
183 close()
187 fun load(
188 mainConfig: MainConfig,
189 gameItemsHolder: GameItemsHolder,
190 tooltipManager: TooltipManager
191 ): GameSaveData {
192 val gameFolder = mainConfig.gameFolder
193 val savesPath = "$gameFolder$SAVES_DIR"
195 val dropFile = Gdx.files.absolute("$savesPath$DROP_FILE")
196 val mobsFile = Gdx.files.absolute("$savesPath$MOBS_FILE")
197 val containersFile = Gdx.files.absolute("$savesPath$CONTAINERS_FILE")
199 val dropBytes = dropFile.readBytes()
200 val mobsBytes = mobsFile.readBytes()
201 val containersBytes = containersFile.readBytes()
203 val dropController = ProtoBuf.decodeFromByteArray<SaveDataDto.DropControllerSaveData>(dropBytes)
204 .let { saveData -> DropController.fromSaveData(saveData, gameItemsHolder) }
205 val mobsController = ProtoBuf.decodeFromByteArray<SaveDataDto.MobsControllerSaveData>(mobsBytes)
206 .let { saveData -> MobsController.fromSaveData(saveData, gameItemsHolder, tooltipManager) }
207 val containerController = ProtoBuf.decodeFromByteArray<SaveDataDto.ContainerControllerSaveData>(containersBytes)
208 .let { saveData -> ContainerController.fromSaveData(saveData, dropController, gameItemsHolder) }
210 val (foreMap, backMap) = loadMap(gameItemsHolder, savesPath)
212 return GameSaveData(mobsController, dropController, containerController, foreMap, backMap)
215 fun save(
216 mainConfig: MainConfig,
217 dropController: DropController,
218 mobsController: MobsController,
219 containerController: ContainerController,
220 gameWorld: GameWorld
221 ) {
222 val gameFolder = mainConfig.gameFolder
223 val savesPath = "$gameFolder$SAVES_DIR"
225 Gdx.files.absolute(savesPath).mkdirs()
227 val dropFile = Gdx.files.absolute("$savesPath$DROP_FILE")
228 val mobsFile = Gdx.files.absolute("$savesPath$MOBS_FILE")
229 val containersFile = Gdx.files.absolute("$savesPath$CONTAINERS_FILE")
231 val dropBytes = ProtoBuf.encodeToByteArray(dropController.getSaveData())
232 val mobsBytes = ProtoBuf.encodeToByteArray(mobsController.getSaveData())
233 val containersBytes = ProtoBuf.encodeToByteArray(containerController.getSaveData())
235 dropFile.writeBytes(dropBytes, false)
236 mobsFile.writeBytes(mobsBytes, false)
237 containersFile.writeBytes(containersBytes, false)
239 saveMap(gameWorld, savesPath)
242 fun exists(mainConfig: MainConfig): Boolean {
243 val gameFolder = mainConfig.gameFolder
244 val savesPath = "$gameFolder$SAVES_DIR"
246 return Gdx.files.absolute("$savesPath$DROP_FILE").exists() &&
247 Gdx.files.absolute("$savesPath$MOBS_FILE").exists() &&
248 Gdx.files.absolute("$savesPath$CONTAINERS_FILE").exists() &&
249 Gdx.files.absolute("$savesPath$DICT_FILE").exists() &&
250 Gdx.files.absolute("$savesPath$FOREMAP_FILE").exists() &&
251 Gdx.files.absolute("$savesPath$BACKMAP_FILE").exists()