From 988d2f9d552f36579b8fa6721302bc026a212445 Mon Sep 17 00:00:00 2001 From: Drex Date: Thu, 15 Aug 2024 23:16:57 +0200 Subject: [PATCH] Improve rollback / restore logic (#278) --- .../preview/ServerPlayerEntityMixin.java | 26 ++-- .../ledger/actions/AbstractActionType.kt | 1 + .../ledger/actions/ActionType.kt | 1 + .../ledger/actions/ItemChangeActionType.kt | 20 +-- .../commands/subcommands/RestoreCommand.kt | 10 +- .../commands/subcommands/RollbackCommand.kt | 10 +- .../ledger/database/DatabaseManager.kt | 122 +++++++----------- .../packet/receiver/SearchC2SPacket.kt | 25 +++- .../ledger/utility/ItemChangeLogic.kt | 78 +++++++++++ 9 files changed, 168 insertions(+), 125 deletions(-) create mode 100644 src/main/kotlin/com/github/quiltservertools/ledger/utility/ItemChangeLogic.kt diff --git a/src/main/java/com/github/quiltservertools/ledger/mixin/preview/ServerPlayerEntityMixin.java b/src/main/java/com/github/quiltservertools/ledger/mixin/preview/ServerPlayerEntityMixin.java index 1119d587..35ca6fc2 100644 --- a/src/main/java/com/github/quiltservertools/ledger/mixin/preview/ServerPlayerEntityMixin.java +++ b/src/main/java/com/github/quiltservertools/ledger/mixin/preview/ServerPlayerEntityMixin.java @@ -5,6 +5,7 @@ import com.github.quiltservertools.ledger.utility.HandlerWithContext; import com.llamalad7.mixinextras.sugar.Local; import kotlin.Pair; +import net.minecraft.inventory.SimpleInventory; import net.minecraft.item.ItemStack; import net.minecraft.screen.ScreenHandler; import net.minecraft.server.network.ServerPlayerEntity; @@ -18,6 +19,9 @@ import java.util.List; +import static com.github.quiltservertools.ledger.utility.ItemChangeLogicKt.addItem; +import static com.github.quiltservertools.ledger.utility.ItemChangeLogicKt.removeMatchingItem; + @Mixin(targets = "net.minecraft.server.network.ServerPlayerEntity$1") public abstract class ServerPlayerEntityMixin { @@ -40,28 +44,14 @@ private DefaultedList modifyStacks(DefaultedList stacks, @ if (preview == null) return stacks; List> modifiedItems = preview.getModifiedItems().get(pos); if (modifiedItems == null) return stacks; - // Copy original list - DefaultedList previewStacks = DefaultedList.of(); - previewStacks.addAll(stacks); + SimpleInventory inventory = new SimpleInventory(stacks.toArray(new ItemStack[]{})); for (Pair modifiedItem : modifiedItems) { if (modifiedItem.component2()) { - // Add item - for (int i = 0; i < previewStacks.size(); i++) { - if (previewStacks.get(i).isEmpty()) { - previewStacks.set(i, modifiedItem.component1()); - break; - } - } + addItem(modifiedItem.component1(), inventory); } else { - // Remove item - for (int i = 0; i < previewStacks.size(); i++) { - if (ItemStack.areItemsEqual(previewStacks.get(i), modifiedItem.component1())) { - previewStacks.set(i, ItemStack.EMPTY); - break; - } - } + removeMatchingItem(modifiedItem.component1(), inventory); } } - return previewStacks; + return inventory.getHeldStacks(); } } diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/actions/AbstractActionType.kt b/src/main/kotlin/com/github/quiltservertools/ledger/actions/AbstractActionType.kt index c725267d..ad144b9b 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/actions/AbstractActionType.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/actions/AbstractActionType.kt @@ -21,6 +21,7 @@ import java.time.Instant import kotlin.time.ExperimentalTime abstract class AbstractActionType : ActionType { + override var id: Int = -1 override var timestamp: Instant = Instant.now() override var pos: BlockPos = BlockPos.ORIGIN override var world: Identifier? = null diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/actions/ActionType.kt b/src/main/kotlin/com/github/quiltservertools/ledger/actions/ActionType.kt index b2720a6d..d8c4518f 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/actions/ActionType.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/actions/ActionType.kt @@ -14,6 +14,7 @@ import java.time.Instant import kotlin.time.ExperimentalTime interface ActionType { + var id: Int val identifier: String var timestamp: Instant var pos: BlockPos diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/actions/ItemChangeActionType.kt b/src/main/kotlin/com/github/quiltservertools/ledger/actions/ItemChangeActionType.kt index ca379df7..f4d1e782 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/actions/ItemChangeActionType.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/actions/ItemChangeActionType.kt @@ -3,9 +3,11 @@ package com.github.quiltservertools.ledger.actions import com.github.quiltservertools.ledger.actionutils.Preview import com.github.quiltservertools.ledger.utility.NbtUtils import com.github.quiltservertools.ledger.utility.TextColorPallet +import com.github.quiltservertools.ledger.utility.addItem import com.github.quiltservertools.ledger.utility.getOtherChestSide import com.github.quiltservertools.ledger.utility.getWorld import com.github.quiltservertools.ledger.utility.literal +import com.github.quiltservertools.ledger.utility.removeMatchingItem import net.minecraft.block.Blocks import net.minecraft.block.ChestBlock import net.minecraft.block.InventoryProvider @@ -108,15 +110,8 @@ abstract class ItemChangeActionType : AbstractActionType() { if (world != null) { val rollbackStack = getStack(server) - if (inventory != null) { - for (i in 0 until inventory.size()) { - val stack = inventory.getStack(i) - if (ItemStack.areItemsEqual(stack, rollbackStack)) { - inventory.setStack(i, ItemStack.EMPTY) - return true - } - } + return removeMatchingItem(rollbackStack, inventory) } else if (rollbackStack.isOf(Items.WRITABLE_BOOK) || rollbackStack.isOf(Items.WRITTEN_BOOK)) { val blockEntity = world.getBlockEntity(pos) if (blockEntity is LecternBlockEntity) { @@ -136,15 +131,8 @@ abstract class ItemChangeActionType : AbstractActionType() { if (world != null) { val rollbackStack = getStack(server) - if (inventory != null) { - for (i in 0 until inventory.size()) { - val stack = inventory.getStack(i) - if (stack.isEmpty) { - inventory.setStack(i, rollbackStack) - return true - } - } + return addItem(rollbackStack, inventory) } else if (rollbackStack.isOf(Items.WRITABLE_BOOK) || rollbackStack.isOf(Items.WRITTEN_BOOK)) { val blockEntity = world.getBlockEntity(pos) if (blockEntity is LecternBlockEntity && !blockEntity.hasBook()) { diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/commands/subcommands/RestoreCommand.kt b/src/main/kotlin/com/github/quiltservertools/ledger/commands/subcommands/RestoreCommand.kt index 31f141da..0f19278d 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/commands/subcommands/RestoreCommand.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/commands/subcommands/RestoreCommand.kt @@ -34,7 +34,7 @@ object RestoreCommand : BuildableCommand { params.ensureSpecific() Ledger.launch(Dispatchers.IO) { MessageUtils.warnBusy(source) - val actions = DatabaseManager.restoreActions(params) + val actions = DatabaseManager.selectRestore(params) if (actions.isEmpty()) { source.sendError(Text.translatable("error.ledger.command.no_results")) @@ -53,12 +53,16 @@ object RestoreCommand : BuildableCommand { context.source.world.launchMain { val fails = HashMap() - + val actionIds = HashSet() for (action in actions) { if (!action.restore(context.source.server)) { fails[action.identifier] = fails.getOrPut(action.identifier) { 0 } + 1 + } else { + actionIds.add(action.id) } - action.rolledBack = true + } + Ledger.launch(Dispatchers.IO) { + DatabaseManager.restoreActions(actionIds) } for (entry in fails.entries) { diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/commands/subcommands/RollbackCommand.kt b/src/main/kotlin/com/github/quiltservertools/ledger/commands/subcommands/RollbackCommand.kt index 8823c656..98da7128 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/commands/subcommands/RollbackCommand.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/commands/subcommands/RollbackCommand.kt @@ -34,7 +34,7 @@ object RollbackCommand : BuildableCommand { params.ensureSpecific() Ledger.launch(Dispatchers.IO) { MessageUtils.warnBusy(source) - val actions = DatabaseManager.rollbackActions(params) + val actions = DatabaseManager.selectRollback(params) if (actions.isEmpty()) { source.sendError(Text.translatable("error.ledger.command.no_results")) @@ -53,12 +53,16 @@ object RollbackCommand : BuildableCommand { context.source.world.launchMain { val fails = HashMap() - + val actionIds = HashSet() for (action in actions) { if (!action.rollback(context.source.server)) { fails[action.identifier] = fails.getOrPut(action.identifier) { 0 } + 1 + } else { + actionIds.add(action.id) } - action.rolledBack = true + } + Ledger.launch(Dispatchers.IO) { + DatabaseManager.rollbackActions(actionIds) } for (entry in fails.entries) { diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/database/DatabaseManager.kt b/src/main/kotlin/com/github/quiltservertools/ledger/database/DatabaseManager.kt index c374e4b4..cb22f93a 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/database/DatabaseManager.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/database/DatabaseManager.kt @@ -30,6 +30,7 @@ import org.jetbrains.exposed.sql.Op import org.jetbrains.exposed.sql.Query import org.jetbrains.exposed.sql.SchemaUtils import org.jetbrains.exposed.sql.SortOrder +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.inSubQuery import org.jetbrains.exposed.sql.SqlExpressionBuilder.lessEq import org.jetbrains.exposed.sql.SqlLogger @@ -148,18 +149,49 @@ object DatabaseManager { } suspend fun rollbackActions(params: ActionSearchParams): List = execute { - return@execute selectAndRollbackActions(params) + val actions = selectRollback(params) + val actionIds = actions.map { it.id }.toSet() + rollbackActions(actionIds) + return@execute actions + } + + suspend fun rollbackActions(actionIds: Set) = execute { + return@execute rollbackActions(actionIds) } suspend fun restoreActions(params: ActionSearchParams): List = execute { - return@execute selectAndRestoreActions(params) + val actions = selectRestore(params) + val actionIds = actions.map { it.id }.toSet() + restoreActions(actionIds) + return@execute actions + } + + suspend fun restoreActions(actionIds: Set) = execute { + return@execute restoreActions(actionIds) + } + + suspend fun selectRollback(params: ActionSearchParams): List = execute { + val query = buildQuery() + .where(buildQueryParams(params) and (Tables.Actions.rolledBack eq false)) + .orderBy(Tables.Actions.id, SortOrder.DESC) + return@execute getActionsFromQuery(query) + } + + suspend fun selectRestore(params: ActionSearchParams): List = execute { + val query = buildQuery() + .where(buildQueryParams(params) and (Tables.Actions.rolledBack eq true)) + .orderBy(Tables.Actions.id, SortOrder.ASC) + return@execute getActionsFromQuery(query) } suspend fun previewActions( params: ActionSearchParams, type: Preview.Type ): List = execute { - return@execute selectActionsPreview(params, type) + when (type) { + Preview.Type.ROLLBACK -> return@execute selectRollback(params) + Preview.Type.RESTORE -> return@execute selectRestore(params) + } } private fun getActionsFromQuery(query: Query): List { @@ -173,6 +205,7 @@ object DatabaseManager { } val type = typeSupplier.get() + type.id = action[Tables.Actions.id].value type.timestamp = action[Tables.Actions.timestamp] type.pos = BlockPos(action[Tables.Actions.x], action[Tables.Actions.y], action[Tables.Actions.z]) type.world = Identifier.tryParse(action[Tables.Worlds.identifier]) @@ -448,23 +481,11 @@ object DatabaseManager { private fun Transaction.selectActionsSearch(params: ActionSearchParams, page: Int): SearchResults { val actions = mutableListOf() - var totalActions: Long - var query = Tables.Actions - .innerJoin(Tables.ActionIdentifiers) - .innerJoin(Tables.Worlds) - .leftJoin(Tables.Players) - .innerJoin( - Tables.oldObjectTable, - { Tables.Actions.oldObjectId }, - { Tables.oldObjectTable[Tables.ObjectIdentifiers.id] } - ) - .innerJoin(Tables.ObjectIdentifiers, { Tables.Actions.objectId }, { Tables.ObjectIdentifiers.id }) - .innerJoin(Tables.Sources) - .selectAll() + var query = buildQuery() .andWhere { buildQueryParams(params) } - totalActions = countActions(params) + val totalActions: Long = countActions(params) if (totalActions == 0L) return SearchResults(actions, params, page, 0) query = query.orderBy(Tables.Actions.id, SortOrder.DESC) @@ -485,15 +506,8 @@ object DatabaseManager { .andWhere { buildQueryParams(params) } .count() - private fun Transaction.selectActionsPreview( - params: ActionSearchParams, - type: Preview.Type - ): MutableList { - val actions = mutableListOf() - - val isRestore = type == Preview.Type.RESTORE - - val selectQuery = Tables.Actions + private fun Transaction.buildQuery(): Query { + return Tables.Actions .innerJoin(Tables.ActionIdentifiers) .innerJoin(Tables.Worlds) .leftJoin(Tables.Players) @@ -505,68 +519,20 @@ object DatabaseManager { .innerJoin(Tables.ObjectIdentifiers, { Tables.Actions.objectId }, { Tables.ObjectIdentifiers.id }) .innerJoin(Tables.Sources) .selectAll() - .andWhere { buildQueryParams(params) and (Tables.Actions.rolledBack eq isRestore) } - .orderBy(Tables.Actions.id, if (isRestore) SortOrder.ASC else SortOrder.DESC) - actions.addAll(getActionsFromQuery(selectQuery)) - - return actions } - private fun Transaction.selectAndRollbackActions(params: ActionSearchParams): MutableList { - val actions = mutableListOf() - - val selectQuery = Tables.Actions - .innerJoin(Tables.ActionIdentifiers) - .innerJoin(Tables.Worlds) - .leftJoin(Tables.Players) - .innerJoin( - Tables.oldObjectTable, - { Tables.Actions.oldObjectId }, - { Tables.oldObjectTable[Tables.ObjectIdentifiers.id] } - ) - .innerJoin(Tables.ObjectIdentifiers, { Tables.Actions.objectId }, { Tables.ObjectIdentifiers.id }) - .innerJoin(Tables.Sources) - .selectAll() - .andWhere { buildQueryParams(params) and (Tables.Actions.rolledBack eq false) } - .orderBy(Tables.Actions.id, SortOrder.DESC) - val actionIds = selectQuery.map { it[Tables.Actions.id] } - .toSet() // SQLite doesn't support update where so select by ID. Might not be as efficent - actions.addAll(getActionsFromQuery(selectQuery)) - + private fun Transaction.rollbackActions(actionIds: Set) { Tables.Actions - .update({ Tables.Actions.id inList actionIds and (Tables.Actions.rolledBack eq false) }) { + .update({ Tables.Actions.id inList actionIds }) { it[rolledBack] = true } - - return actions } - private fun Transaction.selectAndRestoreActions(params: ActionSearchParams): MutableList { - val actions = mutableListOf() - - val selectQuery = Tables.Actions - .innerJoin(Tables.ActionIdentifiers) - .innerJoin(Tables.Worlds) - .leftJoin(Tables.Players) - .innerJoin( - Tables.oldObjectTable, - { Tables.Actions.oldObjectId }, - { Tables.oldObjectTable[Tables.ObjectIdentifiers.id] } - ) - .innerJoin(Tables.ObjectIdentifiers, { Tables.Actions.objectId }, { Tables.ObjectIdentifiers.id }) - .innerJoin(Tables.Sources) - .selectAll() - .andWhere { buildQueryParams(params) and (Tables.Actions.rolledBack eq true) } - .orderBy(Tables.Actions.id, SortOrder.ASC) - val actionIds = selectQuery.map { it[Tables.Actions.id] }.toSet() - actions.addAll(getActionsFromQuery(selectQuery)) - + private fun Transaction.restoreActions(actionIds: Set) { Tables.Actions - .update({ Tables.Actions.id inList actionIds and (Tables.Actions.rolledBack eq true) }) { + .update({ Tables.Actions.id inList actionIds }) { it[rolledBack] = false } - - return actions } fun getKnownSources() = diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/network/packet/receiver/SearchC2SPacket.kt b/src/main/kotlin/com/github/quiltservertools/ledger/network/packet/receiver/SearchC2SPacket.kt index 2102067a..46c4cf9a 100644 --- a/src/main/kotlin/com/github/quiltservertools/ledger/network/packet/receiver/SearchC2SPacket.kt +++ b/src/main/kotlin/com/github/quiltservertools/ledger/network/packet/receiver/SearchC2SPacket.kt @@ -56,12 +56,18 @@ data class SearchC2SPacket(val restore: Boolean, val args: String) : CustomPaylo Ledger.launch(Dispatchers.IO) { MessageUtils.warnBusy(source) if (payload.restore) { - val actions = DatabaseManager.restoreActions(params) + val actions = DatabaseManager.selectRestore(params) source.world.launchMain { + val actionIds = HashSet() + for (action in actions) { - action.restore(source.server) - action.rolledBack = false + if (action.restore(source.server)) { + actionIds.add(action.id) + } + } + Ledger.launch(Dispatchers.IO) { + DatabaseManager.restoreActions(actionIds) } ResponseS2CPacket.sendResponse( @@ -73,14 +79,19 @@ data class SearchC2SPacket(val restore: Boolean, val args: String) : CustomPaylo ) } } else { - val actions = DatabaseManager.rollbackActions(params) + val actions = DatabaseManager.selectRollback(params) source.world.launchMain { + val actionIds = HashSet() + for (action in actions) { - action.rollback(source.server) - action.rolledBack = true + if (action.rollback(source.server)) { + actionIds.add(action.id) + } + } + Ledger.launch(Dispatchers.IO) { + DatabaseManager.rollbackActions(actionIds) } - ResponseS2CPacket.sendResponse( ResponseContent( LedgerPacketTypes.ROLLBACK.id, diff --git a/src/main/kotlin/com/github/quiltservertools/ledger/utility/ItemChangeLogic.kt b/src/main/kotlin/com/github/quiltservertools/ledger/utility/ItemChangeLogic.kt new file mode 100644 index 00000000..e2394000 --- /dev/null +++ b/src/main/kotlin/com/github/quiltservertools/ledger/utility/ItemChangeLogic.kt @@ -0,0 +1,78 @@ +package com.github.quiltservertools.ledger.utility + +import net.minecraft.inventory.Inventory +import net.minecraft.item.ItemStack + +fun addItem(rollbackStack: ItemStack, inventory: Inventory): Boolean { + // Check if the inventory has enough space + var matchingCountLeft = 0 + for (i in 0 until inventory.size()) { + val stack = inventory.getStack(i) + if (stack.isEmpty) { + matchingCountLeft += rollbackStack.maxCount + } else if (ItemStack.areItemsAndComponentsEqual(stack, rollbackStack)) { + matchingCountLeft += stack.maxCount - stack.count + } + } + if (matchingCountLeft < rollbackStack.count) { + return false + } + var requiredCount = rollbackStack.count + for (i in 0 until inventory.size()) { + val stack = inventory.getStack(i) + if (stack.isEmpty) { + if (requiredCount > rollbackStack.maxCount) { + inventory.setStack(i, rollbackStack.copyWithCount(rollbackStack.maxCount)) + requiredCount -= rollbackStack.maxCount + } else { + inventory.setStack(i, rollbackStack.copyWithCount(requiredCount)) + requiredCount = 0 + } + } else if (ItemStack.areItemsAndComponentsEqual(stack, rollbackStack)) { + val countUntilMax = rollbackStack.maxCount - stack.count + if (requiredCount > countUntilMax) { + inventory.setStack(i, rollbackStack.copyWithCount(rollbackStack.maxCount)) + requiredCount -= countUntilMax + } else { + inventory.setStack(i, rollbackStack.copyWithCount(stack.count + requiredCount)) + requiredCount = 0 + } + } + if (requiredCount <= 0) { + return true + } + } + return false +} + +fun removeMatchingItem(rollbackStack: ItemStack, inventory: Inventory): Boolean { + // Check if the inventory has enough matching items + var matchingCount = 0 + for (i in 0 until inventory.size()) { + val stack = inventory.getStack(i) + if (ItemStack.areItemsAndComponentsEqual(stack, rollbackStack)) { + matchingCount += stack.count + } + } + if (matchingCount < rollbackStack.count) { + return false + } + var requiredCount = rollbackStack.count + for (i in 0 until inventory.size()) { + val stack = inventory.getStack(i) + if (ItemStack.areItemsAndComponentsEqual(stack, rollbackStack)) { + if (requiredCount < stack.count) { + // Only some parts of this stack are needed + inventory.setStack(i, stack.copyWithCount(stack.count - requiredCount)) + requiredCount = 0 + } else { + inventory.setStack(i, ItemStack.EMPTY) + requiredCount -= stack.count + } + if (requiredCount <= 0) { + return true + } + } + } + return false +}