diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/FurnidataLock.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/FurnidataLock.java new file mode 100644 index 00000000..5c0224ec --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/FurnidataLock.java @@ -0,0 +1,13 @@ +package com.eu.habbo.habbohotel.items; + +import java.util.concurrent.locks.ReentrantLock; + +/** + * One process-wide lock serializing every furnidata reindex and every editor-driven + * furnidata write, so an editor write never races the file watcher's reindex and the + * volatile index is never observed mid-swap by two writers. + */ +public final class FurnidataLock { + public static final ReentrantLock LOCK = new ReentrantLock(); + private FurnidataLock() {} +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/FurnidataWatcher.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/FurnidataWatcher.java index 7a068f3d..0b3aa615 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/FurnidataWatcher.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/FurnidataWatcher.java @@ -116,26 +116,31 @@ public class FurnidataWatcher { } private void onChange() { - Path source = this.provider.getSource(); - if (source == null) return; + FurnidataLock.LOCK.lock(); + try { + Path source = this.provider.getSource(); + if (source == null) return; - List delta = this.provider.reindex(new FurnidataReader(source, this.maxBytes).read()); - if (delta.isEmpty()) return; + List delta = this.provider.reindex(new FurnidataReader(source, this.maxBytes).read()); + if (delta.isEmpty()) return; - long now = System.currentTimeMillis(); - if (now - this.lastBroadcast < this.minIntervalMs) { - LOGGER.info("FurnidataWatcher: {} changes indexed but broadcast skipped (min interval) — clients update on next change or reconnect", delta.size()); - return; + long now = System.currentTimeMillis(); + if (now - this.lastBroadcast < this.minIntervalMs) { + LOGGER.info("FurnidataWatcher: {} changes indexed but broadcast skipped (min interval) — clients update on next change or reconnect", delta.size()); + return; + } + this.lastBroadcast = now; + + FurnitureDataReloadComposer composer = (delta.size() > this.deltaCap) + ? new FurnitureDataReloadComposer(FurnitureDataReloadComposer.MODE_RELOAD_HINT, List.of()) + : new FurnitureDataReloadComposer(FurnitureDataReloadComposer.MODE_DELTA, delta); + + broadcast(composer); + LOGGER.info("FurnidataWatcher: broadcast {} ({} entries)", + delta.size() > this.deltaCap ? "reload-hint" : "delta", delta.size()); + } finally { + FurnidataLock.LOCK.unlock(); } - this.lastBroadcast = now; - - FurnitureDataReloadComposer composer = (delta.size() > this.deltaCap) - ? new FurnitureDataReloadComposer(FurnitureDataReloadComposer.MODE_RELOAD_HINT, List.of()) - : new FurnitureDataReloadComposer(FurnitureDataReloadComposer.MODE_DELTA, delta); - - broadcast(composer); - LOGGER.info("FurnidataWatcher: broadcast {} ({} entries)", - delta.size() > this.deltaCap ? "reload-hint" : "delta", delta.size()); } private void broadcast(FurnitureDataReloadComposer composer) {