Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,5 @@ Desktop.ini

# Technical documentation is maintained in TF-Minecraft/Docs
/docs/

/tools/__pycache__/
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<interactiblefurniture.version>0.2.0</interactiblefurniture.version>
<rpcharacters.version>2.16.0</rpcharacters.version>
<simplefactions.version>4.0.0</simplefactions.version>
<tlibs.version>2.4.1</tlibs.version>
<tlibs.version>2.6.0</tlibs.version>
</properties>

<dependencyManagement>
Expand Down
1 change: 1 addition & 0 deletions src/main/java/net/tfminecraft/cooking/Cooking.java
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ public void onEnable() {
TLibs.getItemAPI().registerPathHandler("c", CookingPathHandler.INSTANCE);
openHusbandryDatabase();
HusbandryLifecycleListener.applyStatsRevision();
net.tfminecraft.cooking.husbandry.HusbandryIdentity.start(this, husbandryRepository);
registerListeners();

cookingManager.start();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,20 @@ public boolean onCommand(CommandSender sender, Command command, String label, St
}
boolean self = args.length == 0;
UUID targetId;
net.tfminecraft.tlibs.character.CharacterRef targetCharacter = null;
String ownerName;
if (self) {
if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("Usage: /" + label + " <player>", NamedTextColor.RED));
return true;
}
targetId = player.getUniqueId();
ownerName = player.getName();
targetCharacter = HusbandryIdentity.session(player).map(session -> session.ref()).orElse(null);
if (targetCharacter == null) {
sender.sendMessage(Component.text("Choose a character before listing animals.", NamedTextColor.RED));
return true;
}
ownerName = HusbandryIdentity.name(targetCharacter);
} else {
OfflinePlayer offline = Bukkit.getOfflinePlayerIfCached(args[0]);
if (offline == null) {
Expand All @@ -52,17 +58,22 @@ public boolean onCommand(CommandSender sender, Command command, String label, St
ownerName = offline.getName() == null ? args[0] : offline.getName();
}
List<HusbandryOwned> rows = new ArrayList<>();
for (HusbandryOwned owned : repository.listForPlayer(targetId)) {
for (HusbandryOwned owned : self ? repository.listForCharacter(targetCharacter) : repository.listForPlayer(targetId)) {
HusbandryAnimal animal = HusbandryEntities.getLoaded(owned.animal().uuid()).orElse(owned.animal());
Entity live = Bukkit.getEntity(animal.uuid());
if (live != null) {
HusbandryLocation.remember(animal, live);
}
rows.add(new HusbandryOwned(animal, owned.role()));
}
for (Component line : HusbandryRoster.render(
ownerName, self, rows, HusbandryConfig.maxAnimals(), System.currentTimeMillis(),
HusbandryLocator::isMissing)) {
List<Component> lines = HusbandryRoster.render(ownerName, self, rows, HusbandryConfig.maxAnimals(),
System.currentTimeMillis(), HusbandryLocator::isMissing);
if (self) {
lines.set(0, Component.text("Your character's animals (" + rows.size() + ")", NamedTextColor.GOLD));
lines.add(1, Component.text("Account animal places: " + repository.countForPlayer(targetId)
+ "/" + HusbandryConfig.maxAnimals(), NamedTextColor.GRAY));
}
for (Component line : lines) {
sender.sendMessage(line);
}
return true;
Expand Down
268 changes: 268 additions & 0 deletions src/main/java/net/tfminecraft/cooking/husbandry/HusbandryEstate.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package net.tfminecraft.cooking.husbandry;

import java.util.Optional;
import java.util.UUID;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import net.tfminecraft.tlibs.character.CharacterRef;
import net.tfminecraft.tlibs.character.CharacterSession;
import net.tfminecraft.tlibs.character.Characters;

/** New heir and estate entry points are linked only after a capability probe. */
public final class HusbandryIdentity {
private HusbandryIdentity() {}

public static Optional<CharacterSession> session(Player player) {
return player == null ? Optional.empty() : Characters.currentSession(player).filter(session -> current(session, player));
}

public static boolean current(CharacterSession session, Player player) {
return session != null && player != null && session.ref().ownerId().equals(player.getUniqueId())
&& Characters.isCurrent(session);
}

public static String name(CharacterRef ref) {
return Characters.info(ref.characterId()).filter(info -> info.ref().equals(ref))
.map(info -> info.name()).orElse(ref.characterId().toString());
}

public static boolean supports(ClassLoader loader, boolean estates) {
try {
Class<?> characters = Class.forName("net.tfminecraft.tlibs.character.Characters", false, loader);
characters.getMethod("legacyHeirsReady");
characters.getMethod("legacyHeir", UUID.class);
if (estates) {
for (String name : new String[]{"Estates", "EstateParticipant", "EstateDecision", "Outcome", "Holdings"}) {
Class.forName("net.tfminecraft.tlibs.character.estate." + name, false, loader);
}
}
return true;
} catch (ReflectiveOperationException | LinkageError ex) {
return false;
}
}

public static void start(Plugin plugin, HusbandryRepository repository) {
if (repository == null) return;
ClassLoader loader = HusbandryIdentity.class.getClassLoader();
if (!supports(loader, false)) {
plugin.getLogger().warning("Reviewed animal heirs require TLibs 2.6.0; old owner slots remain held.");
return;
}
HusbandryMigration.start(plugin, repository);
if (supports(loader, true)) HusbandryEstate.start(plugin, repository);
else plugin.getLogger().warning("TLibs has no estate API; animal slots stay with dead characters.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
Expand Down Expand Up @@ -37,7 +36,7 @@ public static void open(Player player, LivingEntity entity, HusbandryAnimal anim
return;
}
boolean mount = HusbandryMounts.isMount(entity);
HusbandryInspectHolder holder = new HusbandryInspectHolder(animal.uuid());
HusbandryInspectHolder holder = new HusbandryInspectHolder(animal.uuid(), HusbandryIdentity.session(player).orElse(null));
String displayName = animal.name();
if (displayName == null || displayName.isBlank() || "???".equals(displayName.trim())) {
displayName = HusbandryEntities.displayName(entity.getType());
Expand Down Expand Up @@ -139,10 +138,11 @@ private static String decayLine(HusbandryAnimal animal) {
private static ItemStack ownersItem(UUID animalUuid) {
List<String> lore = new ArrayList<>();
for (HusbandryOwner owner : HusbandryOwnershipService.listOwners(animalUuid)) {
OfflinePlayer offline = Bukkit.getOfflinePlayer(owner.playerUuid());
String name = offline.getName() == null ? owner.playerUuid().toString() : offline.getName();
String name = HusbandryIdentity.name(owner.ref());
lore.add("§7" + owner.role() + ": " + name);
}
HusbandryRepository repository = HusbandryEntities.repository();
if (repository != null && repository.hasHeldOwner(animalUuid)) lore.add("§7Unresolved owner slot (staff review)");
if (lore.isEmpty()) {
lore.add("§7None (untamed)");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@ public final class HusbandryInspectHolder implements InventoryHolder {

private final UUID animalUuid;
private Inventory inventory;
private final net.tfminecraft.tlibs.character.CharacterSession session;

public HusbandryInspectHolder(UUID animalUuid) {
public HusbandryInspectHolder(UUID animalUuid, net.tfminecraft.tlibs.character.CharacterSession session) {
this.animalUuid = animalUuid;
this.session = session;
}

public boolean isCurrent(org.bukkit.entity.Player player) { return HusbandryIdentity.current(session, player); }

public boolean belongsTo(net.tfminecraft.tlibs.character.CharacterSession outgoing) { return outgoing.equals(session); }

public UUID animalUuid() {
return animalUuid;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ public void onClick(InventoryClickEvent event) {
if (!(event.getWhoClicked() instanceof Player player)) {
return;
}
if (!inspect.isCurrent(player)) {
player.closeInventory();
return;
}
ItemStack clicked = event.getCurrentItem();
if (clicked == null || clicked.getType() != Material.WRITABLE_BOOK) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ private static void apply(
int restoring = 0;
int dropped = 0;
int missing = 0;
int held = 0;
List<String> scannedWorlds = worlds.stream().map(HusbandryEntityScan.WorldDir::world).toList();
for (Map.Entry<UUID, Long> target : targets.entrySet()) {
UUID uuid = target.getKey();
Expand Down Expand Up @@ -134,6 +135,10 @@ private static void apply(
}
try {
String line = deleteGhost(repository, animal);
if (line == null) {
held++;
continue;
}
Bukkit.getLogger().warning(line);
dropped++;
} catch (SqliteDatabaseException ex) {
Expand All @@ -157,12 +162,13 @@ private static void apply(
Bukkit.getLogger().severe("[Cooking] Failed to save husbandry animal location: " + ex.getMessage());
}
}
if (located > 0 || restoring > 0 || dropped > 0 || missing > 0 || !result.complete()) {
if (located > 0 || restoring > 0 || dropped > 0 || missing > 0 || held > 0 || !result.complete()) {
String failed = missing > 0
? ", " + missing + " still marked missing after a failed drop"
: "";
Bukkit.getLogger().info("[Cooking] Animal scan: updated " + located + " locations, restoring "
+ restoring + " lost animals from snapshots, dropped " + dropped + " ghost animals" + failed
+ (held > 0 ? ", held " + held + " missing animal records for staff review" : "")
+ (result.complete() ? "." : " (some chunks could not be read, none dropped)."));
}
}
Expand All @@ -181,13 +187,15 @@ static boolean isConfirmedGhost(boolean scanComplete, String storedWorld, Collec
return scannedWorlds != null && scannedWorlds.contains(storedWorld);
}

/** Deletes the animal and its owners. Returns the log line for the dropped record. */
/** Deletes the animal and its owners, or returns null without mutation for an estate hold. */
static String deleteGhost(HusbandryRepository repository, HusbandryAnimal animal) {
if (repository.isEstateHeld(animal.uuid())) {
MISSING.add(animal.uuid());
return null;
}
List<HusbandryOwner> owners = repository.listOwners(animal.uuid());
// Foreign-key cascades remove account owners, character owners and snapshots together.
repository.deleteAnimal(animal.uuid());
for (HusbandryOwner owner : owners) {
repository.deleteOwner(animal.uuid(), owner.playerUuid());
}
HusbandryEntities.evict(animal.uuid());
MISSING.remove(animal.uuid());
return ghostLog(animal, owners);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package net.tfminecraft.cooking.husbandry;

import org.bukkit.plugin.Plugin;
import net.tfminecraft.tlibs.character.Characters;

/** Kept out of the old-API classloading path. Every pass uses the same archived plan. */
final class HusbandryMigration {
private int quietMinutes;
private String lastProblem;
private HusbandryMigration() {}
static void start(Plugin plugin, HusbandryRepository repository) {
HusbandryMigration migration = new HusbandryMigration();
plugin.getServer().getScheduler().runTaskTimer(plugin, () -> {
if (migration.quietMinutes > 0) {
migration.quietMinutes--;
return;
}
if (migration.attempt(plugin, repository)) migration.quietMinutes = 59;
}, 1, 20L * 60);
}
static void reconcile(Plugin plugin, HusbandryRepository repository) {
new HusbandryMigration().attempt(plugin, repository);
}
private boolean attempt(Plugin plugin, HusbandryRepository repository) {
try {
if (!repository.migrateOwners(Characters::legacyHeirsReady, Characters::legacyHeir)) {
warnChanged(plugin, "Animal ownership waits for reviewed heirs; legacy slots remain held.");
return false;
}
int held = repository.heldOwners();
if (held != 0) {
plugin.getLogger().warning("Animal ownership holds " + held
+ " legacy slots for staff; see husbandry_owner_plan in Data/husbandry.db.");
}
lastProblem = null;
return true;
} catch (RuntimeException ex) {
warnChanged(plugin, "Animal ownership migration rolled back; retrying: " + ex.getMessage());
return false;
}
}
private void warnChanged(Plugin plugin, String message) {
if (!message.equals(lastProblem)) plugin.getLogger().warning(message);
lastProblem = message;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ public void onMount(EntityMountEvent event) {
if (!(event.getEntity() instanceof Player player)) {
return;
}
if (HusbandryEntities.repository() == null || HusbandryIdentity.session(player).isEmpty()) {
event.setCancelled(true);
player.sendMessage("§cYour character's animal records are not ready.");
return;
}
if (mount instanceof LivingEntity living) {
HusbandryAnimal animal = HusbandryMounts.enrollIfNeeded(living);
if (animal != null) {
Expand All @@ -39,6 +44,45 @@ public void onMount(EntityMountEvent event) {
player.sendMessage("§cThis is not your animal.");
}

@EventHandler
public void onCharacterEnd(net.tfminecraft.tlibs.character.CharacterSessionEndEvent event) {
// The event's session is already noncurrent; clean up the outgoing body directly.
Player player = event.getPlayer();
if (!event.getSession().ref().ownerId().equals(player.getUniqueId())) return;
Entity vehicle = player.getVehicle();
if (vehicle != null && HusbandryMounts.isMount(vehicle)) player.leaveVehicle();
org.bukkit.inventory.Inventory open = player.getOpenInventory().getTopInventory();
if (open != null && open.getHolder() instanceof HusbandryInspectHolder holder
&& holder.belongsTo(event.getSession())) player.closeInventory();
HusbandryClaimHint.forget(player.getUniqueId());
}

@EventHandler
public void onCharacterStart(net.tfminecraft.tlibs.character.CharacterSessionStartEvent event) {
if (!HusbandryIdentity.current(event.getSession(), event.getPlayer())) return;
Entity vehicle = event.getPlayer().getVehicle();
if (vehicle != null && HusbandryOwnershipService.hasAnyOwner(vehicle.getUniqueId())
&& !HusbandryOwnershipService.isOwner(event.getPlayer(), vehicle.getUniqueId())
&& !HusbandryOwnershipService.isStaff(event.getPlayer())) event.getPlayer().leaveVehicle();
}

/** Vanilla sitting, feeding and mount-inventory interactions cannot use an account's tame owner. */
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void protectInteract(org.bukkit.event.player.PlayerInteractEntityEvent event) {
if (event.getRightClicked() instanceof org.bukkit.entity.Tameable
&& HusbandryOwnershipService.hasAnyOwner(event.getRightClicked().getUniqueId())
&& !HusbandryOwnershipService.isOwner(event.getPlayer(), event.getRightClicked().getUniqueId())
&& !HusbandryOwnershipService.isStaff(event.getPlayer())) event.setCancelled(true);
}

@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void protectTame(EntityTameEvent event) {
if (event.getOwner() instanceof Player player
&& HusbandryOwnershipService.hasAnyOwner(event.getEntity().getUniqueId())
&& !HusbandryOwnershipService.isOwner(player, event.getEntity().getUniqueId())
&& !HusbandryOwnershipService.isStaff(player)) event.setCancelled(true);
}

@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onTame(EntityTameEvent event) {
if (event.getOwner() instanceof Player player) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
package net.tfminecraft.cooking.husbandry;

import java.util.Locale;
import java.util.UUID;

public final class HusbandryOwner {

private final UUID animalUuid;
private final UUID playerUuid;
private final UUID characterUuid;
private final String role;

/** Legacy metadata remains representable, but cannot grant or mutate character ownership. */
@Deprecated
public HusbandryOwner(UUID animalUuid, UUID playerUuid, String role) {
this(animalUuid, playerUuid, null, role);
}

public HusbandryOwner(UUID animalUuid, UUID playerUuid, UUID characterUuid, String role) {
this.animalUuid = animalUuid;
this.playerUuid = playerUuid;
this.role = role == null || role.isBlank() ? "owner" : role.toLowerCase(Locale.ROOT);
this.characterUuid = characterUuid;
this.role = role;
}

public UUID animalUuid() {
Expand All @@ -23,6 +30,12 @@ public UUID playerUuid() {
return playerUuid;
}

public UUID characterUuid() { return characterUuid; }

public net.tfminecraft.tlibs.character.CharacterRef ref() {
return new net.tfminecraft.tlibs.character.CharacterRef(playerUuid, characterUuid);
}

public String role() {
return role;
}
Expand Down
Loading
Loading