From bf192588a9d0489d3dd722d449dc5dc7b85b6da4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Thu, 5 Nov 2026 17:13:40 +0100 Subject: [PATCH] crash recovery refunds only photos found in the saved data (async scan): after a crash that rolled the shot back to the last save the camera still had the film and the refund duplicated it, a failed scan refunds all, jobs stay in pending.yml until decided --- .../pixelpics/survival/JoinListener.java | 5 + .../pixelpics/survival/PendingPhotos.java | 92 +++++++++++++++---- 2 files changed, 81 insertions(+), 16 deletions(-) diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/survival/JoinListener.java b/src/main/java/eu/mhsl/minecraft/pixelpics/survival/JoinListener.java index d51536d..c76836f 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/survival/JoinListener.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/survival/JoinListener.java @@ -18,6 +18,11 @@ public class JoinListener implements Listener { public void onJoin(PlayerJoinEvent event) { Player player = event.getPlayer(); player.discoverRecipes(SurvivalRecipes.allKeys()); + giveRefunds(player); + } + + /** Hands out the film owed to {@code player} (also used once the crash recovery has decided). */ + static void giveRefunds(Player player) { int films = Main.getInstance().getPendingPhotos().takeRefunds(player.getUniqueId()); for (int i = 0; i < films; i++) CameraItems.give(player, CameraItems.createFilm()); if (films > 0) { diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/survival/PendingPhotos.java b/src/main/java/eu/mhsl/minecraft/pixelpics/survival/PendingPhotos.java index 6db6f90..5d1ba2b 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/survival/PendingPhotos.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/survival/PendingPhotos.java @@ -2,11 +2,20 @@ package eu.mhsl.minecraft.pixelpics.survival; import eu.mhsl.minecraft.pixelpics.Main; import eu.mhsl.minecraft.pixelpics.utils.MapManager; +import eu.mhsl.minecraft.pixelpics.utils.MapUsageScan; import java.io.File; import java.io.IOException; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.UUID; +import org.bukkit.Bukkit; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.plugin.IllegalPluginAccessException; /** * Persists photos that are still rendering ({@code pending.yml}), so a crash cannot swallow film: a job still listed @@ -27,31 +36,78 @@ public final class PendingPhotos { this.yml = YamlConfiguration.loadConfiguration(file); } - /** Turns jobs left over from a crash into film refunds; their maps keep the blank placeholder. */ + /** + * Turns jobs left over from a crash into film refunds; their maps keep the blank placeholder. A crash also rolls + * the world back to its last save: if that save predates the shot, the photo item is in no saved data and the + * camera still holds the film, so a refund would duplicate it. The saved worlds and player files are therefore + * scanned (off the main thread) and only photos found there are refunded; if the scan fails, all are refunded. The + * jobs stay in the file until decided, so another crash meanwhile loses nothing. + */ public void recoverAbandoned() { ConfigurationSection jobs = yml.getConfigurationSection(JOBS); if (jobs == null) return; - int recovered = 0; + Map owed = new LinkedHashMap<>(); for (String key : jobs.getKeys(false)) { ConfigurationSection job = jobs.getConfigurationSection(key); - if (job == null) continue; - int mapId = Integer.parseInt(key); - if (MapManager.hasImage(mapId)) continue; // developed, only the bookkeeping was lost + int mapId; + try { + mapId = Integer.parseInt(key); + } catch (NumberFormatException e) { + continue; + } + if (job == null || MapManager.hasImage(mapId)) continue; // developed, only the bookkeeping was lost if (!MapManager.isManaged(mapId)) MapManager.savePlaceholder(mapId); String player = job.getString("player"); - if (player != null && job.getBoolean("film")) { - addRefund(UUID.fromString(player)); - recovered++; + if (player == null || !job.getBoolean("film")) continue; + try { + owed.put(mapId, UUID.fromString(player)); + } catch (IllegalArgumentException ignored) { + // corrupt entry: nothing to refund } } - yml.set(JOBS, null); - save(); - if (recovered > 0) { - Main.getInstance() - .getLogger() - .warning(recovered + " photo(s) did not develop before the last shutdown; their film is refunded" - + " when the players join."); + if (owed.isEmpty()) { + yml.set(JOBS, null); + save(); + return; } + Main plugin = Main.getInstance(); + List worlds = MapUsageScan.worldFolders(); + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + Set saved; + try { + saved = MapUsageScan.disk(worlds); + } catch (IOException | RuntimeException e) { + plugin.getLogger() + .warning("Could not scan the saved data for photos lost in the crash (" + e.getMessage() + + "); refunding all of their film."); + saved = null; + } + Set found = saved; + try { + Bukkit.getScheduler().runTask(plugin, () -> refundAbandoned(owed, found)); + } catch (IllegalPluginAccessException ignored) { + // disabled meanwhile: the jobs are still in the file, decided at the next start + } + }); + } + + /** Refunds the abandoned photos whose item was saved ({@code saved} null = all). */ + private void refundAbandoned(Map owed, Set saved) { + int refunded = 0; + for (Map.Entry e : owed.entrySet()) { + if (saved == null || saved.contains(e.getKey())) { + addRefund(e.getValue(), false); + refunded++; + } + yml.set(JOBS + "." + e.getKey(), null); + } + save(); + Main.getInstance() + .getLogger() + .warning(owed.size() + " photo(s) did not develop before the last shutdown; " + refunded + + " film refunded when the players join, " + (owed.size() - refunded) + + " not (the crash rolled the shot back: the photo was never saved)."); + for (Player player : Bukkit.getOnlinePlayers()) JoinListener.giveRefunds(player); } public void add(int mapId, UUID player, boolean film) { @@ -66,9 +122,13 @@ public final class PendingPhotos { } public void addRefund(UUID player) { + addRefund(player, true); + } + + private void addRefund(UUID player, boolean save) { String key = REFUNDS + "." + player; yml.set(key, yml.getInt(key) + 1); - save(); + if (save) save(); } /** The films owed to {@code player}, now cleared. */