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
20 changes: 19 additions & 1 deletion src/main/java/net/tfminecraft/simplefactions/SimpleFactions.java
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ public void onDisable() {
net.tfminecraft.simplefactions.war.battle.persistence.BattlePersistenceService.saveAll();
sessionManager.end();
net.tfminecraft.simplefactions.inactivity.InactivityService.save();
saveLoadedFactions();
saveFactionsForShutdown();
for(War w : WarManager.get()){
db.saveWar(w);
}
Expand All @@ -392,6 +392,12 @@ public void onDisable() {
vehicleMaintenancePersistence.save();
}
}
void saveFactionsForShutdown() {
// The save resolves leader characters, so RPCharacters stays asked until it is done.
saveLoadedFactions();
net.tfminecraft.simplefactions.identity.LeaderCharacters.reset();
}

void saveLoadedFactions() {
// An early enable failure must not overwrite partially restored faction state.
if (!FactionManager.isLoaded()) return;
Expand Down Expand Up @@ -430,6 +436,8 @@ public void registerListeners() {
getServer().getPluginManager().registerEvents(bankManager, this);
getServer().getPluginManager().registerEvents(titleManager, this);
getServer().getPluginManager().registerEvents(playerManager, this);
getServer().getPluginManager().registerEvents(
new net.tfminecraft.simplefactions.identity.LeaderCharacterListener(this), this);
getServer().getPluginManager().registerEvents(sessionManager, this);
getServer().getPluginManager().registerEvents(relocationPrompt, this);
getServer().getPluginManager().registerEvents(capitalMovePrompt, this);
Expand Down Expand Up @@ -691,6 +699,16 @@ private void registerRpCharactersIntegration() {
new net.tfminecraft.simplefactions.mercenary.company.RpCharactersMercenaryTraitProbe());
net.tfminecraft.simplefactions.prestige.MemberPlaytime.setProbe(
new net.tfminecraft.simplefactions.prestige.RpCharactersPlaytimeProbe());
net.tfminecraft.simplefactions.identity.LeaderCharacters.setProbe(
new net.tfminecraft.simplefactions.identity.RpCharactersLeaderCharacterProbe());
// Learn every realm and guild leader's character once the server has
// settled, online or not, and ship it with the next map cycle.
getServer().getScheduler().runTaskLater(this, () -> {
net.tfminecraft.simplefactions.identity.LeaderCharacterListener.refresh(null);
if (FactionManager.getMap() != null) {
FactionManager.getMap().markLeaderNamesChanged();
}
}, 200L);
net.tfminecraft.simplefactions.integration.rpcharacters.chat.RpCharactersChatIntegration.register();
if (!officeCharacterDeathRegistered) {
getServer().getPluginManager().registerEvents(new net.tfminecraft.simplefactions.espionage.OfficeCharacterDeathListener(), this);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package net.tfminecraft.simplefactions.database;


import net.tfminecraft.simplefactions.identity.LeaderCharacters;
import net.tfminecraft.simplefactions.war.battle.engine.core.Battle;
import java.io.File;
import java.util.ArrayList;
Expand Down Expand Up @@ -185,6 +186,7 @@ public void loadFactions() {
f.setFoundedAt(data.foundedAt != null ? data.foundedAt : System.currentTimeMillis()/1000L);
f.setCapitalMoves(data.capitalMoves != null ? data.capitalMoves : 0);
f.setEspionage(data.espionage);
f.rememberLeaderCharacter(data.leaderCharacter, data.leaderCharacterOf);

if (data.settlements != null) {
f.getSettlementHandler().load(data.settlements);
Expand Down Expand Up @@ -255,6 +257,7 @@ public void loadFactions() {
}

Guild g = new Guild(gd, f);
g.rememberLeaderCharacter(gd.leaderCharacter, gd.leaderCharacterOf);

// --- Bank ---
if ("true".equalsIgnoreCase(gd.bank)) {
Expand Down Expand Up @@ -324,6 +327,9 @@ public boolean saveFactionChecked(Faction f) {
data.name = f.getName();
data.rgb = f.getRGB();
data.leader = f.getLeader();
rememberLeaderCharacters(f);
data.leaderCharacter = f.getLeaderCharacter();
data.leaderCharacterOf = f.getLeaderCharacterOf();
data.rulerTitle = f.getRulerTitle();
data.government = f.getGovernmentString();
data.culture = f.getCulture();
Expand Down Expand Up @@ -402,6 +408,8 @@ public boolean saveFactionChecked(Faction f) {
gd.id = g.getId();
gd.name = g.getOwnName();
gd.leader = g.getLeader();
gd.leaderCharacter = g.getLeaderCharacter();
gd.leaderCharacterOf = g.getLeaderCharacterOf();
gd.rgb = g.getRGB();
gd.type = g.getType().getId();
gd.capital = g.getCapital();
Expand Down Expand Up @@ -534,6 +542,22 @@ public boolean saveFactionChecked(Faction f) {
* HELPERS
* ===================================================== */

/**
* Brings the realm's and its guilds' leader characters up to date before
* they are written. A name learned here must still reach the map: the join
* refresh that runs later would find nothing new and leave nation.json
* stale, so the map is told now.
*/
static void rememberLeaderCharacters(Faction f) {
boolean changed = LeaderCharacters.refresh(f);
for (Guild g : f.getGuildHandler().getGuilds()) {
if (g != null) changed |= LeaderCharacters.refresh(g);
}
if (changed && FactionManager.getMap() != null) {
FactionManager.getMap().markLeaderNamesChanged();
}
}

public static List<Modifier> loadModifiers(List<String> raw) {
List<Modifier> list = new ArrayList<>();
for (String s : raw) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ public class FactionData {
public String rgb;
public String leader;

/**
* The leader's roleplay character name, for the web map, and the player
* it was read from. See {@code LeaderCharacters}.
*/
@SerializedName("leader character")
public String leaderCharacter;

@SerializedName("leader character of")
public String leaderCharacterOf;

@SerializedName("ruler title")
public String rulerTitle;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ public class GuildData {
public String id;
public String name;
public String leader;

/** The leader's roleplay character name and the player it was read from. */
@com.google.gson.annotations.SerializedName("leader character")
public String leaderCharacter;

@com.google.gson.annotations.SerializedName("leader character of")
public String leaderCharacterOf;
public String rgb;
public String type;
public Integer capital;
Expand Down
23 changes: 23 additions & 0 deletions src/main/java/net/tfminecraft/simplefactions/guild/Guild.java
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,28 @@ public void kick(String member) {
if(stored != null) members.remove(stored);
}
public String getLeader() { return isBase() ? host.getLeader() : leader; }

/** Last known roleplay name of the leader; a realm's own guild shares the realm's. */
private String leaderCharacter;
private String leaderCharacterOf;

public String getLeaderCharacter() {
return isBase() ? host.getLeaderCharacter() : leaderCharacter;
}

public String getLeaderCharacterOf() {
return isBase() ? host.getLeaderCharacterOf() : leaderCharacterOf;
}

public void rememberLeaderCharacter(String name, String player) {
this.leaderCharacter = name;
this.leaderCharacterOf = player;
}

/** A realm's own guild becoming an ordinary one keeps the realm's remembered name. */
void keepHostLeaderCharacter() {
rememberLeaderCharacter(getLeaderCharacter(), getLeaderCharacterOf());
}
public void setLeader(String leader) {
this.leader = leader;
}
Expand Down Expand Up @@ -938,6 +960,7 @@ public void convert(GuildType type) {
this.capital = getCapital();
this.id = getId();
this.leader = getLeader();
keepHostLeaderCharacter();
this.members = new ArrayList<>(getMembers());
this.invites = new ArrayList<>(getInvites());
if (this.name == null || this.name.isBlank()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package net.tfminecraft.simplefactions.identity;

import org.bukkit.Bukkit;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.plugin.Plugin;

import net.tfminecraft.simplefactions.guild.Guild;
import net.tfminecraft.simplefactions.managers.FactionManager;
import net.tfminecraft.simplefactions.objects.Faction;

/**
* Notes a realm or guild leader's character shortly after they join, so the map learns
* it even when the leader is never online at the moment the map exports. The
* delay gives RPCharacters time to load the player's characters. The name is
* kept on the faction and written out with its next save.
*/
public final class LeaderCharacterListener implements Listener {
/** Five seconds: RPCharacters loads player data asynchronously on join. */
private static final long DELAY_TICKS = 100L;

private final Plugin plugin;

public LeaderCharacterListener(Plugin plugin) {
this.plugin = plugin;
}

@EventHandler(priority = EventPriority.MONITOR)
public void onJoin(PlayerJoinEvent event) {
String player = event.getPlayer().getName();
Bukkit.getScheduler().runTaskLater(plugin, () -> rememberFor(player), DELAY_TICKS);
}

static void rememberFor(String player) {
refresh(player);
}

/**
* Re-reads every realm and guild leader's character (or only `player`'s
* when given) and, if any name changed, asks the map to ship nation.json
* on its next cycle.
*/
public static void refresh(String player) {
boolean changed = false;
for (Faction faction : FactionManager.factions) {
if (faction == null) continue;
if (player == null || player.equalsIgnoreCase(faction.getLeader())) {
changed |= LeaderCharacters.refresh(faction);
}
// Guild leaders too; a realm's own guild follows the realm above.
for (Guild guild : faction.getGuildHandler().getGuilds()) {
if (guild == null || guild.isBase()) continue;
if (player != null && !player.equalsIgnoreCase(guild.getLeader())) continue;
changed |= LeaderCharacters.refresh(guild);
}
}
if (changed && FactionManager.getMap() != null) {
FactionManager.getMap().markLeaderNamesChanged();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package net.tfminecraft.simplefactions.identity;

import java.util.Objects;

import net.tfminecraft.simplefactions.guild.Guild;
import net.tfminecraft.simplefactions.objects.Faction;

/**
* The roleplay name of a realm's leader, for the web map.
*
* A faction stores its leader as a Minecraft username, which is not what the
* map should show: players are known by their character there. RPCharacters
* only knows a player's active character while they are online, so the name
* is read whenever the leader is seen online and remembered with the player it
* belongs to. A remembered name is dropped as soon as the leader changes, so a
* new leader never inherits the old one's character.
*
* Production swaps in {@link RpCharactersLeaderCharacterProbe} when that
* plugin is present; without it nothing is ever known, remembered names are
* dropped too, and the map shows no ruler name.
*/
public final class LeaderCharacters {

/** Active character name of an online player, or null if unknown. */
public interface Probe {
String activeCharacterName(String player);

/** False while RPCharacters is missing, so remembered names are not kept either. */
default boolean available() {
return true;
}
}

/** A remembered character name and the player it belongs to. */
public record Remembered(String name, String player) {
public static final Remembered NONE = new Remembered(null, null);
}

private static final Probe UNAVAILABLE = new Probe() {
@Override
public String activeCharacterName(String player) {
return null;
}

@Override
public boolean available() {
return false;
}
};

private static volatile Probe probe = UNAVAILABLE;

private LeaderCharacters() {}

public static void setProbe(Probe next) {
probe = next == null ? UNAVAILABLE : next;
}

public static void reset() {
probe = UNAVAILABLE;
}

/**
* What to remember for `leader` now: their active character if they are
* online with one, else what was remembered for this same leader, else
* nothing. Without RPCharacters it is always nothing.
*/
public static Remembered resolve(String leader, String rememberedName, String rememberedFor) {
if (leader == null || leader.isBlank()) return Remembered.NONE;
Probe current = probe;
if (!current.available()) return Remembered.NONE;
String active = clean(current.activeCharacterName(leader));
if (active != null) return new Remembered(active, leader);
if (rememberedName != null && leader.equalsIgnoreCase(rememberedFor)) {
String kept = clean(rememberedName);
if (kept != null) return new Remembered(kept, leader);
}
return Remembered.NONE;
}

/** Re-reads the realm leader's character onto `faction`; true if the name changed. */
public static boolean refresh(Faction faction) {
Remembered remembered = resolve(
faction.getLeader(), faction.getLeaderCharacter(), faction.getLeaderCharacterOf());
boolean changed = !Objects.equals(remembered.name(), faction.getLeaderCharacter());
faction.rememberLeaderCharacter(remembered.name(), remembered.player());
return changed;
}

/**
* Re-reads the guild leader's character onto `guild`; true if the name
* changed. A realm's own guild shares the realm's, so it is left alone.
*/
public static boolean refresh(Guild guild) {
if (guild.isBase()) return false;
Remembered remembered = resolve(
guild.getLeader(), guild.getLeaderCharacter(), guild.getLeaderCharacterOf());
boolean changed = !Objects.equals(remembered.name(), guild.getLeaderCharacter());
guild.rememberLeaderCharacter(remembered.name(), remembered.player());
return changed;
}

/** Strips Minecraft colour codes and blank names. */
static String clean(String name) {
if (name == null) return null;
String stripped = name.replaceAll("(?i)§[0-9A-FK-ORX]", "").replace("§", "").trim();
return stripped.isEmpty() ? null : stripped;
}
}
Loading
Loading