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
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package net.tfminecraft.simplefactions.government.stability;

import java.util.IdentityHashMap;
import java.util.Map;

import org.bukkit.Bukkit;

import net.tfminecraft.denareconomy.DenarEconomy;
import net.tfminecraft.denareconomy.ledger.Ledger;
import net.tfminecraft.simplefactions.guild.Guild;

/**
* Whether each realm guild is bankrupt, read once per trade recalculation on the server thread.
* The flood asks for a province owner's stability once per foreign guild it reaches, and each ask
* reads the realm guild's treasury from the ledger. During a run on the server thread nothing
* else can post to a treasury or remove a bank, and nothing in a recalculation does, so every ask
* in that run gets the same answer. Everything else in stability, including unrest that decays
* over time, is still read live on every ask.
*
* <p>Runs off the server thread (async previews) read live as before: the server can change a
* guild's bank while they run. If the ledger is closed, every ask reads live too, because a
* closed ledger reads every balance as zero.
*/
public final class BankruptcyMemo {
private static final ThreadLocal<Map<Guild, Boolean>> RUN = new ThreadLocal<>();

private BankruptcyMemo() {}

/** What {@link #begin} replaced, so {@link #end} can put it back. */
public static final class Run {
private final Map<Guild, Boolean> outer;

private Run(Map<Guild, Boolean> outer) {
this.outer = outer;
}
}

/** Starts a run on this thread. A nested run, such as a preview, gets its own answers. */
public static Run begin() {
Run run = new Run(RUN.get());
if (onServerThread()) RUN.set(new IdentityHashMap<>());
else RUN.remove();
return run;
}

public static void end(Run run) {
if (run.outer == null) RUN.remove();
else RUN.set(run.outer);
}

/** {@link Guild#isBankrupt()}, read once per guild in a server-thread run and live otherwise. */
public static boolean isBankrupt(Guild guild) {
Map<Guild, Boolean> run = RUN.get();
if (run == null) return guild.isBankrupt();
if (!ledgerOpen()) {
// A failed read closes the ledger for good, and answers from before it are stale.
run.clear();
return guild.isBankrupt();
}
Boolean bankrupt = run.get(guild);
if (bankrupt == null) {
bankrupt = guild.isBankrupt();
// Keep only answers the ledger stayed open for; this very read may have closed it.
if (ledgerOpen()) run.put(guild, bankrupt);
}
return bankrupt;
}

private static boolean onServerThread() {
return Bukkit.getServer() != null && Bukkit.isPrimaryThread();
}

private static boolean ledgerOpen() {
Ledger ledger = DenarEconomy.getLedger();
return ledger != null && ledger.isOpen();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public static StabilityFacts facts(Faction faction) {
facts.government = law(faction, "government", "autocracy");
facts.electedLeadership = "elected".equalsIgnoreCase(law(faction, "leadership", "fixed"));
facts.provinces = faction.getProvinces() == null ? 0 : faction.getProvinces().size();
facts.bankrupt = faction.getOrCreateMainGuild() != null && faction.getOrCreateMainGuild().isBankrupt();
facts.bankrupt = faction.getOrCreateMainGuild() != null && BankruptcyMemo.isBankrupt(faction.getOrCreateMainGuild());
if (faction.getGovernment() != null && faction.getGovernment().getStabilityModifiers() != null) {
faction.getGovernment().getStabilityModifiers().forEach(modifier -> {
if (modifier != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -642,9 +642,8 @@ public static Guild getGuildByLeader(String leader) {

public static Guild getGuildByString(String id) {
for(Faction f : factions) {
for(Guild guild : f.getGuildHandler().getGuilds()) {
if(guild.getId().equalsIgnoreCase(id)) return guild;
}
Guild guild = f.getGuildHandler().findGuild(id);
if(guild != null) return guild;
}
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import net.tfminecraft.simplefactions.war.resolution.PillageTradeHit;
import net.tfminecraft.simplefactions.enums.GuildModifier;
import net.tfminecraft.simplefactions.government.proposal.TaxTarget;
import net.tfminecraft.simplefactions.government.stability.BankruptcyMemo;
import net.tfminecraft.simplefactions.objects.handler.TaxHandler;
import net.tfminecraft.simplefactions.laws.Law;
import net.tfminecraft.simplefactions.laws.LawGroup;
Expand Down Expand Up @@ -116,6 +117,7 @@ public void recalculate() {
return;
}
InstallationAccess.beginRecalculation();
BankruptcyMemo.Run memo = BankruptcyMemo.begin();
try {
installationAccess = new HashMap<>();
TradeGraph.refreshIfLive(this);
Expand All @@ -130,6 +132,7 @@ public void recalculate() {
recalculateProsperity();
for(Guild guild : FactionManager.getAllGuilds()) getIncome(guild);
} finally {
BankruptcyMemo.end(memo);
InstallationAccess.endRecalculation();
}
}
Expand All @@ -145,6 +148,7 @@ public void recalculateQuiet(List<Guild> guilds, List<Faction> factions) {
previewGuilds = guilds == null ? List.of() : List.copyOf(guilds);
previewFactions = factions == null ? List.of() : List.copyOf(factions);
InstallationAccess.beginRecalculation();
BankruptcyMemo.Run memo = BankruptcyMemo.begin();
try {
installationAccess = new HashMap<>();
TradeGraph.refreshIfLive(this);
Expand All @@ -161,6 +165,7 @@ public void recalculateQuiet(List<Guild> guilds, List<Faction> factions) {
}
recalculateProsperity();
} finally {
BankruptcyMemo.end(memo);
InstallationAccess.endRecalculation();
}
}
Expand All @@ -176,6 +181,7 @@ public void recalculateForSingleGuild(Guild g, boolean save) {
return;
}
InstallationAccess.beginRecalculation();
BankruptcyMemo.Run memo = BankruptcyMemo.begin();
try {
installationAccess = new HashMap<>();
TradeGraph.refreshIfLive(this);
Expand All @@ -189,6 +195,7 @@ public void recalculateForSingleGuild(Guild g, boolean save) {
}
}
} finally {
BankruptcyMemo.end(memo);
InstallationAccess.endRecalculation();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import java.util.List;
import java.util.Map;

import org.bukkit.Bukkit;

import net.tfminecraft.simplefactions.guild.Guild;
import net.tfminecraft.simplefactions.managers.ProvinceManager;
import net.tfminecraft.simplefactions.objects.Faction;
Expand All @@ -23,6 +25,23 @@ public List<Guild> getGuilds() {
return new ArrayList<>(guilds.values());
}

/**
* The first guild in {@link #getGuilds()} order whose id matches, ignoring case. The server
* thread reads the map directly; async previews reach this too, so they read a copy, as
* before, and a guild created or removed meanwhile cannot break their iteration.
*/
public Guild findGuild(String id) {
Iterable<Guild> each = onServerThread() ? guilds.values() : getGuilds();
for (Guild guild : each) {
if (guild.getId().equalsIgnoreCase(id)) return guild;
}
return null;
}

private static boolean onServerThread() {
return Bukkit.getServer() != null && Bukkit.isPrimaryThread();
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
public List<Guild> getReleasableGuilds() {
List<Guild> releasable = new ArrayList<>();
for(Guild g : guilds.values()) {
Expand Down
Loading
Loading