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 1172c0d2..7a068f3d 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 @@ -6,7 +6,11 @@ import com.eu.habbo.messages.outgoing.furniture.FurnitureDataReloadComposer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; +import java.nio.file.ClosedWatchServiceException; +import java.nio.file.DirectoryStream; import java.nio.file.FileSystems; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardWatchEventKinds; import java.nio.file.WatchKey; @@ -17,7 +21,8 @@ import java.util.List; * Watches the furnidata source on a single daemon thread. On change (debounced), * re-indexes via the provider and broadcasts only the delta — or a compact * reload-hint when the delta exceeds the cap. A minimum interval throttles bursts. - * Never throws out of the loop. + * For the split-tier directory layout, the base dir AND its immediate + * subdirectories are registered. Never throws out of the loop. */ public class FurnidataWatcher { @@ -25,17 +30,20 @@ public class FurnidataWatcher { private final FurnitureTextProvider provider; private final Path watchDir; + private final boolean sourceIsDir; private final long maxBytes; private final long debounceMs; private final long minIntervalMs; private final int deltaCap; private volatile boolean running = false; + private volatile WatchService ws; private long lastBroadcast = 0L; public FurnidataWatcher(FurnitureTextProvider provider, Path source, long maxBytes) { this.provider = provider; - this.watchDir = java.nio.file.Files.isDirectory(source) ? source : source.getParent(); + this.sourceIsDir = Files.isDirectory(source); + this.watchDir = this.sourceIsDir ? source : source.getParent(); this.maxBytes = maxBytes; this.debounceMs = Long.parseLong(Emulator.getConfig().getValue("items.furnidata.watch.debounce.ms", "750")); this.minIntervalMs = Long.parseLong(Emulator.getConfig().getValue("items.furnidata.watch.min.interval.ms", "5000")); @@ -52,20 +60,30 @@ public class FurnidataWatcher { public void stop() { this.running = false; + WatchService local = this.ws; + if (local != null) { + try { local.close(); } catch (IOException ignored) { } + } } private void run() { - try (WatchService ws = FileSystems.getDefault().newWatchService()) { - this.watchDir.register(ws, StandardWatchEventKinds.ENTRY_MODIFY, - StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE); - + try { + this.ws = FileSystems.getDefault().newWatchService(); + } catch (IOException e) { + LOGGER.warn("FurnidataWatcher: could not create WatchService", e); + return; + } + try (WatchService service = this.ws) { + registerDirs(service); while (this.running) { - WatchKey key = ws.take(); + WatchKey key = service.take(); key.pollEvents(); Thread.sleep(this.debounceMs); key.pollEvents(); - key.reset(); - + if (!key.reset()) { + LOGGER.warn("FurnidataWatcher: watch key invalidated (directory removed?) — stopping"); + break; + } try { onChange(); } catch (Exception e) { @@ -74,11 +92,29 @@ public class FurnidataWatcher { } } catch (InterruptedException ignored) { Thread.currentThread().interrupt(); + } catch (ClosedWatchServiceException ignored) { + // stop() closed the service — normal shutdown } catch (Exception e) { LOGGER.warn("FurnidataWatcher stopped", e); } } + /** Register the base dir, plus one level of subdirectories for the split-tier layout. */ + private void registerDirs(WatchService service) throws IOException { + this.watchDir.register(service, StandardWatchEventKinds.ENTRY_MODIFY, + StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE); + if (this.sourceIsDir) { + try (DirectoryStream ds = Files.newDirectoryStream(this.watchDir)) { + for (Path child : ds) { + if (Files.isDirectory(child)) { + child.register(service, StandardWatchEventKinds.ENTRY_MODIFY, + StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE); + } + } + } + } + } + private void onChange() { Path source = this.provider.getSource(); if (source == null) return; @@ -88,7 +124,7 @@ public class FurnidataWatcher { long now = System.currentTimeMillis(); if (now - this.lastBroadcast < this.minIntervalMs) { - LOGGER.info("FurnidataWatcher: {} changes throttled (min interval)", delta.size()); + LOGGER.info("FurnidataWatcher: {} changes indexed but broadcast skipped (min interval) — clients update on next change or reconnect", delta.size()); return; } this.lastBroadcast = now; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/FurnitureTextProvider.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/FurnitureTextProvider.java index 622e01be..9f4fccbd 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/FurnitureTextProvider.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/FurnitureTextProvider.java @@ -51,6 +51,7 @@ public class FurnitureTextProvider { LOGGER.info("FurnitureTextProvider: indexed {} furnidata names from {}", this.index.size(), this.source); if (Boolean.parseBoolean(Emulator.getConfig().getValue("items.furnidata.watch.enabled", "true"))) { + if (this.watcher != null) this.watcher.stop(); this.watcher = new FurnidataWatcher(this, this.source, DEFAULT_MAX_BYTES); this.watcher.start(); }