Rewrite Heimdall block handling to support more event types and non-player block changes.

This commit is contained in:
Alex Zenla 2023-02-07 23:41:22 -05:00
parent 688106a6e6
commit e0823f7b15
Signed by: alex
GPG Key ID: C0780728420EBFE5
19 changed files with 228 additions and 218 deletions

View File

@ -1,6 +0,0 @@
package gay.pizza.foundation.heimdall.table
object BlockBreakTable : PlayerTimedLocalEventTable("block_breaks") {
val block = text("block")
val blockData = text("block_data").nullable()
}

View File

@ -0,0 +1,7 @@
package gay.pizza.foundation.heimdall.table
object BlockChangeTable : PlayerTimedLocalEventTable("block_changes") {
val block = text("block")
val data = text("data")
val cause = text("cause")
}

View File

@ -1,6 +0,0 @@
package gay.pizza.foundation.heimdall.table
object BlockPlaceTable : PlayerTimedLocalEventTable("block_places") {
val block = text("block")
val blockData = text("block_data").nullable()
}

View File

@ -1,7 +1,7 @@
package gay.pizza.foundation.heimdall.table
abstract class PlayerTimedLocalEventTable(name: String) : TimedLocalEventTable(name) {
val player = uuid("player")
val player = uuid("player").nullable()
val pitch = double("pitch")
val yaw = double("yaw")
}

View File

@ -1,9 +0,0 @@
package gay.pizza.foundation.heimdall.view
import gay.pizza.foundation.heimdall.table.PlayerTimedLocalEventTable
object BlockChangeView : PlayerTimedLocalEventTable("block_changes") {
val isBreak = bool("break")
val block = text("block")
val blockData = text("block_data").nullable()
}

View File

@ -1,47 +0,0 @@
package gay.pizza.foundation.heimdall.plugin.event
import gay.pizza.foundation.heimdall.plugin.buffer.EventBuffer
import gay.pizza.foundation.heimdall.plugin.buffer.IEventBuffer
import gay.pizza.foundation.heimdall.table.BlockBreakTable
import org.bukkit.Location
import org.bukkit.Material
import org.bukkit.event.EventHandler
import org.bukkit.event.block.BlockBreakEvent
import org.jetbrains.exposed.sql.Transaction
import org.jetbrains.exposed.sql.insert
import java.time.Instant
import java.util.*
class BlockBreak(
val playerUniqueIdentity: UUID,
val location: Location,
val material: Material,
val blockData: String? = null,
val timestamp: Instant = Instant.now()
) : HeimdallEvent() {
constructor(event: BlockBreakEvent) : this(
event.player.uniqueId,
event.block.location,
event.block.type,
event.block.blockData.asString
)
override fun store(transaction: Transaction) {
transaction.apply {
BlockBreakTable.insert {
putPlayerTimedLocalEvent(it, timestamp, location, playerUniqueIdentity)
it[block] = material.key.toString()
it[blockData] = this@BlockBreak.blockData
}
}
}
class Collector(val buffer: IEventBuffer) : EventCollector<BlockBreak> {
@EventHandler
fun onBlockBroken(event: BlockBreakEvent) = buffer.push(BlockBreak(event))
}
companion object : EventCollectorProvider<BlockBreak> {
override fun collector(buffer: EventBuffer): EventCollector<BlockBreak> = Collector(buffer)
}
}

View File

@ -0,0 +1,182 @@
package gay.pizza.foundation.heimdall.plugin.event
import gay.pizza.foundation.heimdall.plugin.buffer.EventBuffer
import gay.pizza.foundation.heimdall.plugin.buffer.IEventBuffer
import gay.pizza.foundation.heimdall.table.BlockChangeTable
import org.bukkit.Location
import org.bukkit.Material
import org.bukkit.block.Block
import org.bukkit.event.EventHandler
import org.bukkit.event.EventPriority
import org.bukkit.event.block.BlockBreakEvent
import org.bukkit.event.block.BlockBurnEvent
import org.bukkit.event.block.BlockDamageEvent
import org.bukkit.event.block.BlockDispenseEvent
import org.bukkit.event.block.BlockEvent
import org.bukkit.event.block.BlockExplodeEvent
import org.bukkit.event.block.BlockFadeEvent
import org.bukkit.event.block.BlockFormEvent
import org.bukkit.event.block.BlockGrowEvent
import org.bukkit.event.block.BlockIgniteEvent
import org.bukkit.event.block.BlockPlaceEvent
import org.bukkit.event.block.BlockSpreadEvent
import org.bukkit.event.block.FluidLevelChangeEvent
import org.jetbrains.exposed.sql.Transaction
import org.jetbrains.exposed.sql.insert
import java.time.Instant
import java.util.*
class BlockChange(
val playerUniqueIdentity: UUID? = null,
val cause: String = "place",
val location: Location,
val material: Material,
val blockData: String,
val timestamp: Instant = Instant.now()
) : HeimdallEvent() {
constructor(
playerUniqueIdentity: UUID? = null,
isBreak: Boolean = false,
cause: String,
event: BlockEvent,
block: Block = event.block
) : this(
playerUniqueIdentity = playerUniqueIdentity,
cause = cause,
location = block.location,
material = if (isBreak) Material.AIR else block.type,
blockData = if (isBreak) Material.AIR.createBlockData().asString
else block.blockData.asString
)
override fun store(transaction: Transaction) {
transaction.apply {
BlockChangeTable.insert {
putPlayerTimedLocalEvent(it, timestamp, location, playerUniqueIdentity)
it[block] = material.key.toString()
it[data] = this@BlockChange.blockData
it[cause] = this@BlockChange.cause
}
}
}
class Collector(val buffer: IEventBuffer) : EventCollector<BlockChange> {
@EventHandler(priority = EventPriority.MONITOR)
fun onBlockPlaced(event: BlockPlaceEvent) = buffer.push(
BlockChange(
playerUniqueIdentity = event.player.uniqueId,
event = event,
cause = "place",
isBreak = false
)
)
@EventHandler(priority = EventPriority.MONITOR)
fun onBlockBreak(event: BlockBreakEvent) = buffer.push(
BlockChange(
playerUniqueIdentity = event.player.uniqueId,
event = event,
cause = "break",
isBreak = true
)
)
@EventHandler(priority = EventPriority.MONITOR)
fun onBlockExplode(event: BlockExplodeEvent) = event.blockList().forEach { block ->
buffer.push(
BlockChange(
event = event,
cause = "explode",
isBreak = true,
block = block
)
)
}
@EventHandler(priority = EventPriority.MONITOR)
fun onBlockBurn(event: BlockBurnEvent) = buffer.push(
BlockChange(
playerUniqueIdentity = null,
event = event,
cause = "burn",
isBreak = true
)
)
@EventHandler(priority = EventPriority.MONITOR)
fun onBlockDamage(event: BlockDamageEvent) = buffer.push(
BlockChange(
playerUniqueIdentity = null,
event = event,
cause = "damage"
)
)
@EventHandler(priority = EventPriority.MONITOR)
fun onBlockForm(event: BlockFormEvent) = buffer.push(
BlockChange(
playerUniqueIdentity = null,
event = event,
cause = "form"
)
)
@EventHandler(priority = EventPriority.MONITOR)
fun onBlockGrow(event: BlockGrowEvent) = buffer.push(
BlockChange(
playerUniqueIdentity = null,
event = event,
cause = "grow"
)
)
@EventHandler(priority = EventPriority.MONITOR)
fun onBlockFade(event: BlockFadeEvent) = buffer.push(
BlockChange(
playerUniqueIdentity = null,
event = event,
cause = "fade"
)
)
@EventHandler(priority = EventPriority.MONITOR)
fun onBlockIgnite(event: BlockIgniteEvent) = buffer.push(
BlockChange(
playerUniqueIdentity = null,
event = event,
cause = "ignite"
)
)
@EventHandler(priority = EventPriority.MONITOR)
fun onBlockDispense(event: BlockDispenseEvent) = buffer.push(
BlockChange(
playerUniqueIdentity = null,
event = event,
cause = "dispense"
)
)
@EventHandler(priority = EventPriority.MONITOR)
fun onBlockSpread(event: BlockSpreadEvent) = buffer.push(
BlockChange(
playerUniqueIdentity = null,
event = event,
cause = "spread"
)
)
@EventHandler(priority = EventPriority.MONITOR)
fun onFluidLevelChange(event: FluidLevelChangeEvent) = buffer.push(
BlockChange(
playerUniqueIdentity = null,
event = event,
cause = "fluid-level-change"
)
)
}
companion object : EventCollectorProvider<BlockChange> {
override fun collector(buffer: EventBuffer): EventCollector<BlockChange> = Collector(buffer)
}
}

View File

@ -1,47 +0,0 @@
package gay.pizza.foundation.heimdall.plugin.event
import gay.pizza.foundation.heimdall.plugin.buffer.EventBuffer
import gay.pizza.foundation.heimdall.plugin.buffer.IEventBuffer
import gay.pizza.foundation.heimdall.table.BlockPlaceTable
import org.bukkit.Location
import org.bukkit.Material
import org.bukkit.event.EventHandler
import org.bukkit.event.block.BlockPlaceEvent
import org.jetbrains.exposed.sql.Transaction
import org.jetbrains.exposed.sql.insert
import java.time.Instant
import java.util.*
class BlockPlace(
val playerUniqueIdentity: UUID,
val location: Location,
val material: Material,
val blockData: String? = null,
val timestamp: Instant = Instant.now()
) : HeimdallEvent() {
constructor(event: BlockPlaceEvent) : this(
event.player.uniqueId,
event.block.location,
event.block.type,
event.block.blockData.asString
)
override fun store(transaction: Transaction) {
transaction.apply {
BlockPlaceTable.insert {
putPlayerTimedLocalEvent(it, timestamp, location, playerUniqueIdentity)
it[block] = material.key.toString()
it[blockData] = this@BlockPlace.blockData
}
}
}
class Collector(val buffer: IEventBuffer) : EventCollector<BlockPlace> {
@EventHandler
fun onBlockPlaced(event: BlockPlaceEvent) = buffer.push(BlockPlace(event))
}
companion object : EventCollectorProvider<BlockPlace> {
override fun collector(buffer: EventBuffer): EventCollector<BlockPlace> = Collector(buffer)
}
}

View File

@ -2,8 +2,7 @@ package gay.pizza.foundation.heimdall.plugin.event
object EventCollectorProviders {
val all = listOf<EventCollectorProvider<*>>(
BlockBreak,
BlockPlace,
BlockChange,
EntityKill,
PlayerAdvancement,
PlayerDeath,

View File

@ -28,7 +28,7 @@ fun <T : PlayerTimedLocalEventTable, K : Any> T.putPlayerTimedLocalEvent(
statement: InsertStatement<K>,
time: Instant,
location: Location,
player: UUID
player: UUID?
) {
putTimedLocalEvent(statement, time, location)
statement[this.player] = player

View File

@ -26,9 +26,9 @@ alter table player_positions set (
--
select add_compression_policy('player_positions', interval '3 days', if_not_exists => true);
--
create table if not exists block_breaks (
create table if not exists block_changes (
time timestamp not null,
player uuid not null,
player uuid null,
world uuid not null,
x double precision not null,
y double precision not null,
@ -36,25 +36,12 @@ create table if not exists block_breaks (
pitch double precision not null,
yaw double precision not null,
block text not null,
PRIMARY KEY (time, player, world)
data text not null,
cause text not null,
PRIMARY KEY (time, world, x, y, z)
);
--
select create_hypertable('block_breaks', 'time', 'player', 4, if_not_exists => TRUE);
--
create table if not exists block_places (
time timestamp not null,
player uuid not null,
world uuid not null,
x double precision not null,
y double precision not null,
z double precision not null,
pitch double precision not null,
yaw double precision not null,
block text not null,
PRIMARY KEY (time, player, world)
);
--
select create_hypertable('block_places', 'time', 'player', 4, if_not_exists => TRUE);
select create_hypertable('block_changes', 'time', 'x', 4, if_not_exists => TRUE);
--
create table if not exists player_sessions (
id uuid not null,
@ -140,13 +127,3 @@ create or replace view player_names as
) as name
from unique_player_ids;
--
alter table block_places add column if not exists block_data text null;
--
alter table block_breaks add column if not exists block_data text null;
--
create or replace view block_changes as
select true as break, *
from block_breaks
union all
select false as break, * from block_places;
--

View File

@ -8,11 +8,11 @@ import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.required
import com.github.ajalt.clikt.parameters.types.enum
import com.github.ajalt.clikt.parameters.types.int
import gay.pizza.foundation.heimdall.table.BlockChangeTable
import gay.pizza.foundation.heimdall.table.WorldChangeTable
import gay.pizza.foundation.heimdall.tool.render.*
import gay.pizza.foundation.heimdall.tool.state.*
import gay.pizza.foundation.heimdall.tool.util.compose
import gay.pizza.foundation.heimdall.view.BlockChangeView
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.greaterEq
@ -49,8 +49,6 @@ class BlockChangeTimelapseCommand : CliktCommand("Block Change Timelapse", name
.enum<ImageFormatType> { it.id }
.default(ImageFormatType.Png)
private val considerAirBlocks by option("--consider-air-blocks", help = "Enable Air Block Consideration").flag()
private val fromCoordinate by option("--trim-from", help = "Trim From Coordinate")
private val toCoordinate by option("--trim-to", help = "Trim To Coordinate")
@ -100,11 +98,11 @@ class BlockChangeTimelapseCommand : CliktCommand("Block Change Timelapse", name
val filter = compose(
combine = { a, b -> a and b },
{ trim?.first?.x != null } to { BlockChangeView.x greaterEq trim!!.first.x.toDouble() },
{ trim?.first?.z != null } to { BlockChangeView.z greaterEq trim!!.first.z.toDouble() },
{ trim?.second?.x != null } to { BlockChangeView.x lessEq trim!!.second.x.toDouble() },
{ trim?.second?.z != null } to { BlockChangeView.z lessEq trim!!.second.z.toDouble() },
{ true } to { BlockChangeView.world eq world }
{ trim?.first?.x != null } to { BlockChangeTable.x greaterEq trim!!.first.x.toDouble() },
{ trim?.first?.z != null } to { BlockChangeTable.z greaterEq trim!!.first.z.toDouble() },
{ trim?.second?.x != null } to { BlockChangeTable.x lessEq trim!!.second.x.toDouble() },
{ trim?.second?.z != null } to { BlockChangeTable.z lessEq trim!!.second.z.toDouble() },
{ true } to { BlockChangeTable.world eq world }
)
val changelog = BlockChangelog.query(db, filter)
@ -131,7 +129,6 @@ class BlockChangeTimelapseCommand : CliktCommand("Block Change Timelapse", name
val pool = BlockMapRenderPool(
changelog = changelog,
blockTrackMode = if (considerAirBlocks) BlockTrackMode.AirOnDelete else BlockTrackMode.RemoveOnDelete,
delegate = timelapse,
createRendererFunction = { expanse -> render.createNewRenderer(expanse, db) },
threadPoolExecutor = threadPoolExecutor

View File

@ -36,9 +36,9 @@ class PlayerLocationShareRenderer(
val player = it[PlayerPositionTable.player]
playerSparseMap.createOrModify(
coordinate,
create = { mutableListOf(player) },
modify = { players -> players.add(player) })
allPlayerIds.add(player)
create = { mutableListOf(player!!) },
modify = { players -> players.add(player!!) })
allPlayerIds.add(player!!)
}
}

View File

@ -1,9 +0,0 @@
package gay.pizza.foundation.heimdall.tool.state
import kotlinx.serialization.Serializable
@Serializable
enum class BlockChangeType {
Place,
Break
}

View File

@ -1,6 +1,6 @@
package gay.pizza.foundation.heimdall.tool.state
import gay.pizza.foundation.heimdall.view.BlockChangeView
import gay.pizza.foundation.heimdall.table.BlockChangeTable
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.Op
import org.jetbrains.exposed.sql.select
@ -10,7 +10,7 @@ import java.time.Instant
import java.util.stream.Stream
class BlockChangelog(
val changes: List<BlockChange>
val changes: List<RecordedBlockChange>
) {
fun slice(slice: ChangelogSlice): BlockChangelog = BlockChangelog(changes.filter {
slice.isTimeWithinFullRange(it.time)
@ -60,43 +60,30 @@ class BlockChangelog(
companion object {
fun query(db: Database, filter: Op<Boolean> = Op.TRUE): BlockChangelog = transaction(db) {
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[BlockChangeView.block]
val blockData = row[BlockChangeView.blockData]
BlockChangelog(BlockChangeTable.select(filter).orderBy(BlockChangeTable.time).map { row ->
val time = row[BlockChangeTable.time]
val world = row[BlockChangeTable.world]
val x = row[BlockChangeTable.x]
val y = row[BlockChangeTable.y]
val z = row[BlockChangeTable.z]
val blockMaterial = row[BlockChangeTable.block]
val blockData = row[BlockChangeTable.data]
val location = BlockCoordinate(x.toLong(), y.toLong(), z.toLong())
val fromBlock = if (changeIsBreak) {
BlockState(block, blockData)
} else {
BlockState.AirBlock
}
val block = BlockState(blockMaterial, blockData)
val toBlock = if (changeIsBreak) {
BlockState.AirBlock
} else {
BlockState(block, blockData)
}
BlockChange(
RecordedBlockChange(
time,
world,
if (changeIsBreak) BlockChangeType.Break else BlockChangeType.Place,
location,
fromBlock,
toBlock
block
)
})
}
}
fun <T> splitBy(key: (BlockChange) -> T): Map<T, BlockChangelog> {
val logs = mutableMapOf<T, MutableList<BlockChange>>()
fun <T> splitBy(key: (RecordedBlockChange) -> T): Map<T, BlockChangelog> {
val logs = mutableMapOf<T, MutableList<RecordedBlockChange>>()
for (change in changes) {
val k = key(change)
var log = logs[k]

View File

@ -5,7 +5,7 @@ import gay.pizza.foundation.heimdall.tool.util.minOfAll
import java.util.concurrent.ConcurrentHashMap
import kotlin.math.absoluteValue
class BlockLogTracker(private val mode: BlockTrackMode = BlockTrackMode.RemoveOnDelete, isConcurrent: Boolean = false) {
class BlockLogTracker(isConcurrent: Boolean = false) {
internal val blocks: MutableMap<BlockCoordinate, BlockState> = if (isConcurrent) ConcurrentHashMap() else mutableMapOf()
fun place(position: BlockCoordinate, state: BlockState) {
@ -16,14 +16,6 @@ class BlockLogTracker(private val mode: BlockTrackMode = BlockTrackMode.RemoveOn
blocks.putAll(map)
}
fun delete(position: BlockCoordinate) {
if (mode == BlockTrackMode.AirOnDelete) {
blocks[position] = BlockState.AirBlock
} else {
blocks.remove(position)
}
}
fun calculateZeroBlockOffset(): BlockCoordinate {
val (x, y, z) = blocks.keys.minOfAll(3) { listOf(it.x, it.y, it.z) }
val xOffset = if (x < 0) x.absoluteValue else 0
@ -60,11 +52,7 @@ class BlockLogTracker(private val mode: BlockTrackMode = BlockTrackMode.RemoveOn
}
fun replay(changelog: BlockChangelog) = changelog.changes.forEach { change ->
if (change.type == BlockChangeType.Break) {
delete(change.location)
} else {
place(change.location, change.to)
}
place(change.location, change.state)
}
fun get(position: BlockCoordinate): BlockState? = blocks[position]

View File

@ -8,7 +8,6 @@ import java.util.concurrent.ThreadPoolExecutor
class BlockMapRenderPool<T>(
val changelog: BlockChangelog,
val blockTrackMode: BlockTrackMode,
val createRendererFunction: (BlockExpanse) -> BlockMapRenderer<T>,
val delegate: BlockMapRenderPoolDelegate<T>,
val threadPoolExecutor: ThreadPoolExecutor,
@ -64,7 +63,7 @@ class BlockMapRenderPool<T>(
private fun runPlaybackSlice(id: String, slice: ChangelogSlice) {
val start = System.currentTimeMillis()
val sliced = changelog.slice(slice)
val tracker = BlockLogTracker(blockTrackMode)
val tracker = BlockLogTracker()
tracker.replay(sliced)
if (tracker.isNotEmpty()) {
trackers[slice] = tracker

View File

@ -21,7 +21,7 @@ class PlayerPositionChangelog(
val pitch = row[PlayerPositionTable.z]
val yaw = row[PlayerPositionTable.z]
PlayerPositionChange(time, player, world, x, y, z, pitch, yaw)
PlayerPositionChange(time, player!!, world, x, y, z, pitch, yaw)
})
}
}

View File

@ -3,11 +3,9 @@ package gay.pizza.foundation.heimdall.tool.state
import java.time.Instant
import java.util.UUID
data class BlockChange(
data class RecordedBlockChange(
val time: Instant,
val world: UUID,
val type: BlockChangeType,
val location: BlockCoordinate,
val from: BlockState,
val to: BlockState
val state: BlockState
)