Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
a36ff51
wip: member keys core
ryanbarlow97 Oct 12, 2026
c473b20
wip: commands and identity on member keys
ryanbarlow97 Oct 12, 2026
16290fe
wip: migration, quarantine, waiting state
ryanbarlow97 Oct 12, 2026
700cbab
refactor: key government, guilds and factions by character
ryanbarlow97 Oct 12, 2026
f261d29
refactor: key war, diplomacy and requests by character
ryanbarlow97 Oct 12, 2026
5204182
wip: tests for members, migration and quarantine
ryanbarlow97 Oct 12, 2026
86a32f0
Merge branch 'stage5/members-gov' into stage5/members
ryanbarlow97 Oct 12, 2026
3aac833
Merge branch 'stage5/members-war' into stage5/members
ryanbarlow97 Oct 12, 2026
624b887
refactor: key menus and tab completion by character
ryanbarlow97 Oct 12, 2026
fa0d983
Merge branch 'stage5/members-views' into stage5/members
ryanbarlow97 Oct 12, 2026
2062fde
wip: update core tests for member keys
ryanbarlow97 Oct 12, 2026
375b8a8
test: waiting state, readiness and migration at start
ryanbarlow97 Oct 12, 2026
f76b305
feat: mark guilds whose leader is still quarantined
ryanbarlow97 Oct 12, 2026
43f20f9
test: cover characterless commands, API readiness and migration edges
ryanbarlow97 Oct 12, 2026
e91ff54
test: every declared command waits for identity
ryanbarlow97 Oct 12, 2026
eec0e0a
refactor: key misc areas by character
ryanbarlow97 Oct 12, 2026
5b237fa
Merge branch 'stage5/members-misc' into stage5/members
ryanbarlow97 Oct 12, 2026
099e45a
fix: tie vehicle owner tags, payouts and inactivity to member keys
ryanbarlow97 Oct 12, 2026
0884db6
fix: capture the handover owner's character and key test characters c…
ryanbarlow97 Oct 12, 2026
39ec2d5
test: cover character start logins, quorum gaps and espionage keys
ryanbarlow97 Oct 12, 2026
b0186f5
wip: paused for handoff
ryanbarlow97 Oct 12, 2026
d8baabf
test: cover absent votes and optional campaign paths
ryanbarlow97 Oct 12, 2026
768e40c
feat: expose character faction APIs and captured battle identities
ryanbarlow97 Oct 12, 2026
bdd8420
feat: add durable treasury foundations and captured warband identities
ryanbarlow97 Oct 12, 2026
fa4b129
feat: journal character death cleanup and owed payments
ryanbarlow97 Oct 12, 2026
761c916
fix: retain private office death notice after durable cleanup
ryanbarlow97 Oct 12, 2026
8312b60
feat: plan vehicle upkeep in the realm settlement
ryanbarlow97 Oct 12, 2026
e515d26
feat: plan durable realm settlement and recovery
ryanbarlow97 Oct 12, 2026
919145b
feat: recover daily preparation and retire legacy payouts
ryanbarlow97 Oct 12, 2026
8ecd5ed
feat: journal character treasury caller actions
ryanbarlow97 Oct 12, 2026
d8c25cc
feat: journal treasury charges and exact capital relocation effects
ryanbarlow97 Oct 12, 2026
3134e22
feat: journal character vehicle payments and bootstrap legacy payers
ryanbarlow97 Oct 12, 2026
3d4733b
feat: recover treasury consumers before exposing readiness
ryanbarlow97 Oct 12, 2026
f7c10a9
test: verify relocation consent at journal boundary
ryanbarlow97 Oct 12, 2026
12badff
fix: reject stale treasury participants before posting
ryanbarlow97 Oct 12, 2026
5391a9f
fix: bind loan offers to issuer incarnation
ryanbarlow97 Oct 12, 2026
20d6ccb
test: cover vehicle fee recovery and validation paths
ryanbarlow97 Oct 12, 2026
f5149ad
test: cover settlement, lifecycle and casino recovery paths
ryanbarlow97 Oct 12, 2026
921e207
test: isolate settlement clock from leaked static day
ryanbarlow97 Oct 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
12 changes: 9 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@

<groupId>net.tfminecraft</groupId>
<artifactId>simplefactions</artifactId>
<version>main-SNAPSHOT</version>
<version>4.0.0</version>

<properties>
<argLine></argLine>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.proc>full</maven.compiler.proc>
<denareconomy.version>0.2.4</denareconomy.version>
<denareconomy.version>0.3.0</denareconomy.version>
<rpcharacters.version>2.16.0</rpcharacters.version>
<tlibs.version>2.3.0</tlibs.version>
<tlibs.version>2.4.1</tlibs.version>
<vehicleframework.version>2.10.0</vehicleframework.version>
<vfbuilders.version>2.1.0</vfbuilders.version>
</properties>
Expand Down Expand Up @@ -158,6 +158,12 @@
<version>5.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.49.1.0</version>
<scope>test</scope>
</dependency>
</dependencies>

<repositories>
Expand Down
215 changes: 177 additions & 38 deletions src/main/java/net/tfminecraft/simplefactions/SimpleFactions.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.bukkit.scheduler.BukkitTask;

import net.tfminecraft.simplefactions.Cache;
import net.tfminecraft.simplefactions.migration.CharacterMigration;
import net.tfminecraft.simplefactions.map.ProvinceGrid;
import net.tfminecraft.simplefactions.map.infra.TrackProvinceCache;
import net.tfminecraft.simplefactions.map.presence.ProvincePresenceListener;
Expand Down Expand Up @@ -139,6 +140,8 @@ public class SimpleFactions extends JavaPlugin{
private final MercenaryCommandManager mercenaryCommandManager = new MercenaryCommandManager();
private final BankManager bankManager = new BankManager();
private final Database db = new Database();
private net.tfminecraft.simplefactions.economy.settlement.RealmSettlementService realmSettlementService;
public net.tfminecraft.simplefactions.economy.settlement.RealmSettlementService getRealmSettlementService() { return realmSettlementService; }
private final FactionManager factionManager = new FactionManager();
private final TitleManager titleManager = new TitleManager();
private final PlayerManager playerManager = new PlayerManager();
Expand Down Expand Up @@ -224,7 +227,7 @@ public class SimpleFactions extends JavaPlugin{
private final BattleVehicleEligibilityService.Listener battleVehicleEligibilityListener =
new BattleVehicleEligibilityService.Listener(vehicleRegistry);
private final VehicleMaintenancePayService vehicleMaintenancePayService =
new VehicleMaintenancePayService(vehicleMaintenanceStore, DenarEconomyPlayerBank.INSTANCE);
new VehicleMaintenancePayService(vehicleMaintenanceStore);
private final VehicleMaintenanceRepairListener vehicleMaintenanceRepairListener =
new VehicleMaintenanceRepairListener(vehicleMaintenanceStore);
private final VehicleMaintenancePayListener vehicleMaintenancePayListener =
Expand Down Expand Up @@ -255,18 +258,33 @@ public class SimpleFactions extends JavaPlugin{
new VehicleMaintenanceDecayTask();

private boolean startupComplete;
/** Set when loading began, so a failed start still releases what it took. */
private boolean servicesBegun;
/** True once the data is migrated and every service has started. */
private volatile boolean started;
private boolean startFailed;
private BukkitTask identityPoll;
private CharacterMigration.Heirs migrationHeirs = CharacterMigration.SHARED;
/** Every command in plugin.yml; each answers "starting up" until the plugin has started. */
static final java.util.List<String> COMMANDS = java.util.List.of(
"faction", "guild", "warband", "battle", "raid", "war", "movement", "ledger", "company", "mercenaries");
private static final long IDENTITY_POLL_TICKS = 5L * 20L;

@Override
public void onEnable() {
try {
net.tfminecraft.tlibs.character.Characters.available();
net.tfminecraft.tlibs.character.Characters.legacyHeirsReady();
net.tfminecraft.tlibs.character.Characters.deathId(new java.util.UUID(0L, 0L));
} catch (LinkageError error) {
incompatibleCharacterApi = true;
getLogger().severe("SimpleFactions requires TLibs 2.3.0 or newer; disabling.");
getLogger().severe("SimpleFactions requires TLibs 2.4.1 or newer; disabling.");
getServer().getPluginManager().disablePlugin(this);
return;
}
startupComplete = false;
started = false;
startFailed = false;
servicesBegun = false;
config = getConfig();
plugin = this;
FactionManager.inv = inventoryManager;
Expand All @@ -279,11 +297,102 @@ public void onEnable() {
getServer().getPluginManager().disablePlugin(this);
return;
}
awaitIdentity();
}

/**
* Faction data names characters, so nothing loads, ticks, listens or saves
* until the character provider can name every legacy account's heir.
* Commands answer that factions are starting up meanwhile.
*/
private void awaitIdentity() {
for (String name : COMMANDS) {
org.bukkit.command.PluginCommand command = getCommand(name);
if (command == null) continue;
command.setExecutor((sender, cmd, label, args) -> {
sender.sendMessage("§eFactions are starting up; try again in a moment.");
return true;
});
command.setTabCompleter((sender, cmd, label, args) -> java.util.List.of());
}
if (identityReady()) {
start();
return;
}
getLogger().severe("Waiting for character identity (RPCharacters) before loading any faction data; "
+ "factions stay offline until it is ready");
getServer().getPluginManager().registerEvents(new Listener() {
@EventHandler
public void onPluginEnable(PluginEnableEvent event) {
if (("RPCharacters".equalsIgnoreCase(event.getPlugin().getName()) || "DenarEconomy".equalsIgnoreCase(event.getPlugin().getName())) && identityReady()) start();
}
}, this);
identityPoll = getServer().getScheduler().runTaskTimer(this, () -> {
if (identityReady()) start();
}, IDENTITY_POLL_TICKS, IDENTITY_POLL_TICKS);
}

static boolean identityReady() {
return net.tfminecraft.denareconomy.api.Denars.ready()
&& net.tfminecraft.tlibs.character.Characters.available()
&& net.tfminecraft.tlibs.character.Characters.legacyHeirsReady();
}

/** A persisted operation must finish before another character or money mutation is allowed. */
public void failFinancialState(Throwable error) {
started = false;
startupComplete = false;
startFailed = true;
getServer().getServicesManager().unregisterAll(this);
getLogger().log(java.util.logging.Level.SEVERE,
"Faction money recovery is incomplete; disabling until the saved operation can be recovered", error);
getServer().getPluginManager().disablePlugin(this);
}

/** Whether the data is migrated and everything has started; see {@code SimpleFactionsApi.ready()}. */
public boolean isStarted() {
return started;
}

void useMigrationHeirs(CharacterMigration.Heirs heirs) {
migrationHeirs = heirs;
}

/** Migrates the stores if needed, then loads and starts everything. */
void start() {
if (started || startFailed) return;
if (identityPoll != null) {
identityPoll.cancel();
identityPoll = null;
}
try {
CharacterMigration.Result result =
new CharacterMigration(getDataFolder().toPath(), migrationHeirs, getLogger()).run();
if (result.migrated() > 0 || result.assignmentsApplied() > 0) {
getLogger().info("Character migration: " + result.migrated() + " stores migrated, "
+ result.assignmentsApplied() + " quarantine assignments applied");
}
if (result.quarantined() > 0) {
getLogger().warning(result.quarantined() + " quarantined faction records need staff; see /f admin quarantine list");
}
} catch (java.io.IOException | RuntimeException error) {
startFailed = true;
getLogger().log(java.util.logging.Level.SEVERE,
"Cannot migrate faction data to characters; factions stay offline until this is fixed", error);
return;
}
try { startServices(); }
catch (Exception error) { failFinancialState(error); }
if (!started) return;
getServer().getPluginManager().callEvent(new net.tfminecraft.simplefactions.events.SimpleFactionsReadyEvent());
}

private void startServices() {
servicesBegun = true;
net.tfminecraft.simplefactions.identity.LeaderCharacters.useSharedIdentity();
net.tfminecraft.simplefactions.prestige.MemberPlaytime.useSharedIdentity();
net.tfminecraft.simplefactions.mercenary.company.MercenaryEligibility.useSharedIdentity();
registerListeners();
registerRpCharactersChatHooks();

vehicleRegistryPersistence = new VehicleRegistryPersistence(
new File(getDataFolder(), "Cache"),
vehicleRegistry);
Expand All @@ -302,7 +411,10 @@ public void onEnable() {
);
}
net.tfminecraft.simplefactions.inactivity.InactivityService.load();
db.loadFactions();
try {
net.tfminecraft.simplefactions.economy.TreasuryActions.recoverTopologyFiles(getDataFolder().toPath().resolve("economy/callers"));
} catch (Exception error) { failFinancialState(error); return; }
db.loadFactions();
try {
net.tfminecraft.simplefactions.espionage.EspionageService.loadAptitudes(
getDataFolder().toPath().resolve("Cache/character-aptitudes.json"));
Expand Down Expand Up @@ -351,7 +463,33 @@ public void onEnable() {
return;
}
}
factionManager.run();
factionManager.prepare();
WarManager.start();
net.tfminecraft.simplefactions.war.battle.persistence.BattlePersistenceService.loadAll();
try {
for (var guild : FactionManager.getAllGuilds()) {
net.tfminecraft.simplefactions.economy.GuildTreasury.importLegacy(guild.getIncarnationId(),
guild.getBank() == null ? 0 : guild.getBank().legacyBalance(), guild.getLedger().getCitizenTaxesCopy());
guild.updateWealth();
}
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);
realmSettlementService.recover();
net.tfminecraft.simplefactions.identity.death.CharacterDeaths.reconcile();
net.tfminecraft.simplefactions.economy.EconomyRuntime.recovered();
} catch (Exception error) {
failFinancialState(error);
return;
}
factionManager.startTicks();
registerListeners();
registerRpCharactersChatHooks();
if (Cache.provincesEnabled) {
ProvincePresenceTickService.start();
}
Expand All @@ -372,8 +510,6 @@ public void onEnable() {
getCommand(MovementCommandManager.CMD).setExecutor(movementCommandManager);
getCommand(MovementCommandManager.CMD).setTabCompleter(new MovementTabCompletion());
RequestManager.start();
WarManager.start();
net.tfminecraft.simplefactions.war.battle.persistence.BattlePersistenceService.loadAll();
net.tfminecraft.simplefactions.war.battle.persistence.BattlePersistenceService.startAutosave();
BattleScheduleTickService.start();
CampaignViewRefreshService.start();
Expand All @@ -388,11 +524,28 @@ public void onEnable() {
inventoryManager.start();
vehicleMaintenanceDecayTask.start();
startupComplete = true;
net.tfminecraft.simplefactions.identity.LeaderCharacterListener.refresh(null);
started = true;
getServer().getServicesManager().register(net.tfminecraft.denareconomy.api.TaxPolicy.class,
new net.tfminecraft.simplefactions.economy.GuildTaxPolicy(), this, org.bukkit.plugin.ServicePriority.Normal);
getServer().getScheduler().runTaskTimer(this, () -> {
try { net.tfminecraft.simplefactions.identity.death.CharacterDeaths.reconcile(); }
catch (Exception error) { net.tfminecraft.simplefactions.economy.EconomyRuntime.failClosed(error); }
}, 72000L, 72000L);
net.tfminecraft.simplefactions.identity.LeaderCharacterListener.refresh(null);
}
@Override
public void onDisable() {
if (incompatibleCharacterApi) return;
if (identityPoll != null) identityPoll.cancel();
identityPoll = null;
started = false;
getServer().getServicesManager().unregisterAll(this);
net.tfminecraft.simplefactions.identity.death.CharacterDeaths.stop();
net.tfminecraft.simplefactions.vehicles.fees.VehiclePayments.stop();
if (!servicesBegun) {
// Still waiting for identity: nothing was loaded, so nothing may be saved.
return;
}
if (trackRefreshTask != null) trackRefreshTask.cancel();
net.tfminecraft.simplefactions.integration.rpcharacters.chat.RpCharactersChatIntegration.unregister();
MercenaryStatService.clearAll();
Expand Down Expand Up @@ -686,37 +839,23 @@ public void saveVehicleFees() {
vehicleFeeStore.save();
}

/** Remembers who a vehicle was just handed to, for the transfer fee on a later release. */
public void recordVehicleOwner(String vehicleUuid, String playerName) {
vehicleFeeStore.setLastOwner(vehicleUuid, playerName);
/** Remembers the member key of the character a vehicle was just handed to, for a later transfer fee. */
public void recordVehicleOwner(String vehicleUuid, String memberKey) {
vehicleFeeStore.setLastOwner(vehicleUuid, memberKey);
vehicleFeeStore.save();
}

/**
* Records the current owner of every personal vehicle, so a vehicle released before
* anyone built or claimed it through SimpleFactions still has a last owner.
*/
public void recordVehicleOwners() {
if (!getServer().getPluginManager().isPluginEnabled("VehicleFramework")) {
return;
}
try {
for (net.tfminecraft.vehicleframework.data.OwnedVehicleSummary vehicle
: net.tfminecraft.simplefactions.vehicles.registry.VehicleOwnershipQueries.allPersonalVehicles(vehicleRegistry)) {
String owner = net.tfminecraft.simplefactions.vehicles.registry.VehicleOwnershipQueries
.playerNameFromOwner(vehicle.getOwner());
if (owner != null) {
vehicleFeeStore.setLastOwner(vehicle.getUuid(), owner);
}
}
for (net.tfminecraft.simplefactions.vehicles.registry.PlayerVehicleRecord record : vehicleRegistry.getAll()) {
vehicleFeeStore.forgetVehicle(record.getVehicleUuid());
}
vehicleFeeStore.save();
} catch (RuntimeException | LinkageError e) {
getLogger().warning("Could not record vehicle owners for transfer fees: " + e);
}
}
/** Retains captured character payers; a current account session cannot replace a registry identity. */
public void recordVehicleOwners() {
for (var record : vehicleRegistry.getAll()) {
if (record.getMode() != net.tfminecraft.simplefactions.vehicles.registry.OwnershipMode.PERSONAL) {
vehicleFeeStore.forgetVehicle(record.getVehicleUuid());
} else if (record.getPlayerUuid() != null && vehicleFeeStore.getLastOwner(record.getVehicleUuid()) == null) {
vehicleFeeStore.setLastOwner(record.getVehicleUuid(), record.getPlayerUuid().toString());
}
}
vehicleFeeStore.save();
}

public InstallationVehicleUnberthService getInstallationVehicleUnberthService() {
return installationVehicleUnberthService;
Expand Down
Loading