mirror of
https://github.com/GayPizzaSpecifications/foundation.git
synced 2025-08-03 05:30:55 +00:00
Implement world reassembly from a Heimdall backup.
This commit is contained in:
@ -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<String, WorldLoadWorld>
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class WorldLoadWorld(
|
||||||
|
val name: String,
|
||||||
|
val blocks: Map<Long, Map<Long, Map<Long, ExportedBlock>>>
|
||||||
|
)
|
@ -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.buffer.EventBuffer
|
||||||
import gay.pizza.foundation.heimdall.plugin.event.*
|
import gay.pizza.foundation.heimdall.plugin.event.*
|
||||||
import gay.pizza.foundation.heimdall.plugin.export.ExportAllChunksCommand
|
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 gay.pizza.foundation.heimdall.plugin.model.HeimdallConfig
|
||||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer
|
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer
|
||||||
import org.bukkit.event.EventHandler
|
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")
|
val exportChunksCommand = getCommand("export_all_chunks") ?: throw Exception("Failed to get export_all_chunks command")
|
||||||
exportChunksCommand.setExecutor(ExportAllChunksCommand(this))
|
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 foundation = FoundationCoreLoader.get(server)
|
||||||
val configPath = copyDefaultConfig<FoundationHeimdallPlugin>(
|
val configPath = copyDefaultConfig<FoundationHeimdallPlugin>(
|
||||||
slF4JLogger,
|
slF4JLogger,
|
||||||
|
@ -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<out String>
|
||||||
|
): Boolean {
|
||||||
|
if (args.size != 1) {
|
||||||
|
sender.sendMessage("Usage: import_world_load <path>")
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -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<Pair<Location, Material>>()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,3 +13,7 @@ commands:
|
|||||||
description: Export All Chunks
|
description: Export All Chunks
|
||||||
usage: /export_all_chunks
|
usage: /export_all_chunks
|
||||||
permission: heimdall.command.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
|
||||||
|
@ -30,6 +30,7 @@ class GjallarhornCommand : CliktCommand(invokeWithoutSubcommand = true) {
|
|||||||
maximumPoolSize = dbPoolSize
|
maximumPoolSize = dbPoolSize
|
||||||
idleTimeout = Duration.ofMinutes(5).toMillis()
|
idleTimeout = Duration.ofMinutes(5).toMillis()
|
||||||
maxLifetime = Duration.ofMinutes(10).toMillis()
|
maxLifetime = Duration.ofMinutes(10).toMillis()
|
||||||
|
schema = "heimdall"
|
||||||
})
|
})
|
||||||
val db = Database.connect(pool)
|
val db = Database.connect(pool)
|
||||||
currentContext.findOrSetObject { db }
|
currentContext.findOrSetObject { db }
|
||||||
|
@ -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<Database>()
|
||||||
|
|
||||||
|
val path by argument("load-format-file").path()
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
val worlds = mutableMapOf<String, WorldLoadWorld>()
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
@ -1,14 +1,12 @@
|
|||||||
package gay.pizza.foundation.heimdall.tool
|
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 com.github.ajalt.clikt.core.subcommands
|
||||||
|
import gay.pizza.foundation.heimdall.tool.commands.*
|
||||||
|
|
||||||
fun main(args: Array<String>) = GjallarhornCommand().subcommands(
|
fun main(args: Array<String>) = GjallarhornCommand().subcommands(
|
||||||
BlockChangeTimelapseCommand(),
|
BlockChangeTimelapseCommand(),
|
||||||
PlayerSessionExport(),
|
PlayerSessionExport(),
|
||||||
PlayerPositionExport(),
|
PlayerPositionExport(),
|
||||||
ChunkExportLoaderCommand()
|
ChunkExportLoaderCommand(),
|
||||||
|
GenerateWorldLoadFile()
|
||||||
).main(args)
|
).main(args)
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
package gay.pizza.foundation.heimdall.tool.state
|
package gay.pizza.foundation.heimdall.tool.state
|
||||||
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
data class BlockChange(
|
data class BlockChange(
|
||||||
val time: Instant,
|
val time: Instant,
|
||||||
|
val world: UUID,
|
||||||
val type: BlockChangeType,
|
val type: BlockChangeType,
|
||||||
val location: BlockCoordinate,
|
val location: BlockCoordinate,
|
||||||
val from: BlockState,
|
val from: BlockState,
|
||||||
|
@ -63,10 +63,11 @@ class BlockChangelog(
|
|||||||
BlockChangelog(BlockChangeView.select(filter).orderBy(BlockChangeView.time).map { row ->
|
BlockChangelog(BlockChangeView.select(filter).orderBy(BlockChangeView.time).map { row ->
|
||||||
val time = row[BlockChangeView.time]
|
val time = row[BlockChangeView.time]
|
||||||
val changeIsBreak = row[BlockChangeView.isBreak]
|
val changeIsBreak = row[BlockChangeView.isBreak]
|
||||||
|
val world = row[BlockChangeView.world]
|
||||||
val x = row[BlockChangeView.x]
|
val x = row[BlockChangeView.x]
|
||||||
val y = row[BlockChangeView.y]
|
val y = row[BlockChangeView.y]
|
||||||
val z = row[BlockChangeView.z]
|
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 location = BlockCoordinate(x.toLong(), y.toLong(), z.toLong())
|
||||||
|
|
||||||
val fromBlock = if (changeIsBreak) {
|
val fromBlock = if (changeIsBreak) {
|
||||||
@ -83,6 +84,7 @@ class BlockChangelog(
|
|||||||
|
|
||||||
BlockChange(
|
BlockChange(
|
||||||
time,
|
time,
|
||||||
|
world,
|
||||||
if (changeIsBreak) BlockChangeType.Break else BlockChangeType.Place,
|
if (changeIsBreak) BlockChangeType.Break else BlockChangeType.Place,
|
||||||
location,
|
location,
|
||||||
fromBlock,
|
fromBlock,
|
||||||
@ -91,4 +93,18 @@ class BlockChangelog(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <T> splitBy(key: (BlockChange) -> T): Map<T, BlockChangelog> {
|
||||||
|
val logs = mutableMapOf<T, MutableList<BlockChange>>()
|
||||||
|
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) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,6 +50,15 @@ class BlockLogTracker(private val mode: BlockTrackMode = BlockTrackMode.RemoveOn
|
|||||||
return map
|
return map
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <T> buildBlockMap(offset: BlockCoordinate = BlockCoordinate.zero, value: (BlockState) -> T): BlockCoordinateSparseMap<T> {
|
||||||
|
val map = BlockCoordinateSparseMap<T>()
|
||||||
|
blocks.forEach { (position, state) ->
|
||||||
|
val realPosition = offset.applyAsOffset(position)
|
||||||
|
map.put(realPosition, value(state))
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
fun replay(changelog: BlockChangelog) = changelog.changes.forEach { change ->
|
fun replay(changelog: BlockChangelog) = changelog.changes.forEach { change ->
|
||||||
if (change.type == BlockChangeType.Break) {
|
if (change.type == BlockChangeType.Break) {
|
||||||
delete(change.location)
|
delete(change.location)
|
||||||
|
Reference in New Issue
Block a user