mirror of
https://github.com/curioustorvald/Terrarum.git
synced 2026-06-11 02:54:04 +09:00
serialisation of the world is split into two packages: complex one is moved under the modulebasegame package
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
package net.torvald.terrarum.modulebasegame.serialise
|
||||
|
||||
import net.torvald.gdx.graphics.PixmapIO2
|
||||
import net.torvald.terrarum.App.printdbg
|
||||
import net.torvald.terrarum.ItemCodex
|
||||
import net.torvald.terrarum.ModMgr
|
||||
import net.torvald.terrarum.ReferencingRanges.PREFIX_DYNAMICITEM
|
||||
import net.torvald.terrarum.modulebasegame.IngameRenderer
|
||||
import net.torvald.terrarum.modulebasegame.TerrarumIngame
|
||||
import net.torvald.terrarum.modulebasegame.gameactors.FixtureBase
|
||||
import net.torvald.terrarum.modulebasegame.gameactors.IngamePlayer
|
||||
import net.torvald.terrarum.modulebasegame.gameactors.Pocketed
|
||||
import net.torvald.terrarum.realestate.LandUtil
|
||||
import net.torvald.terrarum.toInt
|
||||
import net.torvald.terrarum.savegame.*
|
||||
import net.torvald.terrarum.serialise.Common
|
||||
import java.io.File
|
||||
import java.util.zip.GZIPOutputStream
|
||||
|
||||
/**
|
||||
* Will happily overwrite existing entry
|
||||
*/
|
||||
private fun addFile(disk: VirtualDisk, file: DiskEntry) {
|
||||
disk.entries[file.entryID] = file
|
||||
file.parentEntryID = 0
|
||||
val dir = VDUtil.getAsDirectory(disk, 0)
|
||||
if (!dir.contains(file.entryID)) dir.add(file.entryID)
|
||||
}
|
||||
|
||||
abstract class SavingThread(private val errorHandler: (Throwable) -> Unit) : Runnable {
|
||||
abstract fun save()
|
||||
|
||||
override fun run() {
|
||||
try {
|
||||
save()
|
||||
}
|
||||
catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
errorHandler(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Created by minjaesong on 2021-09-14.
|
||||
*/
|
||||
class WorldSavingThread(
|
||||
val time_t: Long,
|
||||
val disk: VirtualDisk,
|
||||
val outFile: File,
|
||||
val ingame: TerrarumIngame,
|
||||
val hasThumbnail: Boolean,
|
||||
val isAuto: Boolean,
|
||||
val callback: () -> Unit,
|
||||
val errorHandler: (Throwable) -> Unit
|
||||
) : SavingThread(errorHandler) {
|
||||
|
||||
override fun save() {
|
||||
|
||||
disk.saveMode = 2 * isAuto.toInt() // no quick
|
||||
|
||||
if (hasThumbnail) {
|
||||
while (!IngameRenderer.fboRGBexportedLatch) {
|
||||
Thread.sleep(1L)
|
||||
}
|
||||
}
|
||||
|
||||
val allTheActors = ingame.actorContainerActive.cloneToList() + ingame.actorContainerInactive.cloneToList()
|
||||
|
||||
val playersList: List<IngamePlayer> = allTheActors.filterIsInstance<IngamePlayer>()
|
||||
val actorsList = allTheActors.filter { WriteWorld.actorAcceptable(it) }
|
||||
val layers = intArrayOf(0,1).map { ingame.world.getLayer(it) }
|
||||
val cw = ingame.world.width / LandUtil.CHUNK_W
|
||||
val ch = ingame.world.height / LandUtil.CHUNK_H
|
||||
|
||||
WriteSavegame.saveProgress = 0f
|
||||
WriteSavegame.saveProgressMax = 3f + (cw * ch * layers.size) + actorsList.size
|
||||
|
||||
|
||||
val tgaout = ByteArray64GrowableOutputStream()
|
||||
val gzout = GZIPOutputStream(tgaout)
|
||||
|
||||
printdbg(this, "Writing metadata...")
|
||||
|
||||
val creation_t = ingame.world.creationTime
|
||||
|
||||
|
||||
// Write subset of Ingame.ItemCodex
|
||||
// The existing ItemCodex must be rewritten to clear out obsolete records
|
||||
|
||||
// We're assuming the dynamic item generated by players does exist in the world, and it's recorded
|
||||
// into the world's dynamicToStaticTable, therefore every item recorded into the world's dynamicToStaticTable
|
||||
// can be found in this world without need to look up the players
|
||||
ingame.world.dynamicToStaticTable.clear()
|
||||
ingame.world.dynamicItemInventory.clear()
|
||||
actorsList.filterIsInstance<Pocketed>().forEach { actor ->
|
||||
actor.inventory.forEach { (itemid, _) ->
|
||||
|
||||
printdbg(this, "World side dynamicitem: $itemid contained in $actor")
|
||||
|
||||
if (itemid.startsWith("$PREFIX_DYNAMICITEM:")) {
|
||||
ingame.world.dynamicToStaticTable[itemid] = ItemCodex.dynamicToStaticID(itemid)
|
||||
ingame.world.dynamicItemInventory[itemid] = ItemCodex[itemid]!!
|
||||
}
|
||||
}
|
||||
}
|
||||
actorsList.filterIsInstance<FixtureBase>().forEach { fixture ->
|
||||
fixture.inventory?.forEach { (itemid, _) ->
|
||||
|
||||
printdbg(this, "World side dynamicitem: $itemid contained in $fixture")
|
||||
|
||||
if (itemid.startsWith("$PREFIX_DYNAMICITEM:")) {
|
||||
ingame.world.dynamicToStaticTable[itemid] = ItemCodex.dynamicToStaticID(itemid)
|
||||
ingame.world.dynamicItemInventory[itemid] = ItemCodex[itemid]!!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (hasThumbnail) {
|
||||
PixmapIO2._writeTGA(gzout, IngameRenderer.fboRGBexport, true, true)
|
||||
IngameRenderer.fboRGBexport.dispose()
|
||||
|
||||
val thumbContent = EntryFile(tgaout.toByteArray64())
|
||||
val thumb = DiskEntry(-2, 0, creation_t, time_t, thumbContent)
|
||||
addFile(disk, thumb)
|
||||
}
|
||||
|
||||
WriteSavegame.saveProgress += 1f
|
||||
|
||||
// Write World //
|
||||
|
||||
val worldMeta = EntryFile(WriteWorld.encodeToByteArray64(ingame, time_t, actorsList, playersList))
|
||||
val world = DiskEntry(-1L, 0, creation_t, time_t, worldMeta)
|
||||
addFile(disk, world)
|
||||
|
||||
WriteSavegame.saveProgress += 1f
|
||||
|
||||
|
||||
for (layer in layers.indices) {
|
||||
for (cx in 0 until cw) {
|
||||
for (cy in 0 until ch) {
|
||||
val chunkNumber = LandUtil.chunkXYtoChunkNum(ingame.world, cx, cy).toLong()
|
||||
|
||||
// Echo("Writing chunks... ${(cw*ch*layer) + chunkNumber + 1}/${cw*ch*layers.size}")
|
||||
|
||||
val chunkBytes = WriteWorld.encodeChunk(layers[layer]!!, cx, cy)
|
||||
val entryID = 0x1_0000_0000L or layer.toLong().shl(24) or chunkNumber
|
||||
|
||||
val entryContent = EntryFile(chunkBytes)
|
||||
val entry = DiskEntry(entryID, 0, creation_t, time_t, entryContent)
|
||||
// "W1L0-92,15"
|
||||
addFile(disk, entry)
|
||||
|
||||
WriteSavegame.saveProgress += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Write Actors //
|
||||
actorsList.forEachIndexed { count, it ->
|
||||
// Echo("Writing actors... ${count+1}/${actorsList.size}")
|
||||
|
||||
val actorContent = EntryFile(WriteActor.encodeToByteArray64(it))
|
||||
val actor = DiskEntry(it.referenceID.toLong(), 0, creation_t, time_t, actorContent)
|
||||
addFile(disk, actor)
|
||||
|
||||
WriteSavegame.saveProgress += 1
|
||||
}
|
||||
|
||||
|
||||
// write loadorder //
|
||||
val loadOrderBa64Writer = ByteArray64Writer(Common.CHARSET)
|
||||
loadOrderBa64Writer.write(ModMgr.loadOrder.joinToString("\n"))
|
||||
loadOrderBa64Writer.flush(); loadOrderBa64Writer.close()
|
||||
val loadOrderText = loadOrderBa64Writer.toByteArray64()
|
||||
val loadOrderContents = EntryFile(loadOrderText)
|
||||
addFile(disk, DiskEntry(-4L, 0L, creation_t, time_t, loadOrderContents))
|
||||
|
||||
|
||||
|
||||
// Echo("Writing file to disk...")
|
||||
|
||||
disk.entries[0]!!.modificationDate = time_t
|
||||
// entry zero MUST NOT be used to get lastPlayDate, but we'll update it anyway
|
||||
// use entry -1 for that purpose!
|
||||
disk.capacity = 0
|
||||
VDUtil.dumpToRealMachine(disk, outFile)
|
||||
|
||||
|
||||
|
||||
printdbg(this, "Game saved with size of ${outFile.length()} bytes")
|
||||
|
||||
|
||||
if (hasThumbnail) IngameRenderer.fboRGBexportedLatch = false
|
||||
WriteSavegame.savingStatus = 255
|
||||
|
||||
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function called means the "Avatar" was not externally created and thus has no sprite-bodypart-name-to-entry-number-map
|
||||
*
|
||||
* Created by minjaesong on 2021-10-08
|
||||
*/
|
||||
class PlayerSavingThread(
|
||||
val time_t: Long,
|
||||
val disk: VirtualDisk,
|
||||
val outFile: File,
|
||||
val ingame: TerrarumIngame,
|
||||
val hasThumbnail: Boolean,
|
||||
val isAuto: Boolean,
|
||||
val callback: () -> Unit,
|
||||
val errorHandler: (Throwable) -> Unit
|
||||
) : SavingThread(errorHandler) {
|
||||
|
||||
override fun save() {
|
||||
disk.saveMode = 2 * isAuto.toInt() // no quick
|
||||
disk.capacity = 0L
|
||||
|
||||
WriteSavegame.saveProgress = 0f
|
||||
|
||||
printdbg(this, "Writing The Player...")
|
||||
WritePlayer(ingame.actorGamer, disk, ingame, time_t)
|
||||
disk.entries[0]!!.modificationDate = time_t
|
||||
VDUtil.dumpToRealMachine(disk, outFile)
|
||||
|
||||
callback()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package net.torvald.terrarum.modulebasegame.serialise
|
||||
|
||||
import net.torvald.gdx.graphics.PixmapIO2
|
||||
import net.torvald.terrarum.App.printdbg
|
||||
import net.torvald.terrarum.modulebasegame.IngameRenderer
|
||||
import net.torvald.terrarum.modulebasegame.TerrarumIngame
|
||||
import net.torvald.terrarum.modulebasegame.gameactors.IngamePlayer
|
||||
import net.torvald.terrarum.realestate.LandUtil
|
||||
import net.torvald.terrarum.savegame.*
|
||||
import net.torvald.terrarum.serialise.Common
|
||||
import net.torvald.terrarum.toInt
|
||||
import net.torvald.terrarum.utils.PlayerLastStatus
|
||||
import java.io.File
|
||||
import java.util.zip.GZIPOutputStream
|
||||
|
||||
/**
|
||||
* Created by minjaesong on 2021-09-29.
|
||||
*/
|
||||
class QuickSingleplayerWorldSavingThread(
|
||||
val time_t: Long,
|
||||
val disk: VirtualDisk,
|
||||
val outFile: File,
|
||||
val ingame: TerrarumIngame,
|
||||
val hasThumbnail: Boolean,
|
||||
val isAuto: Boolean,
|
||||
val callback: () -> Unit,
|
||||
val errorHandler: (Throwable) -> Unit
|
||||
) : SavingThread(errorHandler) {
|
||||
|
||||
/**
|
||||
* Will happily overwrite existing entry
|
||||
*/
|
||||
private fun addFile(disk: VirtualDisk, file: DiskEntry) {
|
||||
disk.entries[file.entryID] = file
|
||||
file.parentEntryID = 0
|
||||
val dir = VDUtil.getAsDirectory(disk, 0)
|
||||
if (!dir.contains(file.entryID)) dir.add(file.entryID)
|
||||
}
|
||||
|
||||
|
||||
private val chunkProgressMultiplier = 1f
|
||||
private val actorProgressMultiplier = 1f
|
||||
|
||||
|
||||
override fun save() {
|
||||
val skimmer = DiskSkimmer(outFile, Common.CHARSET)
|
||||
|
||||
if (hasThumbnail) {
|
||||
while (!IngameRenderer.fboRGBexportedLatch) {
|
||||
Thread.sleep(1L)
|
||||
}
|
||||
}
|
||||
|
||||
val allTheActors = ingame.actorContainerActive.cloneToList() + ingame.actorContainerInactive.cloneToList()
|
||||
|
||||
val playersList: List<IngamePlayer> = allTheActors.filter{ it is IngamePlayer } as List<IngamePlayer>
|
||||
val actorsList = allTheActors.filter { WriteWorld.actorAcceptable(it) }
|
||||
val chunks = ingame.modifiedChunks
|
||||
|
||||
val chunkCount = chunks.map { it.size }.sum()
|
||||
|
||||
WriteSavegame.saveProgress = 0f
|
||||
WriteSavegame.saveProgressMax = 2f +
|
||||
(chunkCount) * chunkProgressMultiplier +
|
||||
actorsList.size * actorProgressMultiplier
|
||||
|
||||
|
||||
val tgaout = ByteArray64GrowableOutputStream()
|
||||
val gzout = GZIPOutputStream(tgaout)
|
||||
|
||||
printdbg(this, "Writing metadata...")
|
||||
|
||||
val creation_t = ingame.world.creationTime
|
||||
|
||||
|
||||
if (hasThumbnail) {
|
||||
PixmapIO2._writeTGA(gzout, IngameRenderer.fboRGBexport, true, true)
|
||||
IngameRenderer.fboRGBexport.dispose()
|
||||
|
||||
val thumbContent = EntryFile(tgaout.toByteArray64())
|
||||
val thumb = DiskEntry(-2, 0, creation_t, time_t, thumbContent)
|
||||
addFile(disk, thumb)
|
||||
}
|
||||
|
||||
WriteSavegame.saveProgress += 1f
|
||||
|
||||
// Write World //
|
||||
// record all player's last position
|
||||
playersList.forEach {
|
||||
ingame.world.playersLastStatus[it.uuid] = PlayerLastStatus(it, ingame.isMultiplayer)
|
||||
}
|
||||
val worldMeta = EntryFile(WriteWorld.encodeToByteArray64(ingame, time_t, actorsList, playersList))
|
||||
val world = DiskEntry(-1L, 0, creation_t, time_t, worldMeta)
|
||||
addFile(disk, world); skimmer.appendEntryOnly(world)
|
||||
|
||||
WriteSavegame.saveProgress += 1f
|
||||
|
||||
var chunksWrote = 1
|
||||
chunks.forEachIndexed { layerNum, chunks ->
|
||||
|
||||
if (chunks.size != 0) {
|
||||
ingame.world.getLayer(layerNum)?.let { layer ->
|
||||
chunks.forEach { chunkNumber ->
|
||||
|
||||
// Echo("Writing chunks... $chunksWrote/$chunkCount")
|
||||
|
||||
val chunkXY = LandUtil.chunkNumToChunkXY(ingame.world, chunkNumber)
|
||||
|
||||
// println("Chunk xy from number $chunkNumber -> (${chunkXY.x}, ${chunkXY.y})")
|
||||
|
||||
val chunkBytes = WriteWorld.encodeChunk(layer, chunkXY.x, chunkXY.y)
|
||||
val entryID = 0x1_0000_0000L or layerNum.toLong().shl(24) or chunkNumber.toLong()
|
||||
|
||||
val entryContent = EntryFile(chunkBytes)
|
||||
val entry = DiskEntry(entryID, 0, creation_t, time_t, entryContent)
|
||||
// "W1L0-92,15"
|
||||
addFile(disk, entry); skimmer.appendEntryOnly(entry)
|
||||
|
||||
WriteSavegame.saveProgress += chunkProgressMultiplier
|
||||
chunksWrote += 1
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Write Actors //
|
||||
actorsList.forEachIndexed { count, it ->
|
||||
printdbg(this, "Writing actors... ${count+1}/${actorsList.size}")
|
||||
|
||||
val actorContent = EntryFile(WriteActor.encodeToByteArray64(it))
|
||||
val actor = DiskEntry(it.referenceID.toLong(), 0, creation_t, time_t, actorContent)
|
||||
addFile(disk, actor); skimmer.appendEntryOnly(actor)
|
||||
|
||||
WriteSavegame.saveProgress += actorProgressMultiplier
|
||||
}
|
||||
|
||||
|
||||
skimmer.rewriteDirectories()
|
||||
skimmer.injectDiskCRC(disk.hashCode())
|
||||
skimmer.setSaveMode(1 + 2 * isAuto.toInt())
|
||||
|
||||
printdbg(this, "Game saved with size of ${outFile.length()} bytes")
|
||||
|
||||
|
||||
if (hasThumbnail) IngameRenderer.fboRGBexportedLatch = false
|
||||
WriteSavegame.savingStatus = 255
|
||||
ingame.clearModifiedChunks()
|
||||
|
||||
callback()
|
||||
}
|
||||
|
||||
}
|
||||
186
src/net/torvald/terrarum/modulebasegame/serialise/WriteActor.kt
Normal file
186
src/net/torvald/terrarum/modulebasegame/serialise/WriteActor.kt
Normal file
@@ -0,0 +1,186 @@
|
||||
package net.torvald.terrarum.modulebasegame.serialise
|
||||
|
||||
import net.torvald.spriteanimation.AssembledSpriteAnimation
|
||||
import net.torvald.spriteanimation.HasAssembledSprite
|
||||
import net.torvald.terrarum.ItemCodex
|
||||
import net.torvald.terrarum.ModMgr
|
||||
import net.torvald.terrarum.ReferencingRanges.PREFIX_DYNAMICITEM
|
||||
import net.torvald.terrarum.gameactors.Actor
|
||||
import net.torvald.terrarum.gameactors.ActorWithBody
|
||||
import net.torvald.terrarum.modulebasegame.TerrarumIngame
|
||||
import net.torvald.terrarum.modulebasegame.gameactors.IngamePlayer
|
||||
import net.torvald.terrarum.savegame.*
|
||||
import net.torvald.terrarum.serialise.Common
|
||||
import net.torvald.terrarum.spriteassembler.ADProperties
|
||||
import java.io.Reader
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Created by minjaesong on 2021-08-24.
|
||||
*/
|
||||
object WriteActor {
|
||||
|
||||
// genver must be found on fixed location of the JSON string
|
||||
operator fun invoke(actor: Actor): String {
|
||||
val s = Common.jsoner.toJson(actor, actor.javaClass)
|
||||
return """{"genver":${Common.GENVER},"class":"${actor.javaClass.canonicalName}",${s.substring(1)}"""
|
||||
}
|
||||
|
||||
fun encodeToByteArray64(actor: Actor): ByteArray64 {
|
||||
val baw = ByteArray64Writer(Common.CHARSET)
|
||||
|
||||
val header = """{"genver":${Common.GENVER},"class":"${actor.javaClass.canonicalName}""""
|
||||
baw.write(header)
|
||||
Common.jsoner.toJson(actor, actor.javaClass, baw)
|
||||
baw.flush(); baw.close()
|
||||
// by this moment, contents of the baw will be:
|
||||
// {"class":"some.class.Name"{"actorValue":{},......}
|
||||
// (note that first bracket is not closed, and another open bracket after "class" property)
|
||||
// and we want to turn it into this:
|
||||
// {"class":"some.class.Name","actorValue":{},......}
|
||||
val ba = baw.toByteArray64()
|
||||
ba[header.toByteArray(Common.CHARSET).size.toLong()] = ','.code.toByte()
|
||||
|
||||
return ba
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Player-specific [WriteActor]. Will write JSON and Animation Description Languages
|
||||
*
|
||||
* Created by minjaesong on 2021-10-07.
|
||||
*/
|
||||
object WritePlayer {
|
||||
|
||||
/**
|
||||
* Will happily overwrite existing entry
|
||||
*/
|
||||
private fun addFile(disk: VirtualDisk, file: DiskEntry) {
|
||||
disk.entries[file.entryID] = file
|
||||
file.parentEntryID = 0
|
||||
val dir = VDUtil.getAsDirectory(disk, 0)
|
||||
if (!dir.contains(file.entryID)) dir.add(file.entryID)
|
||||
}
|
||||
|
||||
operator fun invoke(player: IngamePlayer, playerDisk: VirtualDisk, ingame: TerrarumIngame?, time_t: Long) {
|
||||
player.lastPlayTime = time_t
|
||||
player.totalPlayTime += time_t - (ingame?.loadedTime_t ?: time_t)
|
||||
|
||||
|
||||
// restore player prop backup created on load-time for multiplayer
|
||||
if (ingame?.isMultiplayer == true) {
|
||||
player.setPosition(player.unauthorisedPlayerProps.physics.position)
|
||||
player.actorValue = player.unauthorisedPlayerProps.actorValue!!
|
||||
player.inventory = player.unauthorisedPlayerProps.inventory!!
|
||||
}
|
||||
|
||||
player.worldCurrentlyPlaying = ingame?.world?.worldIndex ?: UUID(0L,0L)
|
||||
|
||||
// Write subset of Ingame.ItemCodex
|
||||
// The existing ItemCodex must be rewritten to clear out obsolete records
|
||||
player.dynamicToStaticTable.clear()
|
||||
player.dynamicItemInventory.clear()
|
||||
player.inventory.forEach { (itemid, _) ->
|
||||
if (itemid.startsWith("$PREFIX_DYNAMICITEM:")) {
|
||||
player.dynamicToStaticTable[itemid] = ItemCodex.dynamicToStaticID(itemid)
|
||||
player.dynamicItemInventory[itemid] = ItemCodex[itemid]!!
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val actorJson = WriteActor.encodeToByteArray64(player)
|
||||
|
||||
val adl = player.animDesc!!.getRawADL()
|
||||
val adlGlow = player.animDescGlow?.getRawADL() // NULLABLE!
|
||||
|
||||
val jsonContents = EntryFile(actorJson)
|
||||
val jsonCreationDate = playerDisk.getEntry(-1)?.creationDate ?: time_t
|
||||
addFile(playerDisk, DiskEntry(-1L, 0L, jsonCreationDate, time_t, jsonContents))
|
||||
|
||||
val adlContents = EntryFile(ByteArray64.fromByteArray(adl.toByteArray(Common.CHARSET)))
|
||||
val adlCreationDate = playerDisk.getEntry(-2)?.creationDate ?: time_t
|
||||
addFile(playerDisk, DiskEntry(-2L, 0L, adlCreationDate, time_t, adlContents))
|
||||
|
||||
if (adlGlow != null) {
|
||||
val adlGlowContents = EntryFile(ByteArray64.fromByteArray(adlGlow.toByteArray(Common.CHARSET)))
|
||||
val adlGlowCreationDate = playerDisk.getEntry(-3)?.creationDate ?: time_t
|
||||
addFile(playerDisk, DiskEntry(-3L, 0L, adlGlowCreationDate, time_t, adlGlowContents))
|
||||
}
|
||||
|
||||
// write loadorder //
|
||||
val loadOrderBa64Writer = ByteArray64Writer(Common.CHARSET)
|
||||
loadOrderBa64Writer.write(ModMgr.loadOrder.joinToString("\n"))
|
||||
loadOrderBa64Writer.flush(); loadOrderBa64Writer.close()
|
||||
val loadOrderText = loadOrderBa64Writer.toByteArray64()
|
||||
val loadOrderContents = EntryFile(loadOrderText)
|
||||
addFile(playerDisk, DiskEntry(-4L, 0L, jsonCreationDate, time_t, loadOrderContents))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Player-specific [ReadActor].
|
||||
*
|
||||
* @param disk disk
|
||||
* @param dataStream Reader containing JSON file
|
||||
*
|
||||
* Created by minjaesong on 2021-10-07.
|
||||
*/
|
||||
object ReadPlayer {
|
||||
|
||||
operator fun invoke(disk: SimpleFileSystem, dataStream: Reader): IngamePlayer =
|
||||
ReadActor(disk, dataStream) as IngamePlayer
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Actor's JSON representation is expected to have "class" property on the root object, such as:
|
||||
* ```
|
||||
* "class":"net.torvald.terrarum.modulebasegame.gameactors.IngamePlayer"
|
||||
* ```
|
||||
*
|
||||
* Created by minjaesong on 2021-08-27.
|
||||
*/
|
||||
object ReadActor {
|
||||
|
||||
operator fun invoke(disk: SimpleFileSystem, dataStream: Reader): Actor =
|
||||
fillInDetails(disk, Common.jsoner.fromJson(null, dataStream))
|
||||
|
||||
private fun fillInDetails(disk: SimpleFileSystem, actor: Actor): Actor {
|
||||
actor.reload()
|
||||
|
||||
|
||||
if (actor is ActorWithBody && actor is IngamePlayer) {
|
||||
val animFile = disk.getFile(-2L)
|
||||
val animFileGlow = disk.getFile(-3L)
|
||||
val bodypartsFile = disk.getFile(-1025)
|
||||
|
||||
actor.animDesc = ADProperties(ByteArray64Reader(animFile!!.bytes, Common.CHARSET))
|
||||
actor.sprite = AssembledSpriteAnimation(actor.animDesc!!, actor, if (bodypartsFile != null) disk else null, if (bodypartsFile != null) -1025 else null)
|
||||
if (animFileGlow != null) {
|
||||
actor.animDescGlow = ADProperties(ByteArray64Reader(animFileGlow.bytes, Common.CHARSET))
|
||||
actor.spriteGlow = AssembledSpriteAnimation(actor.animDescGlow!!, actor, if (bodypartsFile != null) disk else null, if (bodypartsFile != null) -1025 else null)
|
||||
}
|
||||
|
||||
ItemCodex.loadFromSave(disk.getBackingFile(), actor.dynamicToStaticTable, actor.dynamicItemInventory)
|
||||
|
||||
// val heldItem = ItemCodex[actor.inventory.itemEquipped[GameItem.EquipPosition.HAND_GRIP]]
|
||||
|
||||
/*if (bodypartsFile != null)
|
||||
actor.reassembleSpriteFromDisk(disk, actor.sprite!!, actor.spriteGlow, heldItem)
|
||||
else
|
||||
actor.reassembleSprite(actor.sprite!!, actor.spriteGlow, heldItem)*/
|
||||
}
|
||||
else if (actor is ActorWithBody && actor is HasAssembledSprite) {
|
||||
if (actor.animDesc != null) actor.sprite = AssembledSpriteAnimation(actor.animDesc!!, actor)
|
||||
if (actor.animDescGlow != null) actor.spriteGlow = AssembledSpriteAnimation(actor.animDescGlow!!, actor)
|
||||
|
||||
//actor.reassembleSprite(actor.sprite, actor.spriteGlow, null)
|
||||
}
|
||||
|
||||
|
||||
return actor
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package net.torvald.terrarum.modulebasegame.serialise
|
||||
|
||||
import com.badlogic.gdx.graphics.Pixmap
|
||||
import net.torvald.terrarum.*
|
||||
import net.torvald.terrarum.App.printdbg
|
||||
import net.torvald.terrarum.console.Echo
|
||||
import net.torvald.terrarum.gameactors.AVKey
|
||||
import net.torvald.terrarum.gameworld.BlockLayer
|
||||
import net.torvald.terrarum.gameworld.GameWorld
|
||||
import net.torvald.terrarum.langpack.Lang
|
||||
import net.torvald.terrarum.modulebasegame.ChunkLoadingLoadScreen
|
||||
import net.torvald.terrarum.modulebasegame.IngameRenderer
|
||||
import net.torvald.terrarum.modulebasegame.TerrarumIngame
|
||||
import net.torvald.terrarum.modulebasegame.gameactors.IngamePlayer
|
||||
import net.torvald.terrarum.realestate.LandUtil
|
||||
import net.torvald.terrarum.savegame.*
|
||||
import net.torvald.terrarum.serialise.Common
|
||||
import net.torvald.terrarum.worlddrawer.WorldCamera
|
||||
import java.io.File
|
||||
import java.io.Reader
|
||||
import java.util.logging.Level
|
||||
|
||||
/**
|
||||
* It's your responsibility to create a new VirtualDisk if your save is new, and create a backup for modifying existing save.
|
||||
*
|
||||
* Created by minjaesong on 2021-09-03.
|
||||
*/
|
||||
object WriteSavegame {
|
||||
|
||||
enum class SaveMode {
|
||||
META, PLAYER, WORLD, SHARED, QUICK_WORLD
|
||||
}
|
||||
|
||||
@Volatile var savingStatus = -1 // -1: not started, 0: saving in progress, 255: saving finished
|
||||
@Volatile var saveProgress = 0f
|
||||
@Volatile var saveProgressMax = 1f
|
||||
|
||||
private fun getSaveThread(time_t: Long, mode: SaveMode, disk: VirtualDisk, outFile: File, ingame: TerrarumIngame, hasThumbnail: Boolean, isAuto: Boolean, errorHandler: (Throwable) -> Unit, callback: () -> Unit) = when (mode) {
|
||||
SaveMode.WORLD -> WorldSavingThread(time_t, disk, outFile, ingame, hasThumbnail, isAuto, callback, errorHandler)
|
||||
SaveMode.PLAYER -> PlayerSavingThread(time_t, disk, outFile, ingame, hasThumbnail, isAuto, callback, errorHandler)
|
||||
SaveMode.QUICK_WORLD -> QuickSingleplayerWorldSavingThread(time_t, disk, outFile, ingame, hasThumbnail, isAuto, callback, errorHandler)
|
||||
else -> throw IllegalArgumentException("$mode")
|
||||
}
|
||||
|
||||
operator fun invoke(time_t: Long, mode: SaveMode, disk: VirtualDisk, outFile: File, ingame: TerrarumIngame, isAuto: Boolean, errorHandler: (Throwable) -> Unit, callback: () -> Unit) {
|
||||
savingStatus = 0
|
||||
val hasThumbnail = (mode == SaveMode.WORLD || mode == SaveMode.QUICK_WORLD)
|
||||
printdbg(this, "Save queued")
|
||||
|
||||
if (hasThumbnail) {
|
||||
IngameRenderer.screencapExportCallback = { fb ->
|
||||
printdbg(this, "Generating thumbnail...")
|
||||
|
||||
val w = 960
|
||||
val h = 640
|
||||
|
||||
val cx = /*1-*/(WorldCamera.x % 2)
|
||||
val cy = /*1-*/(WorldCamera.y % 2)
|
||||
|
||||
val x = (fb.width - w) / 2 - cx // force the even-numbered position
|
||||
val y = (fb.height - h) / 2 - cy // force the even-numbered position
|
||||
|
||||
val p = Pixmap.createFromFrameBuffer(x, y, w, h)
|
||||
IngameRenderer.fboRGBexport = p
|
||||
//PixmapIO2._writeTGA(gzout, p, true, true)
|
||||
//p.dispose()
|
||||
IngameRenderer.fboRGBexportedLatch = true
|
||||
|
||||
printdbg(this, "Done thumbnail generation")
|
||||
}
|
||||
IngameRenderer.screencapRequested = true
|
||||
}
|
||||
|
||||
val savingThread = Thread(getSaveThread(time_t, mode, disk, outFile, ingame, hasThumbnail, isAuto, errorHandler, callback), "TerrarumBasegameGameSaveThread")
|
||||
savingThread.start()
|
||||
|
||||
// it is caller's job to keep the game paused or keep a "save in progress" ui up
|
||||
// use callback to fire the after-the-saving-progress job
|
||||
}
|
||||
|
||||
|
||||
fun immediate(time_t: Long, mode: SaveMode, disk: VirtualDisk, outFile: File, ingame: TerrarumIngame, isAuto: Boolean, errorHandler: (Throwable) -> Unit, callback: () -> Unit) {
|
||||
|
||||
savingStatus = 0
|
||||
|
||||
printdbg(this, "Immediate save fired")
|
||||
|
||||
val savingThread = Thread(getSaveThread(time_t, mode, disk, outFile, ingame, false, isAuto, errorHandler, callback), "TerrarumBasegameGameSaveThread")
|
||||
savingThread.start()
|
||||
|
||||
// it is caller's job to keep the game paused or keep a "save in progress" ui up
|
||||
// use callback to fire the after-the-saving-progress job
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Load and setup the game for the first load.
|
||||
*
|
||||
* To load additional actors/worlds, use ReadActor/ReadWorld.
|
||||
*
|
||||
* Created by minjaesong on 2021-09-03.
|
||||
*/
|
||||
object LoadSavegame {
|
||||
|
||||
fun getSavegameNickname(worldDisk: SimpleFileSystem) = worldDisk.getDiskName(Common.CHARSET)
|
||||
fun getWorldSavefileName(nick: String, world: GameWorld) = "$nick-${world.worldIndex}"
|
||||
fun getPlayerSavefileName(player: IngamePlayer) = (player.actorValue.getAsString(AVKey.NAME) ?: "Player") + "-${player.uuid}"
|
||||
|
||||
fun getFileBytes(disk: SimpleFileSystem, id: Long): ByteArray64 = disk.getFile(id)!!.bytes
|
||||
fun getFileReader(disk: SimpleFileSystem, id: Long): Reader = ByteArray64Reader(getFileBytes(disk, id), Common.CHARSET)
|
||||
|
||||
/**
|
||||
* @param playerDisk DiskSkimmer representing the Player.
|
||||
* @param worldDisk0 DiskSkimmer representing the World to be loaded.
|
||||
* If unset, last played world for the Player will be loaded.
|
||||
*/
|
||||
operator fun invoke(playerDisk: DiskSkimmer, worldDisk0: DiskSkimmer? = null) {
|
||||
val newIngame = TerrarumIngame(App.batch)
|
||||
val player = ReadActor.invoke(playerDisk, ByteArray64Reader(playerDisk.getFile(-1L)!!.bytes, Common.CHARSET)) as IngamePlayer
|
||||
|
||||
printdbg(this, "Player localhash: ${player.localHashStr}, hasSprite: ${player.sprite != null}")
|
||||
|
||||
val currentWorldId = player.worldCurrentlyPlaying
|
||||
val worldDisk = worldDisk0 ?: App.savegameWorlds[currentWorldId]!!
|
||||
val world = ReadWorld(ByteArray64Reader(worldDisk.getFile(-1L)!!.bytes, Common.CHARSET), worldDisk.diskFile)
|
||||
|
||||
world.layerTerrain = BlockLayer(world.width, world.height)
|
||||
world.layerWall = BlockLayer(world.width, world.height)
|
||||
|
||||
newIngame.world = world // must be set before the loadscreen, otherwise the loadscreen will try to read from the NullWorld which is already destroyed
|
||||
newIngame.worldDisk = VDUtil.readDiskArchive(worldDisk.diskFile, Level.INFO)
|
||||
newIngame.playerDisk = VDUtil.readDiskArchive(playerDisk.diskFile, Level.INFO)
|
||||
newIngame.savegameNickname = getSavegameNickname(worldDisk)
|
||||
newIngame.worldSavefileName = getWorldSavefileName(newIngame.savegameNickname, world)
|
||||
newIngame.playerSavefileName = getPlayerSavefileName(player)
|
||||
|
||||
// worldDisk.dispose()
|
||||
// playerDisk.dispose()
|
||||
|
||||
val loadJob = { it: LoadScreenBase ->
|
||||
val loadscreen = it as ChunkLoadingLoadScreen
|
||||
loadscreen.addMessage(Lang["MENU_IO_LOADING"])
|
||||
|
||||
|
||||
val actors = world.actors.distinct()
|
||||
val worldParam = TerrarumIngame.Codices(newIngame.worldDisk, world, actors, player)
|
||||
|
||||
|
||||
newIngame.gameLoadInfoPayload = worldParam
|
||||
newIngame.gameLoadMode = TerrarumIngame.GameLoadMode.LOAD_FROM
|
||||
|
||||
|
||||
// load all the world blocklayer chunks
|
||||
val cw = LandUtil.CHUNK_W
|
||||
val ch = LandUtil.CHUNK_H
|
||||
val chunkCount = world.width * world.height / (cw * ch)
|
||||
val worldLayer = arrayOf(world.getLayer(0), world.getLayer(1))
|
||||
for (chunk in 0L until (world.width * world.height) / (cw * ch)) {
|
||||
for (layer in worldLayer.indices) {
|
||||
loadscreen.addMessage("${Lang["MENU_IO_LOADING"]} ${chunk*worldLayer.size+layer+1}/${chunkCount*2}")
|
||||
|
||||
val chunkFile = newIngame.worldDisk.getFile(0x1_0000_0000L or layer.toLong().shl(24) or chunk)!!
|
||||
val chunkXY = LandUtil.chunkNumToChunkXY(world, chunk.toInt())
|
||||
|
||||
ReadWorld.decodeChunkToLayer(chunkFile.getContent(), worldLayer[layer]!!, chunkXY.x, chunkXY.y)
|
||||
}
|
||||
}
|
||||
|
||||
loadscreen.addMessage("Updating Block Mappings...")
|
||||
world.renumberTilesAfterLoad()
|
||||
|
||||
|
||||
Echo("${ccW}World loaded: $ccY${newIngame.worldDisk.getDiskName(Common.CHARSET)}")
|
||||
printdbg(this, "World loaded: ${newIngame.worldDisk.getDiskName(Common.CHARSET)}")
|
||||
}
|
||||
|
||||
val loadScreen = ChunkLoadingLoadScreen(newIngame, world.width, world.height, loadJob)
|
||||
Terrarum.setCurrentIngameInstance(newIngame)
|
||||
App.setLoadScreen(loadScreen)
|
||||
}
|
||||
|
||||
}
|
||||
138
src/net/torvald/terrarum/modulebasegame/serialise/WriteWorld.kt
Normal file
138
src/net/torvald/terrarum/modulebasegame/serialise/WriteWorld.kt
Normal file
@@ -0,0 +1,138 @@
|
||||
package net.torvald.terrarum.modulebasegame.serialise
|
||||
|
||||
import net.torvald.terrarum.ItemCodex
|
||||
import net.torvald.terrarum.gameactors.Actor
|
||||
import net.torvald.terrarum.gameactors.NoSerialise
|
||||
import net.torvald.terrarum.gameworld.BlockLayer
|
||||
import net.torvald.terrarum.gameworld.GameWorld
|
||||
import net.torvald.terrarum.modulebasegame.TerrarumIngame
|
||||
import net.torvald.terrarum.modulebasegame.gameactors.IngamePlayer
|
||||
import net.torvald.terrarum.modulebasegame.worldgenerator.RoguelikeRandomiser
|
||||
import net.torvald.terrarum.realestate.LandUtil
|
||||
import net.torvald.terrarum.savegame.ByteArray64
|
||||
import net.torvald.terrarum.savegame.ByteArray64Writer
|
||||
import net.torvald.terrarum.serialise.Common
|
||||
import net.torvald.terrarum.serialise.toUint
|
||||
import net.torvald.terrarum.utils.PlayerLastStatus
|
||||
import net.torvald.terrarum.weather.WeatherMixer
|
||||
import java.io.File
|
||||
import java.io.Reader
|
||||
|
||||
/**
|
||||
* Created by minjaesong on 2021-08-23.
|
||||
*/
|
||||
object WriteWorld {
|
||||
|
||||
fun actorAcceptable(actor: Actor): Boolean {
|
||||
return actor !is NoSerialise // IngamePlayers is also NoSerialised because they must not be saved with the world
|
||||
}
|
||||
|
||||
private fun preWrite(ingame: TerrarumIngame, time_t: Long, actorsList: List<Actor>, playersList: List<IngamePlayer>): GameWorld {
|
||||
val world = ingame.world
|
||||
val currentPlayTime_t = time_t - ingame.loadedTime_t
|
||||
|
||||
world.comp = Common.COMP_GZIP
|
||||
world.lastPlayTime = time_t
|
||||
world.totalPlayTime += currentPlayTime_t
|
||||
|
||||
world.actors.clear()
|
||||
world.actors.addAll(actorsList.map { it.referenceID }.sorted().distinct())
|
||||
|
||||
world.randSeeds[0] = RoguelikeRandomiser.RNG.state0
|
||||
world.randSeeds[1] = RoguelikeRandomiser.RNG.state1
|
||||
world.randSeeds[2] = WeatherMixer.RNG.state0
|
||||
world.randSeeds[3] = WeatherMixer.RNG.state1
|
||||
|
||||
// record all player's last position
|
||||
playersList.forEach {
|
||||
world.playersLastStatus.put(it.uuid.toString(), PlayerLastStatus(it, ingame.isMultiplayer))
|
||||
}
|
||||
|
||||
return world
|
||||
}
|
||||
|
||||
// genver must be found on fixed location of the JSON string
|
||||
operator fun invoke(ingame: TerrarumIngame, time_t: Long, actorsList: List<Actor>, playersList: List<IngamePlayer>): String {
|
||||
val s = Common.jsoner.toJson(preWrite(ingame, time_t, actorsList, playersList))
|
||||
return """{"genver":${Common.GENVER},${s.substring(1)}"""
|
||||
}
|
||||
|
||||
fun encodeToByteArray64(ingame: TerrarumIngame, time_t: Long, actorsList: List<Actor>, playersList: List<IngamePlayer>): ByteArray64 {
|
||||
val baw = ByteArray64Writer(Common.CHARSET)
|
||||
|
||||
val header = """{"genver":${Common.GENVER}"""
|
||||
baw.write(header)
|
||||
Common.jsoner.toJson(preWrite(ingame, time_t, actorsList, playersList), baw)
|
||||
baw.flush(); baw.close()
|
||||
// by this moment, contents of the baw will be:
|
||||
// {"genver":123456{"actorValue":{},......}
|
||||
// (note that first bracket is not closed, and another open bracket after "genver" property)
|
||||
// and we want to turn it into this:
|
||||
// {"genver":123456,"actorValue":{},......}
|
||||
val ba = baw.toByteArray64()
|
||||
ba[header.toByteArray(Common.CHARSET).size.toLong()] = ','.code.toByte()
|
||||
|
||||
return ba
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Gzipped chunk. Tile numbers are stored in Big Endian.
|
||||
*/
|
||||
fun encodeChunk(layer: BlockLayer, cx: Int, cy: Int): ByteArray64 {
|
||||
val ba = ByteArray64()
|
||||
for (y in cy * LandUtil.CHUNK_H until (cy + 1) * LandUtil.CHUNK_H) {
|
||||
for (x in cx * LandUtil.CHUNK_W until (cx + 1) * LandUtil.CHUNK_W) {
|
||||
val tilenum = layer.unsafeGetTile(x, y)
|
||||
ba.add(tilenum.ushr(8).and(255).toByte())
|
||||
ba.add(tilenum.and(255).toByte())
|
||||
}
|
||||
}
|
||||
|
||||
return Common.zip(ba)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Created by minjaesong on 2021-08-25.
|
||||
*/
|
||||
object ReadWorld {
|
||||
|
||||
operator fun invoke(worldDataStream: Reader, origin: File?): GameWorld =
|
||||
fillInDetails(Common.jsoner.fromJson(GameWorld::class.java, worldDataStream), origin)
|
||||
|
||||
private fun fillInDetails(world: GameWorld, origin: File?): GameWorld {
|
||||
world.tileNumberToNameMap.forEach { l, s ->
|
||||
world.tileNameToNumberMap[s] = l.toInt()
|
||||
}
|
||||
|
||||
ItemCodex.loadFromSave(origin, world.dynamicToStaticTable, world.dynamicItemInventory)
|
||||
|
||||
return world
|
||||
}
|
||||
|
||||
private val cw = LandUtil.CHUNK_W
|
||||
private val ch = LandUtil.CHUNK_H
|
||||
|
||||
fun decodeChunkToLayer(chunk: ByteArray64, targetLayer: BlockLayer, cx: Int, cy: Int) {
|
||||
val bytes = Common.unzip(chunk)
|
||||
if (bytes.size != cw * ch * 2L)
|
||||
throw UnsupportedOperationException("Chunk size mismatch: decoded chunk size is ${bytes.size} bytes " +
|
||||
"where ${LandUtil.CHUNK_W * LandUtil.CHUNK_H * 2L} bytes (Int16 of ${LandUtil.CHUNK_W}x${LandUtil.CHUNK_H}) were expected")
|
||||
|
||||
for (k in 0 until cw * ch) {
|
||||
val tilenum = bytes[2L*k].toUint().shl(8) or bytes[2L*k + 1].toUint()
|
||||
val offx = k % cw
|
||||
val offy = k / cw
|
||||
|
||||
// try {
|
||||
targetLayer.unsafeSetTile(cx * cw + offx, cy * ch + offy, tilenum)
|
||||
// }
|
||||
// catch (e: IndexOutOfBoundsException) {
|
||||
// printdbgerr(this, "IndexOutOfBoundsException, cx = $cx, cy = $cy, k = $k, offx = $offx, offy = $offy")
|
||||
// throw e
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user