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
67 changes: 67 additions & 0 deletions src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import org.apache.commons.lang.WordUtils;
Expand Down Expand Up @@ -69,8 +72,45 @@ public static void requestNodeBenefitSync() {
syncFactionNodeBenefits();
}

/** Each active node's guild and that guild's realm at the last sync, to spot realm changes. */
private static List<Object> lastOwners = List.of();
/** Whether the last sync finished; a failed one is retried even if no owner changed. */
private static boolean synced = false;

/**
* Node benefits sit on each realm's own guild and only node events resync them. A dissolve,
* elevation or relocation moves a node to another realm without one, so {@link #realmCycle}
* resyncs when any active node's guild or realm changed since the last sync, or that sync failed.
*/
public static void syncIfRealmsChanged() {
if (!SimpleFactionsApi.ready()) return;
if (!synced || !sameOwners(owners(), lastOwners)) syncFactionNodeBenefits();
}

private static List<Object> owners() {
List<Object> owners = new ArrayList<>();
for(Node n : nodes) {
if(!n.hasGuild() || !Boolean.TRUE.equals(n.getIsActive())) continue;
Guild g = n.getGuild();
owners.add(n);
owners.add(g);
owners.add(g == null ? null : g.getFaction());
}
return owners;
}

private static boolean sameOwners(List<Object> now, List<Object> before) {
if (now.size() != before.size()) return false;
for (int i = 0; i < now.size(); i++) {
if (now.get(i) != before.get(i)) return false;
}
return true;
}

public static void syncFactionNodeBenefits() {
if (!SimpleFactionsApi.ready()) return;
synced = false;
List<Object> owners = owners();
Map<Faction, Double> prestigeByFaction = new HashMap<>();
Map<Faction, Double> wealthByFaction = new HashMap<>();
for(Node n : nodes) {
Expand All @@ -82,6 +122,7 @@ public static void syncFactionNodeBenefits() {
prestigeByFaction.merge(f, n.getPrestigeGain(), Double::sum);
wealthByFaction.merge(f, n.getWealthModifier(), Double::sum);
}
Set<Guild> realmGuilds = Collections.newSetFromMap(new IdentityHashMap<>());
for(Faction f : FactionManager.getCopy()) {
double prestige = prestigeByFaction.getOrDefault(f, 0.0);
double wealth = wealthByFaction.getOrDefault(f, 0.0);
Expand All @@ -90,7 +131,17 @@ public static void syncFactionNodeBenefits() {
Guild mainGuild = f.getOrCreateMainGuild();
mainGuild.addWealthModifier(new Modifier("Nodes", wealth, true));
mainGuild.updateWealth();
realmGuilds.add(mainGuild);
}
// A realm's own guild that became an ordinary guild (its realm dissolved into an overlord)
// kept its last node value; only realm guilds carry one.
for(Guild g : FactionManager.getAllGuilds()) {
if(g == null || realmGuilds.contains(g)) continue;
if(g.getWealthModifiers().removeIf(m -> "Nodes".equalsIgnoreCase(m.getType()))) g.updateWealth();
}
// Only a finished sync counts, so the realm check retries one that failed part way.
lastOwners = owners;
synced = true;
}

public static int getExtraCapacity(Guild g) {
Expand Down Expand Up @@ -191,6 +242,7 @@ public String getClickedFurniture(Block b) {
public void start() {
particleCycle();
hourCycle();
realmCycle();
new BukkitRunnable()
{
public void run()
Expand Down Expand Up @@ -239,6 +291,21 @@ public void run()
}
}.runTaskTimer(DowsingMain.plugin, 0L, 1200L);
}
/** Every 30 s: resync node benefits when a node's guild or realm changed without a node event. */
public void realmCycle(){
new BukkitRunnable()
{
public void run()
{
if (!DowsingMain.plugin.nodesReady()) return;
try {
syncIfRealmsChanged();
} catch(RuntimeException ex) {
DowsingMain.plugin.getLogger().log(java.util.logging.Level.SEVERE, "Failed to check node realms", ex);
}
}
}.runTaskTimer(DowsingMain.plugin, 0L, 600L);
}
public void particleCycle(){
new BukkitRunnable()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,66 @@ void factionBenefitsAggregateOnlyActiveOwnedNodesAndClearStaleBenefits() {
verify(emptyMain).addWealthModifier(modifier.capture()); assertEquals(0.0, modifier.getValue().getAmount()); verify(emptyMain).updateWealth();
}

@Test
void syncDropsANodeValueLeftOnAGuildThatIsNoLongerItsRealmsOwn() {
Guild formerRealm = mock(Guild.class), ordinary = mock(Guild.class), realm = mock(Guild.class);
List<Modifier> stale = new ArrayList<>(List.of(new Modifier("Bank", 5.0, false), new Modifier("Nodes", 40.0, true)));
when(formerRealm.getWealthModifiers()).thenReturn(stale);
when(ordinary.getWealthModifiers()).thenReturn(new ArrayList<>(List.of(new Modifier("Bank", 1.0, false))));
Faction owner = mock(Faction.class); when(owner.getOrCreateMainGuild()).thenReturn(realm);
f.factions.when(FactionManager::getCopy).thenReturn(new ArrayList<>(List.of(owner)));
// Only the guild a realm names as its own keeps a value, whatever its type says.
when(formerRealm.isBase()).thenReturn(true);
f.factions.when(FactionManager::getAllGuilds).thenReturn(new ArrayList<>(java.util.Arrays.asList(formerRealm, ordinary, realm, null)));
NodeManager.syncFactionNodeBenefits();
assertEquals(List.of("Bank"), stale.stream().map(Modifier::getType).toList());
verify(formerRealm).updateWealth();
verify(ordinary, never()).updateWealth();
verify(realm, never()).getWealthModifiers(); verify(realm).updateWealth();
}

@Test
void theMinuteCheckResyncsOnlyWhenANodesGuildOrRealmChanged() {
Faction first = mock(Faction.class), second = mock(Faction.class);
Guild firstMain = mock(Guild.class), secondMain = mock(Guild.class);
when(first.getOrCreateMainGuild()).thenReturn(firstMain); when(second.getOrCreateMainGuild()).thenReturn(secondMain);
f.factions.when(FactionManager::getCopy).thenReturn(new ArrayList<>(List.of(first, second)));
when(f.guild.getFaction()).thenReturn(first);
when(f.node.getIsActive()).thenReturn(true); when(f.node.getWealthModifier()).thenReturn(10.0);
Node idle = f.nodeAt(f.location);
NodeManager.nodes.addAll(List.of(f.node, idle));
NodeManager.syncFactionNodeBenefits();
verify(first, times(1)).updatePrestige();
NodeManager.syncIfRealmsChanged();
verify(first, times(1)).updatePrestige();
// The guild's realm dissolved into another one: the node's value has to follow it.
when(f.guild.getFaction()).thenReturn(second);
NodeManager.syncIfRealmsChanged();
verify(first, times(2)).updatePrestige();
ArgumentCaptor<Modifier> modifier = ArgumentCaptor.forClass(Modifier.class);
verify(secondMain, times(2)).addWealthModifier(modifier.capture());
assertEquals(10.0, modifier.getValue().getAmount());
NodeManager.syncIfRealmsChanged();
verify(first, times(2)).updatePrestige();
Guild other = mock(Guild.class); when(other.getFaction()).thenReturn(second);
when(f.node.getGuild()).thenReturn(other);
NodeManager.syncIfRealmsChanged();
verify(first, times(3)).updatePrestige();
when(f.node.getGuild()).thenReturn(null);
NodeManager.syncIfRealmsChanged();
verify(first, times(4)).updatePrestige();
NodeManager.nodes.remove(idle);
NodeManager.syncIfRealmsChanged();
verify(first, times(4)).updatePrestige();
NodeManager.nodes.clear();
NodeManager.syncIfRealmsChanged();
verify(first, times(5)).updatePrestige();
net.tfminecraft.dowsing.testsupport.FactionFixture.api.when(net.tfminecraft.simplefactions.api.SimpleFactionsApi::ready).thenReturn(false);
NodeManager.nodes.add(f.node);
NodeManager.syncIfRealmsChanged();
verify(first, times(5)).updatePrestige();
}

@Test
void countsCapacityAndUpkeepRespectOwnershipSpecialNodesAndPurchasedLimits() {
assertEquals(0, NodeManager.getExtraCapacity(null)); assertFalse(NodeManager.canPurchaseCapacity(null));
Expand Down Expand Up @@ -211,7 +271,7 @@ void timersUseTheirIntendedCadenceAndRunParticlesChecksAndEfficiencyGrowth() {
Node claimed = f.node, unclaimed = f.nodeAt(f.location);
when(unclaimed.isClaimable()).thenReturn(true); NodeManager.nodes.addAll(List.of(claimed, unclaimed));
try (Cycles cycles = new Cycles()) {
f.manager.start(); assertEquals(java.util.Set.of(5L, 1200L, 72000L), cycles.tasks.keySet());
f.manager.start(); assertEquals(java.util.Set.of(5L, 600L, 1200L, 72000L), cycles.tasks.keySet());
cycles.tasks.get(5L).run(); verify(claimed).check();
verify(f.world).spawnParticle(Particle.HAPPY_VILLAGER, new Location(f.world, 2.5, 65, 3.5), 10);
cycles.tasks.get(72000L).run(); verify(claimed).growEfficiency(); verify(unclaimed).growEfficiency();
Expand All @@ -234,6 +294,53 @@ void productionCycleCompletesRealNodeOutputThenConsumesTheNextInputBatch() {
}
}

@Test
void aSyncThatFailsPartWayIsRetriedByTheRealmCheck() {
Faction realm = mock(Faction.class); Guild main = mock(Guild.class);
when(realm.getOrCreateMainGuild()).thenReturn(main);
when(f.guild.getFaction()).thenReturn(realm); when(f.node.getIsActive()).thenReturn(true);
NodeManager.nodes.add(f.node);
NodeManager.syncFactionNodeBenefits();
Faction moved = mock(Faction.class); when(f.guild.getFaction()).thenReturn(moved);
f.factions.when(FactionManager::getCopy).thenThrow(new IllegalStateException("half way"));
assertThrows(IllegalStateException.class, NodeManager::syncIfRealmsChanged);
f.factions.when(FactionManager::getCopy).thenReturn(new ArrayList<>(List.of(moved)));
Guild movedMain = mock(Guild.class); when(moved.getOrCreateMainGuild()).thenReturn(movedMain);
NodeManager.syncIfRealmsChanged();
verify(moved).updatePrestige();
NodeManager.syncIfRealmsChanged();
verify(moved, times(1)).updatePrestige();
// A node event's sync that fails with the same owners is retried too.
f.factions.when(FactionManager::getCopy).thenThrow(new IllegalStateException("again"));
assertThrows(IllegalStateException.class, NodeManager::requestNodeBenefitSync);
f.factions.when(FactionManager::getCopy).thenReturn(new ArrayList<>(List.of(moved)));
NodeManager.syncIfRealmsChanged();
verify(moved, times(2)).updatePrestige();
NodeManager.syncIfRealmsChanged();
verify(moved, times(2)).updatePrestige();
}

@Test
void theRealmCycleResyncsWhenReadyAndLogsAFailureInsteadOfStopping() {
Faction realm = mock(Faction.class); Guild main = mock(Guild.class);
when(realm.getOrCreateMainGuild()).thenReturn(main);
f.factions.when(FactionManager::getCopy).thenReturn(new ArrayList<>(List.of(realm)));
when(f.guild.getFaction()).thenReturn(realm); when(f.node.getIsActive()).thenReturn(true);
NodeManager.nodes.add(f.node);
try (Cycles cycles = new Cycles()) {
f.manager.start();
when(DowsingMain.plugin.nodesReady()).thenReturn(false);
cycles.tasks.get(600L).run();
verify(realm, never()).updatePrestige();
when(DowsingMain.plugin.nodesReady()).thenReturn(true);
cycles.tasks.get(600L).run();
verify(realm).updatePrestige();
when(f.node.getIsActive()).thenThrow(new IllegalStateException("broken node"));
cycles.tasks.get(600L).run();
verify(f.logger).log(eq(java.util.logging.Level.SEVERE), eq("Failed to check node realms"), any(IllegalStateException.class));
}
}

@Test
void failedOutputKeepsTheCyclePendingAndExceptionsDoNotStopOtherNodes() {
Node inactive = f.nodeAt(f.location), unowned = f.nodeAt(f.location), broken = f.nodeAt(f.location), pending = f.node;
Expand Down