From e8084d72833f3b33a2d60e32056ad15ffc2515bd Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Tue, 7 Feb 2023 09:01:43 -0500 Subject: [PATCH] Implement world reassembly from a Heimdall backup. --- .../heimdall/load/WorldLoadFormat.kt | 15 +++++ .../plugin/FoundationHeimdallPlugin.kt | 4 ++ .../plugin/load/ImportWorldLoadCommand.kt | 37 ++++++++++ .../heimdall/plugin/load/WorldReassembler.kt | 67 +++++++++++++++++++ .../src/main/resources/plugin.yml | 4 ++ .../heimdall/tool/GjallarhornCommand.kt | 1 + .../tool/commands/GenerateWorldLoadFile.kt | 50 ++++++++++++++ .../pizza/foundation/heimdall/tool/main.kt | 8 +-- .../heimdall/tool/state/BlockChange.kt | 2 + .../heimdall/tool/state/BlockChangelog.kt | 18 ++++- .../heimdall/tool/state/BlockLogTracker.kt | 9 +++ 11 files changed, 209 insertions(+), 6 deletions(-) create mode 100644 common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/load/WorldLoadFormat.kt create mode 100644 foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/load/ImportWorldLoadCommand.kt create mode 100644 foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/load/WorldReassembler.kt create mode 100644 tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/commands/GenerateWorldLoadFile.kt diff --git a/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/load/WorldLoadFormat.kt b/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/load/WorldLoadFormat.kt new file mode 100644 index 0000000..39ee76f --- /dev/null +++ b/common-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/load/WorldLoadFormat.kt @@ -0,0 +1,15 @@ +package gay.pizza.foundation.heimdall.load + +import gay.pizza.foundation.heimdall.export.ExportedBlock +import kotlinx.serialization.Serializable + +@Serializable +class WorldLoadFormat( + val worlds: Map +) + +@Serializable +class WorldLoadWorld( + val name: String, + val blocks: Map>> +) diff --git a/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/FoundationHeimdallPlugin.kt b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/FoundationHeimdallPlugin.kt index bdf7bb9..b43f6c8 100644 --- a/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/FoundationHeimdallPlugin.kt +++ b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/FoundationHeimdallPlugin.kt @@ -10,6 +10,7 @@ import gay.pizza.foundation.heimdall.plugin.buffer.BufferFlushThread import gay.pizza.foundation.heimdall.plugin.buffer.EventBuffer import gay.pizza.foundation.heimdall.plugin.event.* import gay.pizza.foundation.heimdall.plugin.export.ExportAllChunksCommand +import gay.pizza.foundation.heimdall.plugin.load.ImportWorldLoadCommand import gay.pizza.foundation.heimdall.plugin.model.HeimdallConfig import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer import org.bukkit.event.EventHandler @@ -45,6 +46,9 @@ class FoundationHeimdallPlugin : JavaPlugin(), Listener { val exportChunksCommand = getCommand("export_all_chunks") ?: throw Exception("Failed to get export_all_chunks command") exportChunksCommand.setExecutor(ExportAllChunksCommand(this)) + val importWorldLoadCommand = getCommand("import_world_load") ?: throw Exception("Failed to get import_world_load command") + importWorldLoadCommand.setExecutor(ImportWorldLoadCommand(this)) + val foundation = FoundationCoreLoader.get(server) val configPath = copyDefaultConfig( slF4JLogger, diff --git a/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/load/ImportWorldLoadCommand.kt b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/load/ImportWorldLoadCommand.kt new file mode 100644 index 0000000..89f5e02 --- /dev/null +++ b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/load/ImportWorldLoadCommand.kt @@ -0,0 +1,37 @@ +package gay.pizza.foundation.heimdall.plugin.load + +import gay.pizza.foundation.heimdall.load.WorldLoadFormat +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import org.bukkit.command.Command +import org.bukkit.command.CommandExecutor +import org.bukkit.command.CommandSender +import org.bukkit.plugin.Plugin +import java.nio.file.Paths +import kotlin.io.path.exists +import kotlin.io.path.inputStream + +class ImportWorldLoadCommand(private val plugin: Plugin) : CommandExecutor { + override fun onCommand( + sender: CommandSender, + command: Command, + label: String, + args: Array + ): Boolean { + if (args.size != 1) { + sender.sendMessage("Usage: import_world_load ") + return true + } + val pathString = args[0] + val path = Paths.get(pathString) + if (!path.exists()) { + sender.sendMessage("Path '${path}' not found.") + } + val format = Json.decodeFromStream(WorldLoadFormat.serializer(), path.inputStream()) + val reassembler = WorldReassembler(plugin, sender.server, format) { message -> + sender.sendMessage(message) + } + reassembler.loadInBackground() + return true + } +} diff --git a/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/load/WorldReassembler.kt b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/load/WorldReassembler.kt new file mode 100644 index 0000000..65f3083 --- /dev/null +++ b/foundation-heimdall/src/main/kotlin/gay/pizza/foundation/heimdall/plugin/load/WorldReassembler.kt @@ -0,0 +1,67 @@ +package gay.pizza.foundation.heimdall.plugin.load + +import gay.pizza.foundation.heimdall.load.WorldLoadFormat +import gay.pizza.foundation.heimdall.load.WorldLoadWorld +import org.bukkit.Location +import org.bukkit.Material +import org.bukkit.Server +import org.bukkit.plugin.Plugin +import org.bukkit.scheduler.BukkitRunnable +import java.util.concurrent.atomic.AtomicLong + +class WorldReassembler(val plugin: Plugin, val server: Server, val format: WorldLoadFormat, val feedback: (String) -> Unit) { + fun loadInBackground() { + server.scheduler.runTaskAsynchronously(plugin) { -> + for (world in server.worlds) { + val id = world.uid + var load: WorldLoadWorld? = format.worlds[id.toString().lowercase()] + if (load == null) { + load = format.worlds.values.firstOrNull { it.name == world.name } + } + + if (load == null) { + feedback("Unable to match world ${world.uid} (${world.name}) to a loadable world, skipping") + continue + } + + val blocksToMake = mutableListOf>() + + for ((x, zBlocks) in load.blocks) { + for ((z, yBlocks) in zBlocks) { + for ((y, block) in yBlocks) { + val material: Material? = Material.matchMaterial(block.type) + + if (material == null) { + feedback("Unknown Material '${block.type}' at $x $y $z") + continue + } + + blocksToMake.add(Location(world, x.toDouble(), y.toDouble(), z.toDouble()) to material) + } + } + } + blocksToMake.sortBy { it.first.x } + + feedback("Will place ${blocksToMake.size} blocks in ${world.name}") + + val count = AtomicLong() + var ticks = 0L + blocksToMake.chunked(1000) { section -> + val copy = section.toList() + val runnable = object : BukkitRunnable() { + override fun run() { + for ((location, material) in copy) { + val block = world.getBlockAt(location) + block.type = material + count.incrementAndGet() + } + feedback("Placed ${count.get()} blocks in ${world.name}") + } + } + runnable.runTaskLater(plugin, ticks) + ticks += 10 + } + } + } + } +} diff --git a/foundation-heimdall/src/main/resources/plugin.yml b/foundation-heimdall/src/main/resources/plugin.yml index 9af5b0a..70f4079 100644 --- a/foundation-heimdall/src/main/resources/plugin.yml +++ b/foundation-heimdall/src/main/resources/plugin.yml @@ -13,3 +13,7 @@ commands: description: Export All Chunks usage: /export_all_chunks permission: heimdall.command.export_all_chunks + import_world_load: + description: Import World Load + usage: /import_world_load + permission: heimdall.command.import_world_load diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/GjallarhornCommand.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/GjallarhornCommand.kt index 37d1f6f..1ee1c38 100644 --- a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/GjallarhornCommand.kt +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/GjallarhornCommand.kt @@ -30,6 +30,7 @@ class GjallarhornCommand : CliktCommand(invokeWithoutSubcommand = true) { maximumPoolSize = dbPoolSize idleTimeout = Duration.ofMinutes(5).toMillis() maxLifetime = Duration.ofMinutes(10).toMillis() + schema = "heimdall" }) val db = Database.connect(pool) currentContext.findOrSetObject { db } diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/commands/GenerateWorldLoadFile.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/commands/GenerateWorldLoadFile.kt new file mode 100644 index 0000000..b8c9e6b --- /dev/null +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/commands/GenerateWorldLoadFile.kt @@ -0,0 +1,50 @@ +package gay.pizza.foundation.heimdall.tool.commands + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.requireObject +import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.types.path +import gay.pizza.foundation.heimdall.export.ExportedBlock +import gay.pizza.foundation.heimdall.load.WorldLoadFormat +import gay.pizza.foundation.heimdall.load.WorldLoadWorld +import gay.pizza.foundation.heimdall.table.WorldChangeTable +import gay.pizza.foundation.heimdall.tool.state.BlockChangelog +import gay.pizza.foundation.heimdall.tool.state.BlockLogTracker +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.encodeToStream +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.transactions.transaction +import org.slf4j.LoggerFactory +import kotlin.io.path.createFile +import kotlin.io.path.deleteExisting +import kotlin.io.path.deleteIfExists +import kotlin.io.path.outputStream + +class GenerateWorldLoadFile : CliktCommand(name = "generate-world-load", help = "Generate World Load File") { + private val db by requireObject() + + val path by argument("load-format-file").path() + + override fun run() { + val worlds = mutableMapOf() + val worldChangelogs = BlockChangelog.query(db).splitBy { it.world } + val worldNames = transaction(db) { + WorldChangeTable.selectAll() + .associate { it[WorldChangeTable.toWorld] to it[WorldChangeTable.toWorldName] } + } + for ((id, changelog) in worldChangelogs) { + val tracker = BlockLogTracker() + tracker.replay(changelog) + val sparse = tracker.buildBlockMap { ExportedBlock(it.type) } + val blocks = sparse.blocks + worlds[id.toString().lowercase()] = WorldLoadWorld( + worldNames[id] ?: "unknown_$id", + blocks + ) + } + val format = WorldLoadFormat(worlds) + path.deleteIfExists() + Json.encodeToStream(format, path.outputStream()) + } +} diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/main.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/main.kt index 7e33350..f9539d0 100644 --- a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/main.kt +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/main.kt @@ -1,14 +1,12 @@ package gay.pizza.foundation.heimdall.tool -import gay.pizza.foundation.heimdall.tool.commands.BlockChangeTimelapseCommand -import gay.pizza.foundation.heimdall.tool.commands.ChunkExportLoaderCommand -import gay.pizza.foundation.heimdall.tool.commands.PlayerPositionExport -import gay.pizza.foundation.heimdall.tool.commands.PlayerSessionExport import com.github.ajalt.clikt.core.subcommands +import gay.pizza.foundation.heimdall.tool.commands.* fun main(args: Array) = GjallarhornCommand().subcommands( BlockChangeTimelapseCommand(), PlayerSessionExport(), PlayerPositionExport(), - ChunkExportLoaderCommand() + ChunkExportLoaderCommand(), + GenerateWorldLoadFile() ).main(args) diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockChange.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockChange.kt index 413ce60..c6ed172 100644 --- a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockChange.kt +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockChange.kt @@ -1,9 +1,11 @@ package gay.pizza.foundation.heimdall.tool.state import java.time.Instant +import java.util.UUID data class BlockChange( val time: Instant, + val world: UUID, val type: BlockChangeType, val location: BlockCoordinate, val from: BlockState, diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockChangelog.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockChangelog.kt index d97c9ac..aa62050 100644 --- a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockChangelog.kt +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockChangelog.kt @@ -63,10 +63,11 @@ class BlockChangelog( BlockChangelog(BlockChangeView.select(filter).orderBy(BlockChangeView.time).map { row -> val time = row[BlockChangeView.time] val changeIsBreak = row[BlockChangeView.isBreak] + val world = row[BlockChangeView.world] val x = row[BlockChangeView.x] val y = row[BlockChangeView.y] val z = row[BlockChangeView.z] - val block = row[gay.pizza.foundation.heimdall.view.BlockChangeView.block] + val block = row[BlockChangeView.block] val location = BlockCoordinate(x.toLong(), y.toLong(), z.toLong()) val fromBlock = if (changeIsBreak) { @@ -83,6 +84,7 @@ class BlockChangelog( BlockChange( time, + world, if (changeIsBreak) BlockChangeType.Break else BlockChangeType.Place, location, fromBlock, @@ -91,4 +93,18 @@ class BlockChangelog( }) } } + + fun splitBy(key: (BlockChange) -> T): Map { + val logs = mutableMapOf>() + for (change in changes) { + val k = key(change) + var log = logs[k] + if (log == null) { + log = mutableListOf() + logs[k] = log + } + log.add(change) + } + return logs.mapValues { BlockChangelog(it.value) } + } } diff --git a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockLogTracker.kt b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockLogTracker.kt index 4b1b2cf..3454fe7 100644 --- a/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockLogTracker.kt +++ b/tool-gjallarhorn/src/main/kotlin/gay/pizza/foundation/heimdall/tool/state/BlockLogTracker.kt @@ -50,6 +50,15 @@ class BlockLogTracker(private val mode: BlockTrackMode = BlockTrackMode.RemoveOn return map } + fun buildBlockMap(offset: BlockCoordinate = BlockCoordinate.zero, value: (BlockState) -> T): BlockCoordinateSparseMap { + val map = BlockCoordinateSparseMap() + blocks.forEach { (position, state) -> + val realPosition = offset.applyAsOffset(position) + map.put(realPosition, value(state)) + } + return map + } + fun replay(changelog: BlockChangelog) = changelog.changes.forEach { change -> if (change.type == BlockChangeType.Break) { delete(change.location)