From 4c00a1f891c019267dd175ec69617f781c5d0b04 Mon Sep 17 00:00:00 2001 From: Technofied <40795318+Technofied@users.noreply.github.com> Date: Sat, 27 Jun 2026 21:51:16 +0800 Subject: [PATCH 1/7] first cut of Realty auto-vaulting --- bin/main/config.yml | 21 +++ bin/main/plugin.yml | 39 +++++ build.gradle | 5 + .../vault/VaultStoragePlugin.java | 12 ++ .../RealtyOccupantChangeListener.java | 56 +++++++ .../internal/service/AutoVaultService.java | 148 ++++++++++++++++++ .../internal/service/VaultCaptureService.java | 137 ++++++++++++---- .../internal/util/config/ConfigPaths.java | 4 +- .../vault/internal/util/region/RegionKey.java | 17 ++ .../util/scan/OfflineRegionScanner.java | 130 +++++++++++++++ src/main/resources/config.yml | 10 ++ src/main/resources/plugin.yml | 2 +- 12 files changed, 547 insertions(+), 34 deletions(-) create mode 100644 bin/main/config.yml create mode 100644 bin/main/plugin.yml create mode 100644 src/main/java/net/democracycraft/vault/internal/listener/RealtyOccupantChangeListener.java create mode 100644 src/main/java/net/democracycraft/vault/internal/service/AutoVaultService.java create mode 100644 src/main/java/net/democracycraft/vault/internal/util/region/RegionKey.java create mode 100644 src/main/java/net/democracycraft/vault/internal/util/scan/OfflineRegionScanner.java diff --git a/bin/main/config.yml b/bin/main/config.yml new file mode 100644 index 0000000..79f22c2 --- /dev/null +++ b/bin/main/config.yml @@ -0,0 +1,21 @@ +# Default configuration for VaultStorage +# This file is copied to the plugin data folder on first run. + +mysql: + host: "localhost" + port: 3306 + database: "vault_storage" + user: "root" + password: "" + useSSL: false + +# Automatically vault Bolt-locked containers when a Realty region changes occupant +# (bought/transferred or rented). A container is vaulted when its Bolt owner is not the +# new occupant and is not a WorldGuard owner/member of the region. Requires the Realty plugin. +auto-vault: + # Master switch. When false, no Realty listener is registered. + enabled: true + # Grace period (in server ticks, 20 = 1 second) between the occupant change and the sweep. + # If the region changes hands again within this window, the pending sweep is replaced. + delay-ticks: 1200 + diff --git a/bin/main/plugin.yml b/bin/main/plugin.yml new file mode 100644 index 0000000..e0be6ff --- /dev/null +++ b/bin/main/plugin.yml @@ -0,0 +1,39 @@ +name: VaultStorage +version: '1.0.15' +main: net.democracycraft.vault.VaultStoragePlugin +api-version: '1.21' +authors: [ Alepando ] +depend: [ Bolt , WorldGuard, WorldEdit, Essentials ] +softdepend: [ ChestShop, Realty ] + +commands: + vault: + description: Vault storage tools + usage: /vault [menu] + permission: vaultstorage.user + +permissions: + vaultstorage.user: + description: Allows using basic vault commands + default: op + vaultstorage.admin: + description: Admin override for vault commands + default: op + vaultstorage.action.view: + description: Allows viewing vaults in a virtual inventory + default: true + vaultstorage.action.edit: + description: Allows editing vault contents in a virtual inventory + default: op + vaultstorage.action.copy: + description: Allows copying items from a vault to the player inventory + default: op + vaultstorage.action.place: + description: Allows placing back the original block and restoring its contents + default: op + vaultstorage.admin.override: + description: Allows capturing/placing vault blocks even if not a region member or outside regions + default: false + vaultstorage.action.capture: + description: Allows capturing a container into a vault (requires region ownership & container ownership unless override) + default: op diff --git a/build.gradle b/build.gradle index a5e89f7..bae52a1 100644 --- a/build.gradle +++ b/build.gradle @@ -36,6 +36,10 @@ repositories { name = "minebench" url = "https://repo.minebench.de/" } + maven { + name = "democracycraft" + url = "https://maven.democracycraft.net/releases" + } } dependencies { @@ -49,6 +53,7 @@ dependencies { compileOnly("net.essentialsx:EssentialsX:2.21.2") compileOnly("org.geysermc.floodgate:api:2.2.4-SNAPSHOT") compileOnly("com.acrobot.chestshop:chestshop:3.12.2") + compileOnly("io.github.md5sha256:realty-paper-api:1.4.0") { transitive = false } } tasks { diff --git a/src/main/java/net/democracycraft/vault/VaultStoragePlugin.java b/src/main/java/net/democracycraft/vault/VaultStoragePlugin.java index 8cb9c08..021190b 100644 --- a/src/main/java/net/democracycraft/vault/VaultStoragePlugin.java +++ b/src/main/java/net/democracycraft/vault/VaultStoragePlugin.java @@ -10,7 +10,9 @@ import net.democracycraft.vault.internal.database.MySQLManager; import net.democracycraft.vault.internal.database.dao.VaultDAOImpl; import net.democracycraft.vault.internal.database.entity.WorldEntity; +import net.democracycraft.vault.internal.listener.RealtyOccupantChangeListener; import net.democracycraft.vault.internal.service.*; +import net.democracycraft.vault.internal.util.config.ConfigPaths; import net.democracycraft.vault.internal.session.BedrockUniqueIdentifierRetriever; import net.democracycraft.vault.internal.session.VaultSessionManager; import net.democracycraft.vault.internal.util.config.ConfigInitializer; @@ -44,6 +46,7 @@ public final class VaultStoragePlugin extends JavaPlugin { private VaultPlacementService placementService; private VaultInventoryService inventoryService; private VaultScanService scanService; + private AutoVaultService autoVaultService; // Integration services @@ -161,6 +164,14 @@ public void onEnable() { // Validate defined permissions vs plugin.yml validatePermissionsMapping(); + + // Realty soft dependency: auto-vault containers when a region's owner/tenant changes. + if (getServer().getPluginManager().getPlugin("Realty") != null + && getConfig().getBoolean(ConfigPaths.AUTOVAULT_ENABLED.getPath(), true)) { + this.autoVaultService = new AutoVaultService(this); + getServer().getPluginManager().registerEvents(new RealtyOccupantChangeListener(autoVaultService), this); + getLogger().info("Realty detected: auto-vault on region occupant change enabled."); + } } private void validatePermissionsMapping() { @@ -203,6 +214,7 @@ private void bootstrapWorlds() { @Override public void onDisable() { + if (this.autoVaultService != null) this.autoVaultService.shutdown(); if (this.mysql != null) this.mysql.disconnect(); } diff --git a/src/main/java/net/democracycraft/vault/internal/listener/RealtyOccupantChangeListener.java b/src/main/java/net/democracycraft/vault/internal/listener/RealtyOccupantChangeListener.java new file mode 100644 index 0000000..7a964a2 --- /dev/null +++ b/src/main/java/net/democracycraft/vault/internal/listener/RealtyOccupantChangeListener.java @@ -0,0 +1,56 @@ +package net.democracycraft.vault.internal.listener; + +import io.github.md5sha256.realty.api.event.RealtyRegionEvent; +import io.github.md5sha256.realty.api.event.RegionBoughtEvent; +import io.github.md5sha256.realty.api.event.RegionRentedEvent; +import io.github.md5sha256.realty.api.event.TenantSetEvent; +import io.github.md5sha256.realty.api.event.TitleTransferredEvent; +import net.democracycraft.vault.internal.service.AutoVaultService; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; + +/** + * Listens for Realty owner/tenant changes and triggers an {@link AutoVaultService} sweep for the new + * occupant. References Realty API types, so it is only instantiated when the Realty plugin is present. + */ +public final class RealtyOccupantChangeListener implements Listener { + + private final AutoVaultService autoVaultService; + + public RealtyOccupantChangeListener(@NotNull AutoVaultService autoVaultService) { + this.autoVaultService = autoVaultService; + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onTitleTransferred(TitleTransferredEvent event) { + submit(event, event.getNewTitleHolderId()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onRegionBought(RegionBoughtEvent event) { + submit(event, event.getBuyerId()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onRegionRented(RegionRentedEvent event) { + submit(event, event.getTenantId()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onTenantSet(TenantSetEvent event) { + // A null new tenant means the tenancy was cleared (out of scope); skip. + submit(event, event.getNewTenantId()); + } + + private void submit(@NotNull RealtyRegionEvent event, @Nullable UUID newOccupant) { + if (newOccupant == null) { + return; + } + autoVaultService.submit(event.getWorldId(), event.getRegionId(), newOccupant); + } +} diff --git a/src/main/java/net/democracycraft/vault/internal/service/AutoVaultService.java b/src/main/java/net/democracycraft/vault/internal/service/AutoVaultService.java new file mode 100644 index 0000000..2f7e892 --- /dev/null +++ b/src/main/java/net/democracycraft/vault/internal/service/AutoVaultService.java @@ -0,0 +1,148 @@ +package net.democracycraft.vault.internal.service; + +import net.democracycraft.vault.VaultStoragePlugin; +import net.democracycraft.vault.api.data.ScanResult; +import net.democracycraft.vault.api.region.VaultRegion; +import net.democracycraft.vault.api.service.WorldGuardService; +import net.democracycraft.vault.internal.util.config.ConfigPaths; +import net.democracycraft.vault.internal.util.region.RegionKey; +import net.democracycraft.vault.internal.util.scan.OfflineRegionScanner; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +/** + * Schedules and runs the delayed, batched auto-vaulting of Bolt-locked containers when a region's + * owner/tenant changes. A container is vaulted when its Bolt owner is not in the allowed set + * ({new occupant} plus the region's WorldGuard owners/members). Runs on the main thread. + */ +public class AutoVaultService { + + private final VaultStoragePlugin plugin; + // Main-thread only: populated from the Realty event handler and the scheduled sweep task. + private final Map pending = new HashMap<>(); + + public AutoVaultService(@NotNull VaultStoragePlugin plugin) { + this.plugin = plugin; + } + + /** + * Schedules an auto-vault sweep of {@code regionId} after the configured grace delay. + * Cancels any pending sweep already queued for the same region+world. + * + * @param worldId world the region lives in + * @param regionId WorldGuard region id + * @param initiator the new occupant (owner/tenant); recorded as vault creator and exempt from vaulting + */ + public void submit(@NotNull UUID worldId, @NotNull String regionId, @NotNull UUID initiator) { + if (!plugin.getConfig().getBoolean(ConfigPaths.AUTOVAULT_ENABLED.getPath(), true)) { + return; + } + RegionKey key = new RegionKey(worldId, regionId); + long delayTicks = Math.max(0L, plugin.getConfig().getLong(ConfigPaths.AUTOVAULT_DELAY_TICKS.getPath(), 1200L)); + + BukkitTask previous = pending.remove(key); + if (previous != null && !previous.isCancelled()) { + previous.cancel(); + } + + BukkitTask task = new BukkitRunnable() { + @Override public void run() { + // A fired task is always the current pending entry (a resubmit cancels the prior one). + pending.remove(key); + runSweep(key, initiator); + } + }.runTaskLater(plugin, delayTicks); + pending.put(key, task); + } + + private void runSweep(@NotNull RegionKey key, @NotNull UUID initiator) { + World world = Bukkit.getWorld(key.worldId()); + if (world == null) { + return; + } + WorldGuardService wgs = plugin.getWorldGuardService(); + if (wgs == null) { + return; + } + VaultRegion region = wgs.getRegionById(key.regionId(), world); + if (region == null) { + // Region was deleted during the delay window; nothing to sweep. + return; + } + + Set allowed = new HashSet<>(); + allowed.addAll(region.owners()); + allowed.addAll(region.members()); + allowed.add(initiator); + + new OfflineRegionScanner(key.worldId(), region.boundingBox(), allowed, results -> { + if (results == null || results.isEmpty()) { + return; + } + vaultBatched(results, initiator); + }).start(); + } + + private void vaultBatched(@NotNull List results, @NotNull UUID initiator) { + VaultCaptureService captureService = plugin.getCaptureService(); + final int batchSize = plugin.getConfig().getInt(ConfigPaths.SCAN_BATCH_SIZE.getPath(), 50); + + new BukkitRunnable() { + int index = 0; + final Set processed = new HashSet<>(); + + @Override public void run() { + long startTime = System.currentTimeMillis(); + int handled = 0; + + while (index < results.size() && handled < batchSize) { + if (System.currentTimeMillis() - startTime > 2) { + break; + } + ScanResult result = results.get(index); + index++; + handled++; + + Block block = result.block(); + Location loc = block.getLocation(); + // Skip blocks already consumed this run (e.g. the far half of a double chest). + if (!processed.add(loc)) continue; + + World world = block.getWorld(); + if (!world.isChunkLoaded(loc.getBlockX() >> 4, loc.getBlockZ() >> 4)) { + world.getChunkAt(loc.getBlockX() >> 4, loc.getBlockZ() >> 4); + } + // Tolerant of blocks changed since the scan (non-containers just drop their protection). + captureService.captureOfflineAsync(block, initiator, result.owner(), done -> {}); + } + + if (index >= results.size()) { + this.cancel(); + } + } + }.runTaskTimer(plugin, 1L, 1L); + } + + /** Cancels all pending sweeps. Call from {@code onDisable}. */ + public void shutdown() { + for (BukkitTask task : new ArrayList<>(pending.values())) { + if (task != null && !task.isCancelled()) { + task.cancel(); + } + } + pending.clear(); + } +} diff --git a/src/main/java/net/democracycraft/vault/internal/service/VaultCaptureService.java b/src/main/java/net/democracycraft/vault/internal/service/VaultCaptureService.java index c4b26cc..d832292 100644 --- a/src/main/java/net/democracycraft/vault/internal/service/VaultCaptureService.java +++ b/src/main/java/net/democracycraft/vault/internal/service/VaultCaptureService.java @@ -33,6 +33,7 @@ import org.bukkit.scheduler.BukkitTask; import org.bukkit.block.data.type.WallSign; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.time.Instant; import java.util.*; @@ -136,6 +137,11 @@ public boolean isContainerEmpty(Block block) { *

This method must be called on the main thread.

*/ public VaultImp captureFromBlock(Player actor, Block block) { + return captureFromBlock(actor.getUniqueId(), block); + } + + /** Player-free variant of {@link #captureFromBlock(Player, Block)}; the vault is owned by {@code vaultOwnerUuid}. */ + public VaultImp captureFromBlock(UUID vaultOwnerUuid, Block block) { if (!(block.getState() instanceof Container container)) { throw new IllegalArgumentException("Target block is not a container."); } @@ -154,8 +160,7 @@ public VaultImp captureFromBlock(Player actor, Block block) { block.setType(Material.AIR); UUID vaultId = UUID.randomUUID(); - UUID owner = actor.getUniqueId(); - return new VaultImp(owner, vaultId, stacks, material, location, when, blockDataString); + return new VaultImp(vaultOwnerUuid, vaultId, stacks, material, location, when, blockDataString); } /** @@ -179,8 +184,18 @@ public record CaptureOutcome(boolean empty, boolean protectionRemoved, VaultImp * */ public CaptureOutcome captureWithDoubleChestSupport(Player actor, @NonNull Block block, UUID originalOwner) { + return captureWithDoubleChestSupport(block, originalOwner, actor.getUniqueId(), actor::sendMessage); + } + + /** + * Player-free variant of {@link #captureWithDoubleChestSupport(Player, Block, UUID)}. The vault owner is + * {@code originalOwner} when non-null, else {@code finalOwnerFallback}; {@code warn} may be null to suppress notices. + */ + public CaptureOutcome captureWithDoubleChestSupport(@NonNull Block block, UUID originalOwner, + @NonNull UUID finalOwnerFallback, + Consumer warn) { BoltService bolt = VaultStoragePlugin.getInstance().getBoltService(); - UUID finalOwner = originalOwner != null ? originalOwner : actor.getUniqueId(); + UUID finalOwner = originalOwner != null ? originalOwner : finalOwnerFallback; // 1. Non-container path: treat as empty capture (remove protection) if (!(block.getState() instanceof Container container)) { @@ -201,7 +216,9 @@ public CaptureOutcome captureWithDoubleChestSupport(Player actor, @NonNull Block // If the target half is empty but the other half contains items, we abort the operation // to prevent accidental unprotection of the full chest. The user is guided to vault the non-empty side. if (targetIsEmpty && !otherHalfIsEmpty) { - actor.sendMessage(Component.text("[Vault Capture Service] You should vault the non-empty side first.").color(NamedTextColor.YELLOW)); + if (warn != null) { + warn.accept(Component.text("[Vault Capture Service] You should vault the non-empty side first.").color(NamedTextColor.YELLOW)); + } // Return empty=true so no vault is made, but protectionRemoved=false so we don't spam "Protection Removed". return new CaptureOutcome(true, false, null, originalOwner, finalOwner, List.of()); } @@ -244,8 +261,8 @@ public CaptureOutcome captureWithDoubleChestSupport(Player actor, @NonNull Block } } - // Capture block contents into vault (removes block) - VaultImp vault = captureFromBlock(actor, block); + // VaultImp owner carries the capturing identity; persistence uses CaptureOutcome.finalOwner(). + VaultImp vault = captureFromBlock(finalOwnerFallback, block); return new CaptureOutcome(false, true, vault, originalOwner, finalOwner, List.copyOf(reProtected)); } @@ -523,18 +540,7 @@ public void run() { "[VaultCaptureService] Vault created successfully: ID=" + newId + " Owner=" + validatedOwner + " for player " + actor.getName() ); } - List items = vault.contents(); - List batch = new ArrayList<>(items.size()); - for (int idx = 0; idx < items.size(); idx++) { - ItemStack itemStack = items.get(idx); - if (itemStack == null) continue; - VaultItemEntity vie = new VaultItemEntity(); - vie.vaultUuid = newId; - vie.slot = idx; - vie.amount = itemStack.getAmount(); - vie.item = ItemSerialization.toBytes(itemStack); - batch.add(vie); - } + List batch = toItemBatch(newId, vault.contents()); if (!batch.isEmpty()) { vaultService.putItems(newId, batch); } @@ -776,8 +782,8 @@ private static boolean isSignBlock(@NotNull Block block) { return block.getState() instanceof Sign; } - /** Unlocks and removes the given shop signs, notifying ChestShop. Must run on the main thread. */ - private void removeAttachedChestShopSigns(@NotNull Player actor, @NotNull List shopSigns) { + /** Unlocks and removes the given shop signs, notifying ChestShop. {@code actor} may be null for automated captures. Must run on the main thread. */ + private void removeAttachedChestShopSigns(@Nullable Player actor, @NotNull List shopSigns) { ChestShopService chestShop = VaultStoragePlugin.getInstance().getChestShopService(); BoltService bolt = VaultStoragePlugin.getInstance().getBoltService(); for (Block signBlock : shopSigns) { @@ -866,18 +872,7 @@ public void captureDirectAsync(@NotNull Player actor, @NotNull Block block, @Not "[VaultCaptureService] Direct vault created: ID=" + newId + " Owner=" + validatedOwner + " for player " + actor.getName() ); } - List items = vault.contents(); - List batch = new ArrayList<>(items.size()); - for (int idx = 0; idx < items.size(); idx++) { - ItemStack itemStack = items.get(idx); - if (itemStack == null) continue; - VaultItemEntity vie = new VaultItemEntity(); - vie.vaultUuid = newId; - vie.slot = idx; - vie.amount = itemStack.getAmount(); - vie.item = ItemSerialization.toBytes(itemStack); - batch.add(vie); - } + List batch = toItemBatch(newId, vault.contents()); if (!batch.isEmpty()) vaultService.putItems(newId, batch); new BukkitRunnable(){ @@ -895,4 +890,82 @@ public void captureDirectAsync(@NotNull Player actor, @NotNull Block block, @Not }.runTaskAsynchronously(plugin); }); } + + /** + * Offline-safe capture: no live {@link Player} and no policy evaluation; the caller decides what to vault. + * The vault is owned by {@code boltOwner} (falls back to {@code initiatorUuid} when null) and created by + * {@code initiatorUuid}. Must be called on the main thread; {@code onDoneMain} runs on the main thread + * with {@code true} when a vault was persisted, {@code false} otherwise. + */ + public void captureOfflineAsync(@NotNull Block block, + @NotNull UUID initiatorUuid, + UUID boltOwner, + @NotNull Consumer onDoneMain) { + var plugin = VaultStoragePlugin.getInstance(); + + CaptureOutcome outcome = captureWithDoubleChestSupport(block, boltOwner, initiatorUuid, null); + if (outcome.empty()) { + onDoneMain.accept(false); + return; + } + + // Drop any ChestShop sign left on the now-captured container. + ChestShopService chestShop = plugin.getChestShopService(); + if (chestShop != null && chestShop.isAvailable()) { + List shopSigns = chestShop.findShopSigns(block); + if (!shopSigns.isEmpty()) { + removeAttachedChestShopSigns(null, shopSigns); + } + } + + UUID finalOwner = outcome.finalOwner(); + if (!UniqueIdentifierResolver.isValidUUID(finalOwner)) { + plugin.getLogger().warning( + "[VaultCaptureService] Offline capture aborted at " + block.getWorld().getName() + ":" + + block.getX() + "," + block.getY() + "," + block.getZ() + + " - invalid vault owner UUID: " + finalOwner); + onDoneMain.accept(false); + return; + } + + VaultImp vault = outcome.vault(); + new BukkitRunnable() { + @Override public void run() { + VaultService vaultService = plugin.getVaultService(); + UUID worldId = block.getWorld().getUID(); + var created = vaultService.createVault(worldId, initiatorUuid, block.getX(), block.getY(), block.getZ(), finalOwner, + vault.blockMaterial() == null ? null : vault.blockMaterial().name(), + vault.blockDataString()); + UUID newId = created.uuid; + plugin.getLogger().info( + "[VaultCaptureService] Offline vault created: ID=" + newId + " owner=" + finalOwner + " initiator=" + initiatorUuid); + + List batch = toItemBatch(newId, vault.contents()); + if (!batch.isEmpty()) vaultService.putItems(newId, batch); + + new BukkitRunnable() { + @Override public void run() { + // No PlayerVaultEvent is fired: this is an automated capture with no player actor. + onDoneMain.accept(true); + } + }.runTask(plugin); + } + }.runTaskAsynchronously(plugin); + } + + /** Serializes a vault's contents into persistable item rows, skipping empty slots. */ + private static List toItemBatch(UUID vaultId, List items) { + List batch = new ArrayList<>(items.size()); + for (int idx = 0; idx < items.size(); idx++) { + ItemStack itemStack = items.get(idx); + if (itemStack == null) continue; + VaultItemEntity vie = new VaultItemEntity(); + vie.vaultUuid = vaultId; + vie.slot = idx; + vie.amount = itemStack.getAmount(); + vie.item = ItemSerialization.toBytes(itemStack); + batch.add(vie); + } + return batch; + } } \ No newline at end of file diff --git a/src/main/java/net/democracycraft/vault/internal/util/config/ConfigPaths.java b/src/main/java/net/democracycraft/vault/internal/util/config/ConfigPaths.java index 7f1e146..170459c 100644 --- a/src/main/java/net/democracycraft/vault/internal/util/config/ConfigPaths.java +++ b/src/main/java/net/democracycraft/vault/internal/util/config/ConfigPaths.java @@ -9,7 +9,9 @@ public enum ConfigPaths { MYSQL_USE_SSL("mysql.useSSL"), SCAN_BATCH_SIZE("scan.batch-size"), SCAN_CACHE_TTL_SECONDS("scan.cache-ttl-seconds"), - SCAN_COOLDOWN_SECONDS("scan.cooldown-seconds"); + SCAN_COOLDOWN_SECONDS("scan.cooldown-seconds"), + AUTOVAULT_ENABLED("auto-vault.enabled"), + AUTOVAULT_DELAY_TICKS("auto-vault.delay-ticks"); private final String path; diff --git a/src/main/java/net/democracycraft/vault/internal/util/region/RegionKey.java b/src/main/java/net/democracycraft/vault/internal/util/region/RegionKey.java new file mode 100644 index 0000000..5ca6911 --- /dev/null +++ b/src/main/java/net/democracycraft/vault/internal/util/region/RegionKey.java @@ -0,0 +1,17 @@ +package net.democracycraft.vault.internal.util.region; + +import org.jetbrains.annotations.NotNull; + +import java.util.Locale; +import java.util.UUID; + +/** + * Identity of a region within a world, used to dedupe and cancel pending auto-vault sweeps. + * The region id is normalized to lower-case to match WorldGuard's id convention. + */ +public record RegionKey(@NotNull UUID worldId, @NotNull String regionId) { + + public RegionKey { + regionId = regionId.toLowerCase(Locale.ROOT); + } +} diff --git a/src/main/java/net/democracycraft/vault/internal/util/scan/OfflineRegionScanner.java b/src/main/java/net/democracycraft/vault/internal/util/scan/OfflineRegionScanner.java new file mode 100644 index 0000000..f465b56 --- /dev/null +++ b/src/main/java/net/democracycraft/vault/internal/util/scan/OfflineRegionScanner.java @@ -0,0 +1,130 @@ +package net.democracycraft.vault.internal.util.scan; + +import net.democracycraft.vault.VaultStoragePlugin; +import net.democracycraft.vault.api.data.ScanResult; +import net.democracycraft.vault.api.service.BoltService; +import net.democracycraft.vault.internal.util.config.ConfigPaths; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; +import org.bukkit.util.BoundingBox; +import org.popcraft.bolt.protection.BlockProtection; +import org.popcraft.bolt.protection.Protection; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.function.Consumer; + +/** + * Player-free, batched scan of a region's Bolt-protected blocks. Collects every {@link BlockProtection} + * in the bounding box whose owner is non-null and not in {@code allowed}, delivering the result to + * {@code callback} on the main thread (or {@code null} if the world unloaded mid-scan). Batched per tick + * via {@code scan.batch-size} with a time budget and on-demand chunk loading. + */ +public class OfflineRegionScanner { + + private final UUID worldId; + private final BoundingBox boundingBox; + private final Set allowed; + private final Consumer> callback; + private BukkitTask task; + + public OfflineRegionScanner(UUID worldId, BoundingBox boundingBox, Set allowed, Consumer> callback) { + this.worldId = worldId; + this.boundingBox = boundingBox; + this.allowed = allowed; + this.callback = callback; + } + + public void start() { + World world = Bukkit.getWorld(worldId); + BoltService boltService = VaultStoragePlugin.getInstance().getBoltService(); + if (world == null || boltService == null) { + callback.accept(null); + return; + } + + Collection protections = boltService.getProtections(boundingBox, world); + if (protections == null || protections.isEmpty()) { + callback.accept(new ArrayList<>()); + return; + } + + List blockProtections = new ArrayList<>(); + for (Protection protection : protections) { + if (protection instanceof BlockProtection bp) { + blockProtections.add(bp); + } + } + + this.task = new ScanTask(blockProtections).runTaskTimer(VaultStoragePlugin.getInstance(), 1L, 1L); + } + + public void cancel() { + if (this.task != null && !this.task.isCancelled()) { + this.task.cancel(); + } + } + + private class ScanTask extends BukkitRunnable { + private final List protections; + private final List results = new ArrayList<>(); + private int index = 0; + private final int batchSize; + + private ScanTask(List protections) { + this.protections = protections; + this.batchSize = VaultStoragePlugin.getInstance().getConfig().getInt(ConfigPaths.SCAN_BATCH_SIZE.getPath(), 50); + } + + @Override + public void run() { + World world = Bukkit.getWorld(worldId); + if (world == null) { + this.cancel(); + callback.accept(null); + return; + } + + long startTime = System.currentTimeMillis(); + int processed = 0; + + while (index < protections.size() && processed < batchSize) { + if (System.currentTimeMillis() - startTime > 2) { + break; + } + + BlockProtection bp = protections.get(index); + index++; + processed++; + + int x = bp.getX(); + int y = bp.getY(); + int z = bp.getZ(); + + if (y < world.getMinHeight() || y >= world.getMaxHeight()) continue; + + UUID owner = bp.getOwner(); + if (owner == null || allowed.contains(owner)) continue; + + // Load chunk if needed + if (!world.isChunkLoaded(x >> 4, z >> 4)) { + world.getChunkAt(x >> 4, z >> 4); + } + + Block block = world.getBlockAt(x, y, z); + results.add(new ScanResult(block, owner, block.getType())); + } + + if (index >= protections.size()) { + this.cancel(); + callback.accept(results); + } + } + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 05f52ce..79f22c2 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -9,3 +9,13 @@ mysql: password: "" useSSL: false +# Automatically vault Bolt-locked containers when a Realty region changes occupant +# (bought/transferred or rented). A container is vaulted when its Bolt owner is not the +# new occupant and is not a WorldGuard owner/member of the region. Requires the Realty plugin. +auto-vault: + # Master switch. When false, no Realty listener is registered. + enabled: true + # Grace period (in server ticks, 20 = 1 second) between the occupant change and the sweep. + # If the region changes hands again within this window, the pending sweep is replaced. + delay-ticks: 1200 + diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index fc8145b..e0be6ff 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -4,7 +4,7 @@ main: net.democracycraft.vault.VaultStoragePlugin api-version: '1.21' authors: [ Alepando ] depend: [ Bolt , WorldGuard, WorldEdit, Essentials ] -softdepend: [ ChestShop ] +softdepend: [ ChestShop, Realty ] commands: vault: From d8b011907c42dcc6c2fa3dd4b664e0aefca8b98f Mon Sep 17 00:00:00 2001 From: Technofied <40795318+Technofied@users.noreply.github.com> Date: Sat, 27 Jun 2026 21:52:16 +0800 Subject: [PATCH 2/7] remove accidentally included bin/ files --- bin/main/config.yml | 21 --------------------- bin/main/plugin.yml | 39 --------------------------------------- 2 files changed, 60 deletions(-) delete mode 100644 bin/main/config.yml delete mode 100644 bin/main/plugin.yml diff --git a/bin/main/config.yml b/bin/main/config.yml deleted file mode 100644 index 79f22c2..0000000 --- a/bin/main/config.yml +++ /dev/null @@ -1,21 +0,0 @@ -# Default configuration for VaultStorage -# This file is copied to the plugin data folder on first run. - -mysql: - host: "localhost" - port: 3306 - database: "vault_storage" - user: "root" - password: "" - useSSL: false - -# Automatically vault Bolt-locked containers when a Realty region changes occupant -# (bought/transferred or rented). A container is vaulted when its Bolt owner is not the -# new occupant and is not a WorldGuard owner/member of the region. Requires the Realty plugin. -auto-vault: - # Master switch. When false, no Realty listener is registered. - enabled: true - # Grace period (in server ticks, 20 = 1 second) between the occupant change and the sweep. - # If the region changes hands again within this window, the pending sweep is replaced. - delay-ticks: 1200 - diff --git a/bin/main/plugin.yml b/bin/main/plugin.yml deleted file mode 100644 index e0be6ff..0000000 --- a/bin/main/plugin.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: VaultStorage -version: '1.0.15' -main: net.democracycraft.vault.VaultStoragePlugin -api-version: '1.21' -authors: [ Alepando ] -depend: [ Bolt , WorldGuard, WorldEdit, Essentials ] -softdepend: [ ChestShop, Realty ] - -commands: - vault: - description: Vault storage tools - usage: /vault [menu] - permission: vaultstorage.user - -permissions: - vaultstorage.user: - description: Allows using basic vault commands - default: op - vaultstorage.admin: - description: Admin override for vault commands - default: op - vaultstorage.action.view: - description: Allows viewing vaults in a virtual inventory - default: true - vaultstorage.action.edit: - description: Allows editing vault contents in a virtual inventory - default: op - vaultstorage.action.copy: - description: Allows copying items from a vault to the player inventory - default: op - vaultstorage.action.place: - description: Allows placing back the original block and restoring its contents - default: op - vaultstorage.admin.override: - description: Allows capturing/placing vault blocks even if not a region member or outside regions - default: false - vaultstorage.action.capture: - description: Allows capturing a container into a vault (requires region ownership & container ownership unless override) - default: op From 12e39363c15aca462b8d472d912e27b6ae13acd9 Mon Sep 17 00:00:00 2001 From: Technofied <40795318+Technofied@users.noreply.github.com> Date: Sat, 27 Jun 2026 22:30:31 +0800 Subject: [PATCH 3/7] fixes to sync chunk loading --- .../internal/service/AutoVaultService.java | 58 ++++----- .../util/scan/OfflineRegionScanner.java | 114 +++--------------- 2 files changed, 41 insertions(+), 131 deletions(-) diff --git a/src/main/java/net/democracycraft/vault/internal/service/AutoVaultService.java b/src/main/java/net/democracycraft/vault/internal/service/AutoVaultService.java index 2f7e892..88b91b1 100644 --- a/src/main/java/net/democracycraft/vault/internal/service/AutoVaultService.java +++ b/src/main/java/net/democracycraft/vault/internal/service/AutoVaultService.java @@ -1,16 +1,14 @@ package net.democracycraft.vault.internal.service; import net.democracycraft.vault.VaultStoragePlugin; -import net.democracycraft.vault.api.data.ScanResult; import net.democracycraft.vault.api.region.VaultRegion; import net.democracycraft.vault.api.service.WorldGuardService; import net.democracycraft.vault.internal.util.config.ConfigPaths; import net.democracycraft.vault.internal.util.region.RegionKey; import net.democracycraft.vault.internal.util.scan.OfflineRegionScanner; +import net.democracycraft.vault.internal.util.scan.OfflineRegionScanner.DisplacedContainer; import org.bukkit.Bukkit; -import org.bukkit.Location; import org.bukkit.World; -import org.bukkit.block.Block; import org.bukkit.scheduler.BukkitRunnable; import org.bukkit.scheduler.BukkitTask; import org.jetbrains.annotations.NotNull; @@ -88,48 +86,40 @@ private void runSweep(@NotNull RegionKey key, @NotNull UUID initiator) { allowed.addAll(region.members()); allowed.add(initiator); - new OfflineRegionScanner(key.worldId(), region.boundingBox(), allowed, results -> { - if (results == null || results.isEmpty()) { - return; - } - vaultBatched(results, initiator); - }).start(); + List displaced = OfflineRegionScanner.scan(key.worldId(), region.boundingBox(), allowed); + if (displaced == null || displaced.isEmpty()) { + return; + } + vaultBatched(key.worldId(), displaced, initiator); } - private void vaultBatched(@NotNull List results, @NotNull UUID initiator) { + private void vaultBatched(@NotNull UUID worldId, @NotNull List displaced, @NotNull UUID initiator) { VaultCaptureService captureService = plugin.getCaptureService(); - final int batchSize = plugin.getConfig().getInt(ConfigPaths.SCAN_BATCH_SIZE.getPath(), 50); + final int perTick = Math.max(1, plugin.getConfig().getInt(ConfigPaths.SCAN_BATCH_SIZE.getPath(), 50)); new BukkitRunnable() { int index = 0; - final Set processed = new HashSet<>(); @Override public void run() { - long startTime = System.currentTimeMillis(); - int handled = 0; - - while (index < results.size() && handled < batchSize) { - if (System.currentTimeMillis() - startTime > 2) { - break; - } - ScanResult result = results.get(index); + World world = Bukkit.getWorld(worldId); + if (world == null) { + this.cancel(); + return; + } + + // Launch up to perTick async chunk loads this tick; each capture runs in its completion + // callback (back on the main thread) so the main thread never blocks on chunk I/O. + int launched = 0; + while (index < displaced.size() && launched < perTick) { + DisplacedContainer dc = displaced.get(index); index++; - handled++; - - Block block = result.block(); - Location loc = block.getLocation(); - // Skip blocks already consumed this run (e.g. the far half of a double chest). - if (!processed.add(loc)) continue; - - World world = block.getWorld(); - if (!world.isChunkLoaded(loc.getBlockX() >> 4, loc.getBlockZ() >> 4)) { - world.getChunkAt(loc.getBlockX() >> 4, loc.getBlockZ() >> 4); - } - // Tolerant of blocks changed since the scan (non-containers just drop their protection). - captureService.captureOfflineAsync(block, initiator, result.owner(), done -> {}); + launched++; + world.getChunkAtAsync(dc.x() >> 4, dc.z() >> 4).thenAccept(chunk -> + // Tolerant of blocks changed since the scan (non-containers just drop their protection). + captureService.captureOfflineAsync(world.getBlockAt(dc.x(), dc.y(), dc.z()), initiator, dc.owner(), done -> {})); } - if (index >= results.size()) { + if (index >= displaced.size()) { this.cancel(); } } diff --git a/src/main/java/net/democracycraft/vault/internal/util/scan/OfflineRegionScanner.java b/src/main/java/net/democracycraft/vault/internal/util/scan/OfflineRegionScanner.java index f465b56..b124880 100644 --- a/src/main/java/net/democracycraft/vault/internal/util/scan/OfflineRegionScanner.java +++ b/src/main/java/net/democracycraft/vault/internal/util/scan/OfflineRegionScanner.java @@ -1,14 +1,9 @@ package net.democracycraft.vault.internal.util.scan; import net.democracycraft.vault.VaultStoragePlugin; -import net.democracycraft.vault.api.data.ScanResult; import net.democracycraft.vault.api.service.BoltService; -import net.democracycraft.vault.internal.util.config.ConfigPaths; import org.bukkit.Bukkit; import org.bukkit.World; -import org.bukkit.block.Block; -import org.bukkit.scheduler.BukkitRunnable; -import org.bukkit.scheduler.BukkitTask; import org.bukkit.util.BoundingBox; import org.popcraft.bolt.protection.BlockProtection; import org.popcraft.bolt.protection.Protection; @@ -18,113 +13,38 @@ import java.util.List; import java.util.Set; import java.util.UUID; -import java.util.function.Consumer; /** - * Player-free, batched scan of a region's Bolt-protected blocks. Collects every {@link BlockProtection} - * in the bounding box whose owner is non-null and not in {@code allowed}, delivering the result to - * {@code callback} on the main thread (or {@code null} if the world unloaded mid-scan). Batched per tick - * via {@code scan.batch-size} with a time budget and on-demand chunk loading. + * In-memory filter over a region's Bolt protections. Returns the locked blocks whose owner is non-null + * and not in {@code allowed}, using only Bolt's stored coordinates/owner — no block reads or chunk loads, + * so it never blocks the main thread. Callers load chunks asynchronously when vaulting the results. */ -public class OfflineRegionScanner { +public final class OfflineRegionScanner { - private final UUID worldId; - private final BoundingBox boundingBox; - private final Set allowed; - private final Consumer> callback; - private BukkitTask task; + private OfflineRegionScanner() {} - public OfflineRegionScanner(UUID worldId, BoundingBox boundingBox, Set allowed, Consumer> callback) { - this.worldId = worldId; - this.boundingBox = boundingBox; - this.allowed = allowed; - this.callback = callback; - } + /** A locked container flagged for vaulting: its coordinates and Bolt owner. */ + public record DisplacedContainer(int x, int y, int z, UUID owner) {} - public void start() { + /** Filters the region's Bolt protections to displaced containers, or {@code null} if services are unavailable. */ + public static List scan(UUID worldId, BoundingBox boundingBox, Set allowed) { World world = Bukkit.getWorld(worldId); BoltService boltService = VaultStoragePlugin.getInstance().getBoltService(); if (world == null || boltService == null) { - callback.accept(null); - return; + return null; } Collection protections = boltService.getProtections(boundingBox, world); - if (protections == null || protections.isEmpty()) { - callback.accept(new ArrayList<>()); - return; - } - - List blockProtections = new ArrayList<>(); - for (Protection protection : protections) { - if (protection instanceof BlockProtection bp) { - blockProtections.add(bp); - } - } - - this.task = new ScanTask(blockProtections).runTaskTimer(VaultStoragePlugin.getInstance(), 1L, 1L); - } - - public void cancel() { - if (this.task != null && !this.task.isCancelled()) { - this.task.cancel(); - } - } - - private class ScanTask extends BukkitRunnable { - private final List protections; - private final List results = new ArrayList<>(); - private int index = 0; - private final int batchSize; - - private ScanTask(List protections) { - this.protections = protections; - this.batchSize = VaultStoragePlugin.getInstance().getConfig().getInt(ConfigPaths.SCAN_BATCH_SIZE.getPath(), 50); - } - - @Override - public void run() { - World world = Bukkit.getWorld(worldId); - if (world == null) { - this.cancel(); - callback.accept(null); - return; - } - - long startTime = System.currentTimeMillis(); - int processed = 0; - - while (index < protections.size() && processed < batchSize) { - if (System.currentTimeMillis() - startTime > 2) { - break; - } - - BlockProtection bp = protections.get(index); - index++; - processed++; - - int x = bp.getX(); - int y = bp.getY(); - int z = bp.getZ(); - - if (y < world.getMinHeight() || y >= world.getMaxHeight()) continue; - + List displaced = new ArrayList<>(); + if (protections != null) { + for (Protection protection : protections) { + if (!(protection instanceof BlockProtection bp)) continue; + if (bp.getY() < world.getMinHeight() || bp.getY() >= world.getMaxHeight()) continue; UUID owner = bp.getOwner(); if (owner == null || allowed.contains(owner)) continue; - - // Load chunk if needed - if (!world.isChunkLoaded(x >> 4, z >> 4)) { - world.getChunkAt(x >> 4, z >> 4); - } - - Block block = world.getBlockAt(x, y, z); - results.add(new ScanResult(block, owner, block.getType())); - } - - if (index >= protections.size()) { - this.cancel(); - callback.accept(results); + displaced.add(new DisplacedContainer(bp.getX(), bp.getY(), bp.getZ(), owner)); } } + return displaced; } } From d551b05139be4a5072c26136a381e5426d5a84c2 Mon Sep 17 00:00:00 2001 From: Technofied <40795318+Technofied@users.noreply.github.com> Date: Sat, 27 Jun 2026 23:14:48 +0800 Subject: [PATCH 4/7] fixes to auto vaulting for efficiency and simplicity --- .../internal/service/AutoVaultService.java | 37 ++++++++----------- .../internal/service/VaultCaptureService.java | 16 ++------ src/main/resources/config.yml | 2 - 3 files changed, 19 insertions(+), 36 deletions(-) diff --git a/src/main/java/net/democracycraft/vault/internal/service/AutoVaultService.java b/src/main/java/net/democracycraft/vault/internal/service/AutoVaultService.java index 88b91b1..b03f94c 100644 --- a/src/main/java/net/democracycraft/vault/internal/service/AutoVaultService.java +++ b/src/main/java/net/democracycraft/vault/internal/service/AutoVaultService.java @@ -13,7 +13,6 @@ import org.bukkit.scheduler.BukkitTask; import org.jetbrains.annotations.NotNull; -import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -51,11 +50,6 @@ public void submit(@NotNull UUID worldId, @NotNull String regionId, @NotNull UUI RegionKey key = new RegionKey(worldId, regionId); long delayTicks = Math.max(0L, plugin.getConfig().getLong(ConfigPaths.AUTOVAULT_DELAY_TICKS.getPath(), 1200L)); - BukkitTask previous = pending.remove(key); - if (previous != null && !previous.isCancelled()) { - previous.cancel(); - } - BukkitTask task = new BukkitRunnable() { @Override public void run() { // A fired task is always the current pending entry (a resubmit cancels the prior one). @@ -63,7 +57,10 @@ public void submit(@NotNull UUID worldId, @NotNull String regionId, @NotNull UUI runSweep(key, initiator); } }.runTaskLater(plugin, delayTicks); - pending.put(key, task); + BukkitTask previous = pending.put(key, task); + if (previous != null) { + previous.cancel(); + } } private void runSweep(@NotNull RegionKey key, @NotNull UUID initiator) { @@ -95,10 +92,11 @@ private void runSweep(@NotNull RegionKey key, @NotNull UUID initiator) { private void vaultBatched(@NotNull UUID worldId, @NotNull List displaced, @NotNull UUID initiator) { VaultCaptureService captureService = plugin.getCaptureService(); - final int perTick = Math.max(1, plugin.getConfig().getInt(ConfigPaths.SCAN_BATCH_SIZE.getPath(), 50)); + final int maxInFlight = Math.max(1, plugin.getConfig().getInt(ConfigPaths.SCAN_BATCH_SIZE.getPath(), 50)); new BukkitRunnable() { int index = 0; + int inFlight = 0; // pending async chunk loads; mutated only on the main thread @Override public void run() { World world = Bukkit.getWorld(worldId); @@ -107,19 +105,20 @@ private void vaultBatched(@NotNull UUID worldId, @NotNull List> 4, dc.z() >> 4).thenAccept(chunk -> - // Tolerant of blocks changed since the scan (non-containers just drop their protection). - captureService.captureOfflineAsync(world.getBlockAt(dc.x(), dc.y(), dc.z()), initiator, dc.owner(), done -> {})); + inFlight++; + world.getChunkAtAsync(dc.x() >> 4, dc.z() >> 4).thenAccept(chunk -> { + inFlight--; + // Tolerant of blocks changed since the scan (non-containers just drop their protection). + captureService.captureOfflineAsync(world.getBlockAt(dc.x(), dc.y(), dc.z()), initiator, dc.owner()); + }); } - if (index >= displaced.size()) { + if (index >= displaced.size() && inFlight == 0) { this.cancel(); } } @@ -128,11 +127,7 @@ private void vaultBatched(@NotNull UUID worldId, @NotNull List(pending.values())) { - if (task != null && !task.isCancelled()) { - task.cancel(); - } - } + pending.values().forEach(BukkitTask::cancel); pending.clear(); } } diff --git a/src/main/java/net/democracycraft/vault/internal/service/VaultCaptureService.java b/src/main/java/net/democracycraft/vault/internal/service/VaultCaptureService.java index d832292..dec60ac 100644 --- a/src/main/java/net/democracycraft/vault/internal/service/VaultCaptureService.java +++ b/src/main/java/net/democracycraft/vault/internal/service/VaultCaptureService.java @@ -894,18 +894,15 @@ public void captureDirectAsync(@NotNull Player actor, @NotNull Block block, @Not /** * Offline-safe capture: no live {@link Player} and no policy evaluation; the caller decides what to vault. * The vault is owned by {@code boltOwner} (falls back to {@code initiatorUuid} when null) and created by - * {@code initiatorUuid}. Must be called on the main thread; {@code onDoneMain} runs on the main thread - * with {@code true} when a vault was persisted, {@code false} otherwise. + * {@code initiatorUuid}. Must be called on the main thread; persistence runs async (fire-and-forget). */ public void captureOfflineAsync(@NotNull Block block, @NotNull UUID initiatorUuid, - UUID boltOwner, - @NotNull Consumer onDoneMain) { + UUID boltOwner) { var plugin = VaultStoragePlugin.getInstance(); CaptureOutcome outcome = captureWithDoubleChestSupport(block, boltOwner, initiatorUuid, null); if (outcome.empty()) { - onDoneMain.accept(false); return; } @@ -924,7 +921,6 @@ public void captureOfflineAsync(@NotNull Block block, "[VaultCaptureService] Offline capture aborted at " + block.getWorld().getName() + ":" + block.getX() + "," + block.getY() + "," + block.getZ() + " - invalid vault owner UUID: " + finalOwner); - onDoneMain.accept(false); return; } @@ -942,13 +938,7 @@ public void captureOfflineAsync(@NotNull Block block, List batch = toItemBatch(newId, vault.contents()); if (!batch.isEmpty()) vaultService.putItems(newId, batch); - - new BukkitRunnable() { - @Override public void run() { - // No PlayerVaultEvent is fired: this is an automated capture with no player actor. - onDoneMain.accept(true); - } - }.runTask(plugin); + // No PlayerVaultEvent and no completion callback: this is an automated, fire-and-forget capture. } }.runTaskAsynchronously(plugin); } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 79f22c2..87f7e16 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -13,9 +13,7 @@ mysql: # (bought/transferred or rented). A container is vaulted when its Bolt owner is not the # new occupant and is not a WorldGuard owner/member of the region. Requires the Realty plugin. auto-vault: - # Master switch. When false, no Realty listener is registered. enabled: true # Grace period (in server ticks, 20 = 1 second) between the occupant change and the sweep. - # If the region changes hands again within this window, the pending sweep is replaced. delay-ticks: 1200 From efcf426840156ccdfd5a4ebbbcbed96fa2c8bf09 Mon Sep 17 00:00:00 2001 From: Technofied <40795318+Technofied@users.noreply.github.com> Date: Sun, 28 Jun 2026 10:20:29 +0800 Subject: [PATCH 5/7] add support for hanging entities --- .../internal/service/AutoVaultService.java | 93 ++++++++++++--- .../internal/service/VaultCaptureService.java | 106 +++++++++++------- .../internal/util/config/ConfigPaths.java | 3 +- src/main/resources/config.yml | 4 + 4 files changed, 145 insertions(+), 61 deletions(-) diff --git a/src/main/java/net/democracycraft/vault/internal/service/AutoVaultService.java b/src/main/java/net/democracycraft/vault/internal/service/AutoVaultService.java index b03f94c..7d96bed 100644 --- a/src/main/java/net/democracycraft/vault/internal/service/AutoVaultService.java +++ b/src/main/java/net/democracycraft/vault/internal/service/AutoVaultService.java @@ -2,28 +2,38 @@ import net.democracycraft.vault.VaultStoragePlugin; import net.democracycraft.vault.api.region.VaultRegion; +import net.democracycraft.vault.api.service.BoltService; import net.democracycraft.vault.api.service.WorldGuardService; import net.democracycraft.vault.internal.util.config.ConfigPaths; import net.democracycraft.vault.internal.util.region.RegionKey; import net.democracycraft.vault.internal.util.scan.OfflineRegionScanner; import net.democracycraft.vault.internal.util.scan.OfflineRegionScanner.DisplacedContainer; import org.bukkit.Bukkit; +import org.bukkit.Chunk; import org.bukkit.World; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Hanging; +import org.bukkit.entity.ItemFrame; +import org.bukkit.entity.Painting; import org.bukkit.scheduler.BukkitRunnable; import org.bukkit.scheduler.BukkitTask; +import org.bukkit.util.BoundingBox; import org.jetbrains.annotations.NotNull; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.function.Function; /** - * Schedules and runs the delayed, batched auto-vaulting of Bolt-locked containers when a region's - * owner/tenant changes. A container is vaulted when its Bolt owner is not in the allowed set - * ({new occupant} plus the region's WorldGuard owners/members). Runs on the main thread. + * Schedules and runs the delayed, batched auto-vaulting of Bolt-locked containers (and, when enabled, + * item frames and paintings) when a region's owner/tenant changes. A lock is vaulted when its Bolt owner + * is not in the allowed set ({new occupant} plus the region's WorldGuard owners/members). Runs on the + * main thread; chunk loads are async. */ public class AutoVaultService { @@ -84,14 +94,62 @@ private void runSweep(@NotNull RegionKey key, @NotNull UUID initiator) { allowed.add(initiator); List displaced = OfflineRegionScanner.scan(key.worldId(), region.boundingBox(), allowed); - if (displaced == null || displaced.isEmpty()) { - return; + if (displaced != null && !displaced.isEmpty()) { + vaultBlocks(key.worldId(), displaced, initiator); + } + if (plugin.getConfig().getBoolean(ConfigPaths.AUTOVAULT_INCLUDE_HANGINGS.getPath(), true)) { + vaultHangings(key.worldId(), region.boundingBox(), allowed, initiator); } - vaultBatched(key.worldId(), displaced, initiator); } - private void vaultBatched(@NotNull UUID worldId, @NotNull List displaced, @NotNull UUID initiator) { + /** Vaults each displaced container, loading its chunk asynchronously. */ + private void vaultBlocks(@NotNull UUID worldId, @NotNull List displaced, @NotNull UUID initiator) { + VaultCaptureService captureService = plugin.getCaptureService(); + forEachChunkPaced(worldId, displaced, + dc -> new int[]{dc.x() >> 4, dc.z() >> 4}, + (world, chunk, dc) -> + // Tolerant of blocks changed since the scan (non-containers just drop their protection). + captureService.captureOfflineAsync(world.getBlockAt(dc.x(), dc.y(), dc.z()), initiator, dc.owner())); + } + + /** + * Vaults displaced item frames and paintings across the region. Entity protections are not indexed + * by coordinate, so every chunk in the region's bounds is loaded and its hanging entities inspected. + */ + private void vaultHangings(@NotNull UUID worldId, @NotNull BoundingBox box, @NotNull Set allowed, @NotNull UUID initiator) { VaultCaptureService captureService = plugin.getCaptureService(); + BoltService bolt = plugin.getBoltService(); + + List chunks = new ArrayList<>(); + int minChunkX = (int) Math.floor(box.getMinX()) >> 4; + int minChunkZ = (int) Math.floor(box.getMinZ()) >> 4; + int maxChunkX = (int) Math.floor(box.getMaxX()) >> 4; + int maxChunkZ = (int) Math.floor(box.getMaxZ()) >> 4; + for (int cx = minChunkX; cx <= maxChunkX; cx++) { + for (int cz = minChunkZ; cz <= maxChunkZ; cz++) { + chunks.add(new int[]{cx, cz}); + } + } + + forEachChunkPaced(worldId, chunks, + coords -> coords, + (world, chunk, coords) -> { + for (Entity entity : chunk.getEntities()) { + if (!(entity instanceof ItemFrame) && !(entity instanceof Painting)) continue; + UUID owner = bolt.getOwner(entity); + if (owner == null || allowed.contains(owner)) continue; + captureService.captureHangingOfflineAsync((Hanging) entity, owner, initiator); + } + }); + } + + /** + * Runs {@code work} for each item, loading the chunk given by {@code chunkOf} asynchronously and keeping + * at most {@code scan.batch-size} loads in flight. {@code work} runs on the main thread once that chunk is + * loaded. Stops if the world unloads. + */ + private void forEachChunkPaced(@NotNull UUID worldId, @NotNull List items, + @NotNull Function chunkOf, @NotNull ChunkWork work) { final int maxInFlight = Math.max(1, plugin.getConfig().getInt(ConfigPaths.SCAN_BATCH_SIZE.getPath(), 50)); new BukkitRunnable() { @@ -104,27 +162,28 @@ private void vaultBatched(@NotNull UUID worldId, @NotNull List> 4, dc.z() >> 4).thenAccept(chunk -> { + world.getChunkAtAsync(chunkCoords[0], chunkCoords[1]).thenAccept(chunk -> { inFlight--; - // Tolerant of blocks changed since the scan (non-containers just drop their protection). - captureService.captureOfflineAsync(world.getBlockAt(dc.x(), dc.y(), dc.z()), initiator, dc.owner()); + work.run(world, chunk, item); }); } - - if (index >= displaced.size() && inFlight == 0) { + if (index >= items.size() && inFlight == 0) { this.cancel(); } } }.runTaskTimer(plugin, 1L, 1L); } + @FunctionalInterface + private interface ChunkWork { + void run(World world, Chunk chunk, T item); + } + /** Cancels all pending sweeps. Call from {@code onDisable}. */ public void shutdown() { pending.values().forEach(BukkitTask::cancel); diff --git a/src/main/java/net/democracycraft/vault/internal/service/VaultCaptureService.java b/src/main/java/net/democracycraft/vault/internal/service/VaultCaptureService.java index dec60ac..74a988b 100644 --- a/src/main/java/net/democracycraft/vault/internal/service/VaultCaptureService.java +++ b/src/main/java/net/democracycraft/vault/internal/service/VaultCaptureService.java @@ -618,45 +618,7 @@ public void onInteractEntity(PlayerInteractEntityEvent event) { new BukkitRunnable() { @Override public void run() { var plugin = VaultStoragePlugin.getInstance(); - VaultService vaultService = plugin.getVaultService(); - int needed = stacks.size(); - var targetOpt = HangingVaultSupport.findFirstVaultWithSpace(vaultService, validatedOwner, needed); - UUID vaultUuid; - int startSlot; - if (targetOpt.isPresent()) { - var t = targetOpt.get(); - vaultUuid = t.vaultUuid(); - startSlot = t.startSlot(); - } else { - // Note: persisted material must be a block; placement uses Block#setType (item names are invalid). - var created = vaultService.createVault( - supporting.getWorld().getUID(), - actor.getUniqueId(), - supporting.getX(), - supporting.getY(), - supporting.getZ(), - validatedOwner, - Material.CHEST.name(), - null - ); - vaultUuid = created.uuid; - startSlot = 0; - plugin.getLogger().info( - "[VaultCaptureService] Hanging capture created vault ID=" + vaultUuid + " owner=" + validatedOwner - ); - } - - List batch = new ArrayList<>(stacks.size()); - int slot = startSlot; - for (ItemStack stack : stacks) { - VaultItemEntity vie = new VaultItemEntity(); - vie.vaultUuid = vaultUuid; - vie.slot = slot++; - vie.amount = stack.getAmount(); - vie.item = ItemSerialization.toBytes(stack); - batch.add(vie); - } - vaultService.putItems(vaultUuid, batch); + UUID vaultUuid = persistHangingStacks(stacks, validatedOwner, actor.getUniqueId(), supporting); new BukkitRunnable() { @Override public void run() { @@ -943,15 +905,73 @@ public void captureOfflineAsync(@NotNull Block block, }.runTaskAsynchronously(plugin); } - /** Serializes a vault's contents into persistable item rows, skipping empty slots. */ + /** + * Offline-safe capture of a Bolt-locked hanging entity (item frame or painting) with no live + * {@link Player} and no policy evaluation. Its contents (the frame/painting plus any displayed item) + * are stored in a vault owned by {@code boltOwner}, merged into an existing vault when one has room, + * and the entity is removed (which also disposes its Bolt protection). + *

Must be called on the main thread with the entity loaded; persistence runs async.

+ */ + public void captureHangingOfflineAsync(@NotNull Hanging hang, @NotNull UUID boltOwner, @NotNull UUID initiatorUuid) { + List stacks = HangingVaultSupport.itemStacksFrom(hang); + if (stacks.isEmpty()) { + return; + } + + var plugin = VaultStoragePlugin.getInstance(); + Block supporting = HangingVaultSupport.resolveSupportingBlock(hang); + new BukkitRunnable() { + @Override public void run() { + persistHangingStacks(stacks, boltOwner, initiatorUuid, supporting); + new BukkitRunnable() { + @Override public void run() { + if (hang.isValid()) hang.remove(); + } + }.runTask(plugin); + } + }.runTaskAsynchronously(plugin); + } + + /** + * Stores hanging stacks into an existing vault owned by {@code boltOwner} that has room, or a new vault + * anchored at {@code supporting} and created by {@code creatorUuid}. Runs on the async persistence thread; + * returns the target vault id. + */ + private UUID persistHangingStacks(@NotNull List stacks, @NotNull UUID boltOwner, @NotNull UUID creatorUuid, @NotNull Block supporting) { + var plugin = VaultStoragePlugin.getInstance(); + VaultService vaultService = plugin.getVaultService(); + var target = HangingVaultSupport.findFirstVaultWithSpace(vaultService, boltOwner, stacks.size()); + UUID vaultUuid; + int startSlot; + if (target.isPresent()) { + vaultUuid = target.get().vaultUuid(); + startSlot = target.get().startSlot(); + } else { + // Persisted material must be a block; placement uses Block#setType (item names are invalid). + var created = vaultService.createVault(supporting.getWorld().getUID(), creatorUuid, + supporting.getX(), supporting.getY(), supporting.getZ(), boltOwner, Material.CHEST.name(), null); + vaultUuid = created.uuid; + startSlot = 0; + plugin.getLogger().info("[VaultCaptureService] Hanging capture created vault ID=" + vaultUuid + " owner=" + boltOwner); + } + vaultService.putItems(vaultUuid, toItemBatch(vaultUuid, stacks, startSlot)); + return vaultUuid; + } + + /** Serializes a vault's contents into persistable item rows starting at slot 0, skipping empty slots. */ private static List toItemBatch(UUID vaultId, List items) { + return toItemBatch(vaultId, items, 0); + } + + /** Serializes items into persistable rows assigned consecutive slots from {@code startSlot}, skipping empty slots. */ + private static List toItemBatch(UUID vaultId, List items, int startSlot) { List batch = new ArrayList<>(items.size()); - for (int idx = 0; idx < items.size(); idx++) { - ItemStack itemStack = items.get(idx); + int slot = startSlot; + for (ItemStack itemStack : items) { if (itemStack == null) continue; VaultItemEntity vie = new VaultItemEntity(); vie.vaultUuid = vaultId; - vie.slot = idx; + vie.slot = slot++; vie.amount = itemStack.getAmount(); vie.item = ItemSerialization.toBytes(itemStack); batch.add(vie); diff --git a/src/main/java/net/democracycraft/vault/internal/util/config/ConfigPaths.java b/src/main/java/net/democracycraft/vault/internal/util/config/ConfigPaths.java index 170459c..24a00de 100644 --- a/src/main/java/net/democracycraft/vault/internal/util/config/ConfigPaths.java +++ b/src/main/java/net/democracycraft/vault/internal/util/config/ConfigPaths.java @@ -11,7 +11,8 @@ public enum ConfigPaths { SCAN_CACHE_TTL_SECONDS("scan.cache-ttl-seconds"), SCAN_COOLDOWN_SECONDS("scan.cooldown-seconds"), AUTOVAULT_ENABLED("auto-vault.enabled"), - AUTOVAULT_DELAY_TICKS("auto-vault.delay-ticks"); + AUTOVAULT_DELAY_TICKS("auto-vault.delay-ticks"), + AUTOVAULT_INCLUDE_HANGINGS("auto-vault.include-hangings"); private final String path; diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 87f7e16..c370772 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -16,4 +16,8 @@ auto-vault: enabled: true # Grace period (in server ticks, 20 = 1 second) between the occupant change and the sweep. delay-ticks: 1200 + # Also vault displaced Bolt-locked item frames and paintings. Finding hanging entities requires + # loading every chunk in the region (they are not indexed by coordinate like blocks), so disable + # this for very large claims if the extra chunk I/O is a concern. + include-hangings: true From 6c2f6c0ef899a58ccde66245ff0dc1fca4d5a179 Mon Sep 17 00:00:00 2001 From: Technofied <40795318+Technofied@users.noreply.github.com> Date: Sun, 28 Jun 2026 10:21:00 +0800 Subject: [PATCH 6/7] simplify comment in config.yml --- src/main/resources/config.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index c370772..609eb60 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -16,8 +16,6 @@ auto-vault: enabled: true # Grace period (in server ticks, 20 = 1 second) between the occupant change and the sweep. delay-ticks: 1200 - # Also vault displaced Bolt-locked item frames and paintings. Finding hanging entities requires - # loading every chunk in the region (they are not indexed by coordinate like blocks), so disable - # this for very large claims if the extra chunk I/O is a concern. + # Vaukt Bolt-locked item frames and paintings. include-hangings: true From c0d8484dc2712a76afc09a3b0ac8b42208767bbd Mon Sep 17 00:00:00 2001 From: Technofied <40795318+Technofied@users.noreply.github.com> Date: Tue, 30 Jun 2026 22:28:39 +0800 Subject: [PATCH 7/7] notify new occupant of region that previous occupant's belongings have been vaulted --- .../internal/service/AutoVaultService.java | 49 ++++++++++++++++--- .../internal/service/VaultCaptureService.java | 10 ++-- .../internal/util/config/ConfigPaths.java | 4 +- src/main/resources/config.yml | 4 ++ 4 files changed, 57 insertions(+), 10 deletions(-) diff --git a/src/main/java/net/democracycraft/vault/internal/service/AutoVaultService.java b/src/main/java/net/democracycraft/vault/internal/service/AutoVaultService.java index 7d96bed..ddcafb9 100644 --- a/src/main/java/net/democracycraft/vault/internal/service/AutoVaultService.java +++ b/src/main/java/net/democracycraft/vault/internal/service/AutoVaultService.java @@ -5,6 +5,7 @@ import net.democracycraft.vault.api.service.BoltService; import net.democracycraft.vault.api.service.WorldGuardService; import net.democracycraft.vault.internal.util.config.ConfigPaths; +import net.democracycraft.vault.internal.util.minimessage.MiniMessageUtil; import net.democracycraft.vault.internal.util.region.RegionKey; import net.democracycraft.vault.internal.util.scan.OfflineRegionScanner; import net.democracycraft.vault.internal.util.scan.OfflineRegionScanner.DisplacedContainer; @@ -15,6 +16,7 @@ import org.bukkit.entity.Hanging; import org.bukkit.entity.ItemFrame; import org.bukkit.entity.Painting; +import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitRunnable; import org.bukkit.scheduler.BukkitTask; import org.bukkit.util.BoundingBox; @@ -27,6 +29,7 @@ import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; /** @@ -73,6 +76,35 @@ public void submit(@NotNull UUID worldId, @NotNull String regionId, @NotNull UUI } } + /** + * Builds the callback the capture service runs whenever it actually vaults one of the previous occupant's + * locks during this sweep. The first such call (and only the first) tells the new occupant, if online and + * notifications are enabled, that those items have been moved to vault storage. Thread-safe and may be + * invoked from async capture tasks; the message send is hopped back onto the main thread. + */ + private Runnable vaultedNotifier(@NotNull UUID initiator) { + if (!plugin.getConfig().getBoolean(ConfigPaths.AUTOVAULT_NOTIFY_OCCUPANT.getPath(), true)) { + return () -> {}; + } + AtomicBoolean sent = new AtomicBoolean(false); + return () -> { + if (!sent.compareAndSet(false, true)) { + return; + } + Bukkit.getScheduler().runTask(plugin, () -> { + Player occupant = Bukkit.getPlayer(initiator); + if (occupant == null) { + return; + } + String template = plugin.getConfig().getString(ConfigPaths.AUTOVAULT_NOTIFY_MESSAGE.getPath()); + if (template == null || template.isBlank()) { + return; + } + occupant.sendMessage(MiniMessageUtil.parseOrPlain(template)); + }); + }; + } + private void runSweep(@NotNull RegionKey key, @NotNull UUID initiator) { World world = Bukkit.getWorld(key.worldId()); if (world == null) { @@ -93,30 +125,35 @@ private void runSweep(@NotNull RegionKey key, @NotNull UUID initiator) { allowed.addAll(region.members()); allowed.add(initiator); + // Shared across both passes so the occupant is messaged at most once per sweep. + Runnable onVaulted = vaultedNotifier(initiator); + List displaced = OfflineRegionScanner.scan(key.worldId(), region.boundingBox(), allowed); if (displaced != null && !displaced.isEmpty()) { - vaultBlocks(key.worldId(), displaced, initiator); + vaultBlocks(key.worldId(), displaced, initiator, onVaulted); } if (plugin.getConfig().getBoolean(ConfigPaths.AUTOVAULT_INCLUDE_HANGINGS.getPath(), true)) { - vaultHangings(key.worldId(), region.boundingBox(), allowed, initiator); + vaultHangings(key.worldId(), region.boundingBox(), allowed, initiator, onVaulted); } } /** Vaults each displaced container, loading its chunk asynchronously. */ - private void vaultBlocks(@NotNull UUID worldId, @NotNull List displaced, @NotNull UUID initiator) { + private void vaultBlocks(@NotNull UUID worldId, @NotNull List displaced, @NotNull UUID initiator, + @NotNull Runnable onVaulted) { VaultCaptureService captureService = plugin.getCaptureService(); forEachChunkPaced(worldId, displaced, dc -> new int[]{dc.x() >> 4, dc.z() >> 4}, (world, chunk, dc) -> // Tolerant of blocks changed since the scan (non-containers just drop their protection). - captureService.captureOfflineAsync(world.getBlockAt(dc.x(), dc.y(), dc.z()), initiator, dc.owner())); + captureService.captureOfflineAsync(world.getBlockAt(dc.x(), dc.y(), dc.z()), initiator, dc.owner(), onVaulted)); } /** * Vaults displaced item frames and paintings across the region. Entity protections are not indexed * by coordinate, so every chunk in the region's bounds is loaded and its hanging entities inspected. */ - private void vaultHangings(@NotNull UUID worldId, @NotNull BoundingBox box, @NotNull Set allowed, @NotNull UUID initiator) { + private void vaultHangings(@NotNull UUID worldId, @NotNull BoundingBox box, @NotNull Set allowed, @NotNull UUID initiator, + @NotNull Runnable onVaulted) { VaultCaptureService captureService = plugin.getCaptureService(); BoltService bolt = plugin.getBoltService(); @@ -138,7 +175,7 @@ private void vaultHangings(@NotNull UUID worldId, @NotNull BoundingBox box, @Not if (!(entity instanceof ItemFrame) && !(entity instanceof Painting)) continue; UUID owner = bolt.getOwner(entity); if (owner == null || allowed.contains(owner)) continue; - captureService.captureHangingOfflineAsync((Hanging) entity, owner, initiator); + captureService.captureHangingOfflineAsync((Hanging) entity, owner, initiator, onVaulted); } }); } diff --git a/src/main/java/net/democracycraft/vault/internal/service/VaultCaptureService.java b/src/main/java/net/democracycraft/vault/internal/service/VaultCaptureService.java index 74a988b..a506445 100644 --- a/src/main/java/net/democracycraft/vault/internal/service/VaultCaptureService.java +++ b/src/main/java/net/democracycraft/vault/internal/service/VaultCaptureService.java @@ -860,7 +860,8 @@ public void captureDirectAsync(@NotNull Player actor, @NotNull Block block, @Not */ public void captureOfflineAsync(@NotNull Block block, @NotNull UUID initiatorUuid, - UUID boltOwner) { + UUID boltOwner, + @NotNull Runnable onVaulted) { var plugin = VaultStoragePlugin.getInstance(); CaptureOutcome outcome = captureWithDoubleChestSupport(block, boltOwner, initiatorUuid, null); @@ -900,7 +901,8 @@ public void captureOfflineAsync(@NotNull Block block, List batch = toItemBatch(newId, vault.contents()); if (!batch.isEmpty()) vaultService.putItems(newId, batch); - // No PlayerVaultEvent and no completion callback: this is an automated, fire-and-forget capture. + // No PlayerVaultEvent: this is an automated capture. Signal the occupant notifier that a vault was made. + onVaulted.run(); } }.runTaskAsynchronously(plugin); } @@ -912,7 +914,8 @@ public void captureOfflineAsync(@NotNull Block block, * and the entity is removed (which also disposes its Bolt protection). *

Must be called on the main thread with the entity loaded; persistence runs async.

*/ - public void captureHangingOfflineAsync(@NotNull Hanging hang, @NotNull UUID boltOwner, @NotNull UUID initiatorUuid) { + public void captureHangingOfflineAsync(@NotNull Hanging hang, @NotNull UUID boltOwner, @NotNull UUID initiatorUuid, + @NotNull Runnable onVaulted) { List stacks = HangingVaultSupport.itemStacksFrom(hang); if (stacks.isEmpty()) { return; @@ -923,6 +926,7 @@ public void captureHangingOfflineAsync(@NotNull Hanging hang, @NotNull UUID bolt new BukkitRunnable() { @Override public void run() { persistHangingStacks(stacks, boltOwner, initiatorUuid, supporting); + onVaulted.run(); new BukkitRunnable() { @Override public void run() { if (hang.isValid()) hang.remove(); diff --git a/src/main/java/net/democracycraft/vault/internal/util/config/ConfigPaths.java b/src/main/java/net/democracycraft/vault/internal/util/config/ConfigPaths.java index 24a00de..7e9833d 100644 --- a/src/main/java/net/democracycraft/vault/internal/util/config/ConfigPaths.java +++ b/src/main/java/net/democracycraft/vault/internal/util/config/ConfigPaths.java @@ -12,7 +12,9 @@ public enum ConfigPaths { SCAN_COOLDOWN_SECONDS("scan.cooldown-seconds"), AUTOVAULT_ENABLED("auto-vault.enabled"), AUTOVAULT_DELAY_TICKS("auto-vault.delay-ticks"), - AUTOVAULT_INCLUDE_HANGINGS("auto-vault.include-hangings"); + AUTOVAULT_INCLUDE_HANGINGS("auto-vault.include-hangings"), + AUTOVAULT_NOTIFY_OCCUPANT("auto-vault.notify-occupant"), + AUTOVAULT_NOTIFY_MESSAGE("auto-vault.notify-message"); private final String path; diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 609eb60..3f1406b 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -18,4 +18,8 @@ auto-vault: delay-ticks: 1200 # Vaukt Bolt-locked item frames and paintings. include-hangings: true + # When a sweep actually vaults the previous occupant's locked items, tell the new occupant + # (if online). Sent once per sweep, only when something was vaulted. Accepts MiniMessage. + notify-occupant: true + notify-message: "Locked containers and items left here by the previous occupant have been cleared into their vault storage."