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
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
<denareconomy.version>0.3.0</denareconomy.version>
<rpcharacters.version>2.16.0</rpcharacters.version>
<tlibs.version>2.4.1</tlibs.version>
<vehicleframework.version>2.10.0</vehicleframework.version>
<vfbuilders.version>2.1.0</vfbuilders.version>
<vehicleframework.version>2.13.0</vehicleframework.version>
<vfbuilders.version>2.2.0</vfbuilders.version>
</properties>

<dependencies>
Expand Down
159 changes: 144 additions & 15 deletions src/main/java/net/tfminecraft/simplefactions/SimpleFactions.java
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,15 @@
import net.tfminecraft.simplefactions.vehicles.maintenance.VehicleUpkeepService;
import net.tfminecraft.simplefactions.vehicles.maintenance.DenarEconomyPlayerBank;
import net.tfminecraft.simplefactions.player.PlayerEconomyManager;
import net.tfminecraft.simplefactions.vehicles.owners.LeaderStamps;
import net.tfminecraft.simplefactions.vehicles.owners.VehicleClaimsOff;
import net.tfminecraft.simplefactions.vehicles.owners.VehicleOwnerApi;
import net.tfminecraft.simplefactions.vehicles.owners.VehicleOwnerMigration;
import net.tfminecraft.simplefactions.vehicles.owners.VehicleOwnerWrites;
import net.tfminecraft.simplefactions.vehicles.owners.VehicleOwnershipGate;
import net.tfminecraft.simplefactions.vehicles.owners.VehicleOwnershipServices;
import net.tfminecraft.simplefactions.vehicles.owners.VehicleQuarantine;
import net.tfminecraft.simplefactions.vehicles.owners.VehicleUpkeepReconcile;

public class SimpleFactions extends JavaPlugin{
public static FileConfiguration config;
Expand Down Expand Up @@ -242,10 +251,17 @@ public class SimpleFactions extends JavaPlugin{
private final VehicleHandoverListener vehicleHandoverListener = new VehicleHandoverListener(
vehicleHandoverSessionManager, vehicleHandoverService, vehicleFeeConfirmations);
private final VehicleReclaimFeeListener vehicleReclaimFeeListener =
new VehicleReclaimFeeListener(vehicleFeeStore, vehicleFeeConfirmations, this::saveVehicleFees);
new VehicleReclaimFeeListener(vehicleFeeStore, this::saveVehicleFees);
private boolean registrationFeeRegistered;
private boolean incompatibleCharacterApi;
private boolean vehicleIntegrationRegistered = false;
/** Once-guard for {@link #startVehicles()}, which both start() and VehicleFramework's enable can trigger. */
private boolean vehiclesStarted;
private static final long VEHICLE_RECONCILE_TICKS = 24L * 60L * 60L * 20L;
// Seams for tests; production probes VehicleFramework and copies its SQLite database.
java.util.function.Supplier<VehicleOwnerApi> vehicleApiProbe = VehicleOwnerWrites::probe;
java.util.function.Function<java.nio.file.Path, VehicleOwnerMigration.Backups> vehicleBackups =
VehicleOwnerMigration.SqliteBackups::new;
VehicleOwnerMigration.Identity vehicleIdentity = new VehicleOwnerMigration.LiveIdentity();
private boolean constructionFreezeRegistered;
private final PlayerEconomyManager playerEconomyManager = new PlayerEconomyManager();
private final VehicleUpkeepService vehicleUpkeepService = new VehicleUpkeepService(
Expand Down Expand Up @@ -285,6 +301,7 @@ public void onEnable() {
started = false;
startFailed = false;
servicesBegun = false;
vehiclesStarted = false;
config = getConfig();
plugin = this;
FactionManager.inv = inventoryManager;
Expand All @@ -297,6 +314,13 @@ public void onEnable() {
getServer().getPluginManager().disablePlugin(this);
return;
}
// VehicleFramework may enable inside this enable window or after start(); startVehicles() runs once.
getServer().getPluginManager().registerEvents(new Listener() {
@EventHandler
public void onPluginEnable(PluginEnableEvent event) {
if ("VehicleFramework".equalsIgnoreCase(event.getPlugin().getName())) startVehicles();
}
}, this);
awaitIdentity();
}

Expand Down Expand Up @@ -341,6 +365,7 @@ static boolean identityReady() {
/** A persisted operation must finish before another character or money mutation is allowed. */
public void failFinancialState(Throwable error) {
started = false;
LeaderStamps.enable(false);
startupComplete = false;
startFailed = true;
getServer().getServicesManager().unregisterAll(this);
Expand Down Expand Up @@ -384,6 +409,12 @@ void start() {
try { startServices(); }
catch (Exception error) { failFinancialState(error); }
if (!started) return;
// Factions are up; vehicles follow once VehicleFramework is enabled too. The ready event says nothing about vehicles.
startVehicles();
if (!started) return;
if (!vehiclesStarted) {
getLogger().info("Vehicle ownership waits for VehicleFramework to enable; factions run without vehicles until then");
}
getServer().getPluginManager().callEvent(new net.tfminecraft.simplefactions.events.SimpleFactionsReadyEvent());
}

Expand Down Expand Up @@ -475,8 +506,6 @@ private void startServices() {
FactionManager.updateAllPrestigeConverged();
net.tfminecraft.simplefactions.identity.death.CharacterDeaths.load(getDataFolder().toPath());
net.tfminecraft.simplefactions.economy.TreasuryActions.recover(getDataFolder().toPath().resolve("economy/callers"));
if (getServer().getPluginManager().isPluginEnabled("VehicleFramework"))
net.tfminecraft.simplefactions.vehicles.registry.VehiclePayerMigration.startup(getDataFolder().toPath(), vehicleRegistry);
net.tfminecraft.simplefactions.vehicles.fees.VehiclePayments.startup(getDataFolder().toPath(), vehicleFeeStore, vehicleMaintenanceStore);
net.tfminecraft.simplefactions.economy.ExternalIncome.recover(getDataFolder().toPath());
realmSettlementService = new net.tfminecraft.simplefactions.economy.settlement.RealmSettlementService(getDataFolder().toPath(), db);
Expand Down Expand Up @@ -540,6 +569,9 @@ public void onDisable() {
if (identityPoll != null) identityPoll.cancel();
identityPoll = null;
started = false;
LeaderStamps.enable(false);
VehicleOwnerWrites.use(null);
VehicleQuarantine.use(null);
getServer().getServicesManager().unregisterAll(this);
net.tfminecraft.simplefactions.identity.death.CharacterDeaths.stop();
net.tfminecraft.simplefactions.vehicles.fees.VehiclePayments.stop();
Expand Down Expand Up @@ -895,23 +927,119 @@ public void onPluginEnable(PluginEnableEvent event) {
net.tfminecraft.simplefactions.integration.rpcharacters.chat.RpCharactersChatIntegration.register();
}

/** The VFBuilders construction freeze and registration fee, which need no vehicle stage. */
private void registerVehicleIntegrationHooks() {
getServer().getPluginManager().registerEvents(new Listener() {
@EventHandler
public void onPluginEnable(PluginEnableEvent event) {
if ("VFBuilders".equalsIgnoreCase(event.getPlugin().getName())
|| "VehicleFramework".equalsIgnoreCase(event.getPlugin().getName())) {
registerVehicleIntegration();
registerConstructionFreeze();
registerRegistrationFee();
}
}
}, this);
registerVehicleIntegration();
registerConstructionFreeze();
registerRegistrationFee();
}

/**
* The vehicle stage, once factions have started and VehicleFramework is enabled:
* payer migration, the owner migration, the leader and upkeep reconciles, then the
* vehicle listeners, and last the ownership authority VehicleFramework waits for.
* Without VehicleFramework 2.13 in characters mode only the payer migration and the
* listeners run, with claims and restamps off.
*/
void startVehicles() {
if (vehiclesStarted || !started || !getServer().getPluginManager().isPluginEnabled("VehicleFramework")) {
return;
}
vehiclesStarted = true;
VehicleOwnerApi api = vehicleApiProbe.get();
VehicleOwnerWrites.use(api);
boolean full = VehicleOwnerWrites.ready();
java.nio.file.Path folder = getDataFolder().toPath();
VehicleQuarantine quarantine = new VehicleQuarantine(folder);
VehicleQuarantine.use(quarantine);
if (api != null) VehicleOwnershipServices.registerQuarantine(getServer().getServicesManager(), this, quarantine);
try {
net.tfminecraft.simplefactions.vehicles.registry.VehiclePayerMigration.startup(folder, vehicleRegistry);
if (full) {
VehicleOwnerMigration.Outcome outcome = new VehicleOwnerMigration(folder, api, vehicleRegistry,
vehicleIdentity, vehicleBackups.apply(vehicleDatabase())).run();
if (outcome.ran()) {
getLogger().info("Vehicle owner migration: " + outcome.migrated() + " migrated, " + outcome.unchanged()
+ " unchanged, " + outcome.quarantined() + " quarantined, " + outcome.gone() + " gone");
}
}
quarantine.load(VehicleOwnerMigration.quarantine(folder));
} catch (Exception error) {
failFinancialState(error);
return;
}
if (!full) {
getLogger().severe("VehicleFramework 2.13 or newer in characters mode is needed for vehicle ownership; "
+ "vehicle claims and leader stamps are off, and upkeep is charged from registry rows");
} else {
try {
LeaderStamps.reconcile(vehicleRegistry);
int filled = net.tfminecraft.simplefactions.vehicles.registry.RegistryAccounts.fill(folder, vehicleRegistry,
character -> net.tfminecraft.simplefactions.identity.Members.accountId(
net.tfminecraft.simplefactions.identity.Members.key(character)),
java.util.List.of(VehicleOwnerMigration.accounts(folder),
net.tfminecraft.simplefactions.vehicles.registry.VehiclePayerMigration.accounts(folder)));
if (filled > 0) getLogger().info("Cached the account on " + filled + " personal vehicle rows");
quarantine.setFindings(upkeepReconcile().run());
} catch (Exception error) {
getLogger().log(java.util.logging.Level.SEVERE, "Vehicle reconcile failed, so vehicle listeners and the "
+ "ownership authority stay off until a restart; VehicleFramework refuses player ownership changes", error);
return;
}
}
registerVehicleIntegration(full);
if (full) {
VehicleOwnershipServices.registerAuthority(getServer().getServicesManager(), this,
() -> started && LeaderStamps.enabled());
LeaderStamps.enable(true);
getServer().getScheduler().runTaskTimer(this, this::reconcileVehicleUpkeep,
VEHICLE_RECONCILE_TICKS, VEHICLE_RECONCILE_TICKS);
}
}

private java.nio.file.Path vehicleDatabase() {
return getServer().getPluginManager().getPlugin("VehicleFramework").getDataFolder().toPath()
.resolve("data/vehicles.db");
}

/** The daily upkeep reconcile; its findings replace the previous ones in the staff list. */
void reconcileVehicleUpkeep() {
try {
VehicleQuarantine.current().setFindings(upkeepReconcile().run());
} catch (RuntimeException error) {
getLogger().log(java.util.logging.Level.WARNING, "Daily vehicle upkeep reconcile failed", error);
}
}

private VehicleUpkeepReconcile upkeepReconcile() {
return new VehicleUpkeepReconcile(VehicleOwnerWrites.api(), vehicleRegistry,
(vehicle, type, character, account) -> {
var payments = net.tfminecraft.simplefactions.vehicles.fees.VehiclePayments.current();
return payments != null && payments.personal(vehicle, type, character, account);
},
SimpleFactions::leadsAFaction,
character -> net.tfminecraft.simplefactions.identity.Members.accountId(
net.tfminecraft.simplefactions.identity.Members.key(character)),
getLogger());
}

static boolean leadsAFaction(java.util.UUID character) {
String key = net.tfminecraft.simplefactions.identity.Members.key(character);
for (Faction faction : FactionManager.factions) {
if (key.equals(faction.getLeader())) return true;
}
return false;
}

private void registerRegistrationFee() {
if (registrationFeeRegistered || !getServer().getPluginManager().isPluginEnabled("VFBuilders")
|| !getServer().getPluginManager().isPluginEnabled("VehicleFramework")) {
Expand Down Expand Up @@ -946,24 +1074,25 @@ private void registerConstructionFreeze() {
net.tfminecraft.simplefactions.war.freeze.VfBuildersConstructionFreeze.register(this);
}

private void registerVehicleIntegration() {
if (vehicleIntegrationRegistered) {
return;
}
if (!getServer().getPluginManager().isPluginEnabled("VehicleFramework")) {
return;
}
/** Step 5 of the vehicle stage, which runs once per start. */
private void registerVehicleIntegration(boolean full) {
getServer().getPluginManager().registerEvents(vehicleIntegrationListener, this);
getServer().getPluginManager().registerEvents(vehicleRegistryClaimListener, this);
getServer().getPluginManager().registerEvents(vehicleTransferListener, this);
getServer().getPluginManager().registerEvents(factionVehicleReleaseListener, this);
getServer().getPluginManager().registerEvents(vehicleMaintenancePayListener, this);
getServer().getPluginManager().registerEvents(vehicleMaintenanceRepairListener, this);
getServer().getPluginManager().registerEvents(vehicleSpawnListener, this);
getServer().getPluginManager().registerEvents(battleVehicleEligibilityListener, this);
getServer().getPluginManager().registerEvents(vehicleHandoverListener, this);
getServer().getPluginManager().registerEvents(vehicleReclaimFeeListener, this);
vehicleIntegrationRegistered = true;
if (full) {
getServer().getPluginManager().registerEvents(vehicleSpawnListener, this);
getServer().getPluginManager().registerEvents(new VehicleOwnershipGate(vehicleRegistry,
vehicleRegistryClaimService, vehicleFeeStore, vehicleFeeConfirmations, this::saveVehicleFees,
getLogger()), this);
} else {
getServer().getPluginManager().registerEvents(new VehicleClaimsOff(), this);
}
// VehicleFramework loads its vehicles as it enables, so wait before reading owners.
getServer().getScheduler().runTaskLater(this, this::recordVehicleOwners, 200L);
if (getServer().getPluginManager().isPluginEnabled("VFBuilders")) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package net.tfminecraft.simplefactions.events;

import org.bukkit.Bukkit;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;

import net.tfminecraft.simplefactions.objects.Faction;

/**
* Fired after a faction's leader changes, from {@code Faction.setLeader} and
* {@code Faction.succeedDeadLeader}, the only two writers of the leader. Loading
* saved factions does not fire it.
*/
public class FactionLeaderChangedEvent extends Event {
private static final HandlerList HANDLERS = new HandlerList();

private final Faction faction;
private final String previousLeader;
private final String newLeader;

public FactionLeaderChangedEvent(Faction faction, String previousLeader, String newLeader) {
this.faction = faction;
this.previousLeader = previousLeader;
this.newLeader = newLeader;
}

/**
* Fires the event on the main thread of a running server. A change made elsewhere is not
* announced; the vehicle backstop and the next boot's reconcile catch its stamps.
*/
public static void fire(Faction faction, String previousLeader, String newLeader) {
if (java.util.Objects.equals(previousLeader, newLeader) || Bukkit.getServer() == null
|| Bukkit.getPluginManager() == null || !Bukkit.isPrimaryThread()) {
return;
}
Bukkit.getPluginManager().callEvent(new FactionLeaderChangedEvent(faction, previousLeader, newLeader));
}

public Faction getFaction() {
return faction;
}

/** The previous leader's member key, possibly null or blank. */
public String getPreviousLeader() {
return previousLeader;
}

/** The new leader's member key, possibly null or blank. */
public String getNewLeader() {
return newLeader;
}

@Override
public HandlerList getHandlers() {
return HANDLERS;
}

public static HandlerList getHandlerList() {
return HANDLERS;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package net.tfminecraft.simplefactions.identity.death;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import java.util.ArrayList;
import java.util.HashMap;
Expand Down Expand Up @@ -44,18 +46,30 @@ Optional<UUID> get(UUID characterId) {
/**
* Journals written before the field existed use the first guild listing the
* character in the rights snapshots, in their order, without civil-war mapping.
* A malformed snapshot gives no entry rather than guessing past it.
*/
static UUID incarnation(DeathJournal journal) {
if (journal.lockGuildIncarnation != null) return journal.lockGuildIncarnation;
JsonPrimitive key = new JsonPrimitive(journal.characterId.toString());
for (DeathJournal.Step step : journal.steps) {
if (!step.kind.equals("rights") || !step.plan.has("guilds")) continue;
for (JsonElement guild : step.plan.getAsJsonArray("guilds")) {
if (guild.getAsJsonObject().getAsJsonArray("members").contains(key)) {
return UUID.fromString(guild.getAsJsonObject().get("incarnation").getAsString());
if (!(step.plan.get("guilds") instanceof JsonArray guilds)) return null;
for (JsonElement guild : guilds) {
if (!(guild instanceof JsonObject object) || !(object.get("members") instanceof JsonArray members)) {
return null;
}
if (members.contains(key)) return uuid(object.get("incarnation"));
}
}
return null;
}

private static UUID uuid(JsonElement value) {
if (!(value instanceof JsonPrimitive primitive) || !primitive.isString()) return null;
try {
return UUID.fromString(primitive.getAsString());
} catch (IllegalArgumentException invalid) {
return null;
}
}
}
Loading
Loading