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
5 changes: 4 additions & 1 deletion src/main/java/net/tfminecraft/companionpets/PetsPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public class PetsPlugin extends JavaPlugin {
private BukkitTask visualTicker;
private BukkitTask tailTicker;
private BukkitTask heirWait;
private net.tfminecraft.companionpets.estate.PetEstate estate;
private PetVisual visual;
private PetRuntime runtime;
private PetListener petListener;
Expand Down Expand Up @@ -93,7 +94,7 @@ public void onEnable() {
if (net.tfminecraft.companionpets.ownership.CharacterAccess.available())
Bukkit.getPluginManager().registerEvents(new net.tfminecraft.companionpets.ownership.CharacterLifecycle(runtime, actions), this);
if (net.tfminecraft.companionpets.estate.EstateProbe.available())
net.tfminecraft.companionpets.estate.PetEstate.start(this, runtime, actions);
estate = net.tfminecraft.companionpets.estate.PetEstate.start(this, runtime, actions);
staff = new net.tfminecraft.companionpets.staff.StaffCommands(runtime, actions);
tests = new net.tfminecraft.companionpets.staff.TestCommands(runtime, actions, staff);
Bukkit.getPluginManager().registerEvents(staff.menus(), this);
Expand Down Expand Up @@ -123,6 +124,8 @@ public void onEnable() {
if (body != null && type != null) runtime.bodies().reattach(body, pet, type);
}
heirWait.cancel();
// The first hourly estate pass ran before ownership was ready; settle due estates now.
if (estate != null) estate.reconcile();
}, 1L, 20L);
}
getLogger().info("CompanionPets enabled (" + config.types().size() + " pet types)");
Expand Down
16 changes: 12 additions & 4 deletions src/main/java/net/tfminecraft/companionpets/estate/PetEstate.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,12 @@ public final class PetEstate implements EstateParticipant {
public PetEstate(PetRuntime runtime, PetActions actions, Path journals) {
this.runtime = runtime; this.actions = actions; this.journals = journals;
}
public static void start(Plugin plugin, PetRuntime runtime, PetActions actions) {
public static PetEstate start(Plugin plugin, PetRuntime runtime, PetActions actions) {
PetEstate estate = new PetEstate(runtime, actions, plugin.getDataFolder().toPath().resolve("estates"));
estate.recover();
Bukkit.getServicesManager().register(EstateParticipant.class, estate, plugin, ServicePriority.Normal);
Bukkit.getScheduler().runTaskTimer(plugin, estate::reconcile, 1L, 72000L);
return estate;
}
@Override public String id() { return "companionpets"; }
@Override public Holdings describe(UUID character) {
Expand All @@ -60,8 +61,7 @@ static String houseLabel(String key, String main) {
}
@Override public Outcome settle(EstateDecision decision) {
if (runtime.store().persistenceConflict()) return Outcome.blocked("Pet source changed outside its current writer; review is required");
if (!net.tfminecraft.companionpets.ownership.CharacterAccess.heirReady() || !runtime.store().ownershipReady())
return Outcome.retry("Reviewed pet ownership and persistence are not ready");
if (!ready()) return Outcome.retry("Reviewed pet ownership and persistence are not ready");
if (!guard.test(decision)) return Outcome.retry("The exact estate decision is not current");
if (decision.property().character().isPresent() && runtime.store().hasUnassignedOwner(decision.testator().ownerId()))
return Outcome.blocked("Legacy pet/house ownership for this account still needs its reviewed heir");
Expand Down Expand Up @@ -301,7 +301,15 @@ private void recover() {
runtime.plugin().getLogger().severe("Pet estate journals unreadable; ownership stays held: " + ex.getMessage());
}
}
void reconcile() {
private boolean ready() {
return net.tfminecraft.companionpets.ownership.CharacterAccess.heirReady() && runtime.store().ownershipReady();
}
/**
* Hourly, and once more when ownership becomes ready after a restart. Until then the pass is
* skipped rather than answering RETRY over estates already settled; a conflict still reports.
*/
public void reconcile() {
if (!runtime.store().persistenceConflict() && !ready()) return;
for (EstateDecision decision : decided.get()) {
if (guard.test(decision)) Estates.report(decision.deathId(), id(), decision.revision(), settle(decision));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,14 @@ private WolfMock body(Pet pet, Location at) {
org.mockito.Mockito.when(identity.legacyHeirsReady()).thenAnswer(call->ready.get());
org.mockito.Mockito.when(identity.legacyHeir(owner.getUniqueId())).thenReturn(java.util.Optional.of(new net.tfminecraft.tlibs.character.CharacterRef(owner.getUniqueId(),owner.getUniqueId())));
org.bukkit.Bukkit.getServicesManager().register(net.tfminecraft.tlibs.character.CharacterIdentity.class,identity,plugin,org.bukkit.plugin.ServicePriority.Highest);
var estates=org.mockito.Mockito.mock(net.tfminecraft.tlibs.character.estate.EstateService.class);
org.bukkit.Bukkit.getServicesManager().register(net.tfminecraft.tlibs.character.estate.EstateService.class,estates,plugin,org.bukkit.plugin.ServicePriority.Highest);
enable();var existing=runtime().store().get(pet.id());assertNull(existing.ownerCharacterId());server.getScheduler().performOneTick();assertNull(existing.ownerCharacterId());
org.mockito.Mockito.verify(estates,org.mockito.Mockito.never()).decided();
ready.set(true);server.getScheduler().performTicks(20);assertSame(existing,runtime().store().get(pet.id()));assertEquals(owner.getUniqueId(),existing.ownerCharacterId());
assertTrue(Files.exists(file("ownership-migrations/plan-0001.yml")));
// The estate pass skipped while heirs were pending runs once as soon as they are ready.
org.mockito.Mockito.verify(estates).decided();
}

@Test void recoveryGuardDisablesStartupAndPreservesFilesWithoutSchedulingActions() throws Exception {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,20 @@ void unrelatedIncompleteDeathDoesNotBlockThisExactHeldIntentResume() throws Exce
estate.decided=()->List.of(decision);estate.guard=ignored->false;estate.reconcile();org.mockito.Mockito.verifyNoInteractions(service);
estate.guard=ignored->true;estate.reconcile();org.mockito.Mockito.verify(service).report(org.mockito.ArgumentMatchers.eq(death),org.mockito.ArgumentMatchers.eq("companionpets"),org.mockito.ArgumentMatchers.eq(1L),org.mockito.ArgumentMatchers.any(Outcome.class));
}
@Test void hourlyPassWaitsSilentlyForReadinessButStillReportsAPersistenceConflict() throws Exception {
var service=org.mockito.Mockito.mock(EstateService.class);
org.bukkit.Bukkit.getServicesManager().register(EstateService.class,service,runtime.plugin(),org.bukkit.plugin.ServicePriority.Highest);
var decision=decision(1,Recipient.FROZEN);estate.decided=()->List.of(decision);
var identity=org.mockito.Mockito.mock(net.tfminecraft.tlibs.character.CharacterIdentity.class);var ready=new java.util.concurrent.atomic.AtomicBoolean();
org.mockito.Mockito.when(identity.legacyHeirsReady()).thenAnswer(call->ready.get());
org.bukkit.Bukkit.getServicesManager().register(net.tfminecraft.tlibs.character.CharacterIdentity.class,identity,runtime.plugin(),org.bukkit.plugin.ServicePriority.Highest);
estate.reconcile();org.mockito.Mockito.verifyNoInteractions(service);
ready.set(true);estate.reconcile();
org.mockito.Mockito.verify(service).report(org.mockito.ArgumentMatchers.eq(death),org.mockito.ArgumentMatchers.eq("companionpets"),org.mockito.ArgumentMatchers.eq(1L),org.mockito.ArgumentMatchers.isA(Outcome.Done.class));
ready.set(false);Path file=runtime.plugin().getDataFolder().toPath().resolve("pets.yml");
Files.writeString(file,Files.readString(file)+"external-progress: kept\n");assertFalse(store.save());estate.reconcile();
org.mockito.Mockito.verify(service).report(org.mockito.ArgumentMatchers.eq(death),org.mockito.ArgumentMatchers.eq("companionpets"),org.mockito.ArgumentMatchers.eq(1L),org.mockito.ArgumentMatchers.isA(Outcome.Blocked.class));
}
@Test void malformedJournalBlocksButFilesystemFailureRemainsRetryable() throws Exception {
var decision=decision(1,Recipient.character(to));assertInstanceOf(Outcome.Done.class,estate.settle(decision));
Path file=folder.resolve(death+"-1.yml");String original=Files.readString(file);
Expand Down
Loading