2222/**
2323 * Finds owned animals sitting in unloaded chunks by reading the saved entity chunks,
2424 * so {@code /animals} can point at them. An owned animal that is not in any saved chunk
25- * of a fully scanned world is a ghost: its row is deleted and logged.
25+ * of a fully scanned world, nor under a logged-out rider, was lost from the save: it is
26+ * respawned from its snapshot, or, without one, its row is deleted as a ghost and logged.
2627 */
2728public final class HusbandryLocator {
2829
@@ -40,6 +41,12 @@ static void markFound(UUID uuid) {
4041 }
4142 }
4243
44+ static void markMissing (UUID uuid ) {
45+ if (uuid != null ) {
46+ MISSING .add (uuid );
47+ }
48+ }
49+
4350 /** Scans once in the background. Call on the main thread after loaded worlds have resumed. */
4451 public static void scanUnloaded () {
4552 HusbandryRepository repository = HusbandryEntities .repository ();
@@ -59,8 +66,9 @@ public static void scanUnloaded() {
5966 for (World world : Bukkit .getWorlds ()) {
6067 worlds .add (new HusbandryEntityScan .WorldDir (world .getName (), entitiesFolder (world )));
6168 }
69+ File playerdata = new File (Bukkit .getWorlds ().get (0 ).getWorldFolder (), "playerdata" );
6270 Bukkit .getScheduler ().runTaskAsynchronously (Cooking .plugin , () -> {
63- HusbandryEntityScan .Result result = HusbandryEntityScan .scan (worlds , targets .keySet ());
71+ HusbandryEntityScan .Result result = HusbandryEntityScan .scan (worlds , playerdata , targets .keySet ());
6472 if (Cooking .plugin != null && Cooking .plugin .isEnabled ()) {
6573 Bukkit .getScheduler ().runTask (Cooking .plugin , () -> apply (targets , result , worlds ));
6674 }
@@ -85,6 +93,7 @@ private static void apply(
8593 return ;
8694 }
8795 int located = 0 ;
96+ int restoring = 0 ;
8897 int dropped = 0 ;
8998 int missing = 0 ;
9099 List <String > scannedWorlds = worlds .stream ().map (HusbandryEntityScan .WorldDir ::world ).toList ();
@@ -98,13 +107,31 @@ private static void apply(
98107 // Loaded or unloaded again during the scan; the stored location is newer.
99108 continue ;
100109 }
110+ if (result .ridden ().contains (uuid )) {
111+ // Saved with a rider who logged out; it comes back when they join.
112+ MISSING .remove (uuid );
113+ continue ;
114+ }
101115 HusbandryEntityScan .Found found = result .found ().get (uuid );
102116 if (found == null ) {
103117 HusbandryAnimal animal = stored .get ();
104118 // An animal last seen in a world that was not scanned may still be there.
105119 if (!isConfirmedGhost (result .complete (), animal .world (), scannedWorlds )) {
106120 continue ;
107121 }
122+ try {
123+ if (HusbandrySnapshots .restore (repository , animal , repository .listOwners (uuid ))) {
124+ MISSING .remove (uuid );
125+ restoring ++;
126+ continue ;
127+ }
128+ } catch (SqliteDatabaseException ex ) {
129+ Bukkit .getLogger ().severe ("[Cooking] Failed to read the snapshot of " + uuid
130+ + ": " + ex .getMessage ());
131+ MISSING .add (uuid );
132+ missing ++;
133+ continue ;
134+ }
108135 try {
109136 String line = deleteGhost (repository , animal );
110137 Bukkit .getLogger ().warning (line );
@@ -130,12 +157,12 @@ private static void apply(
130157 Bukkit .getLogger ().severe ("[Cooking] Failed to save husbandry animal location: " + ex .getMessage ());
131158 }
132159 }
133- if (located > 0 || dropped > 0 || missing > 0 || !result .complete ()) {
160+ if (located > 0 || restoring > 0 || dropped > 0 || missing > 0 || !result .complete ()) {
134161 String failed = missing > 0
135162 ? ", " + missing + " still marked missing after a failed drop"
136163 : "" ;
137- Bukkit .getLogger ().info ("[Cooking] Animal scan: updated " + located + " locations, dropped "
138- + dropped + " ghost animals" + failed
164+ Bukkit .getLogger ().info ("[Cooking] Animal scan: updated " + located + " locations, restoring "
165+ + restoring + " lost animals from snapshots, dropped " + dropped + " ghost animals" + failed
139166 + (result .complete () ? "." : " (some chunks could not be read, none dropped)." ));
140167 }
141168 }
@@ -167,19 +194,23 @@ static String deleteGhost(HusbandryRepository repository, HusbandryAnimal animal
167194 }
168195
169196 static String ghostLog (HusbandryAnimal animal , List <HusbandryOwner > owners ) {
170- String name = animal .name () == null || animal .name ().isBlank () ? "(unnamed)" : animal .name ();
171- String type = animal .type () == null || animal .type ().isBlank () ? "unknown" : animal .type ();
172197 String place = animal .hasLocation ()
173198 ? animal .world () + " " + animal .x () + ", " + animal .y () + ", " + animal .z ()
174199 : "unknown" ;
200+ return "[Cooking] Dropped ghost animal " + describe (animal , owners ) + " last seen " + place ;
201+ }
202+
203+ /** Name, type, uuid and owners, as the ghost and restore log lines print them. */
204+ static String describe (HusbandryAnimal animal , List <HusbandryOwner > owners ) {
205+ String name = animal .name () == null || animal .name ().isBlank () ? "(unnamed)" : animal .name ();
206+ String type = animal .type () == null || animal .type ().isBlank () ? "unknown" : animal .type ();
175207 String ownerText = owners == null || owners .isEmpty ()
176208 ? "none"
177209 : owners .stream ()
178210 .sorted ((left , right ) -> left .playerUuid ().compareTo (right .playerUuid ()))
179211 .map (owner -> owner .playerUuid () + " (" + owner .role () + ")" )
180212 .collect (Collectors .joining (", " ));
181- return "[Cooking] Dropped ghost animal " + name + " (" + type + ") " + animal .uuid ()
182- + " owners=" + ownerText + " last seen " + place ;
213+ return name + " (" + type + ") " + animal .uuid () + " owners=" + ownerText ;
183214 }
184215
185216 private static boolean sameLocation (HusbandryAnimal animal , HusbandryEntityScan .Found found ) {
0 commit comments