diff --git a/README.md b/README.md
index 2c3bac2..1694334 100644
--- a/README.md
+++ b/README.md
@@ -4,8 +4,16 @@
Codex gives players a journal of discoveries: places they enter, creatures they
kill and lore that commands unlock. `/codex` opens the journal, and each entry
-shows its text once it has been found. This copy tracks upstream 2.12.1 and builds
-for Paper 1.21.10. It has no update checker or bStats reporting.
+shows its text once it has been found.
+
+Each roleplay character keeps its own journal, so a new character discovers the
+world afresh and earns the discovery rewards again. An entry any of the account's
+characters has found stays readable for out-of-character reference, and its hover
+text names the living characters who can use it in character. What an account
+found before journals were per character stays readable this way, but no
+character knows it in character until it finds it again. This copy tracks upstream
+2.12.1, builds for Paper 1.21.10 and needs TLibs 2.6.1 with RPCharacters. It stores
+journals in files only, and has no update checker or bStats reporting.
## Documentation
@@ -16,7 +24,9 @@ discoveries, actions and placeholders.
## Tests
-With Java 21, run `mvn clean verify`.
+With Java 21 and the pinned plugin dependencies installed, run `mvn clean verify`.
+JUnit and Mockito cover the per-character journals, which must keep 100% line
+coverage. Upstream Codex has no tests.
## License
diff --git a/pom.xml b/pom.xml
index 6477cc8..b0fe501 100644
--- a/pom.xml
+++ b/pom.xml
@@ -15,6 +15,8 @@
21
UTF-8
+
+ 2.6.1
@@ -36,12 +38,6 @@
2.11.6
provided
-
- com.zaxxer
- HikariCP
- 4.0.3
- compile
-
com.sk89q.worldguard
worldguard-bukkit
@@ -72,6 +68,31 @@
6.0.0.1
provided
+
+ me.plugins
+ tlibs
+ ${tlibs.version}
+ provided
+
+
+
+ *
+ *
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.14.0
+ test
+
+
+ org.mockito
+ mockito-core
+ 5.20.0
+ test
+
@@ -139,25 +160,65 @@
org.apache.maven.plugins
- maven-shade-plugin
- 3.6.2
+ maven-dependency-plugin
+ 3.7.0
+
+
+
+ properties
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.5.4
+
+ @{argLine} -javaagent:${org.mockito:mockito-core:jar} -Xshare:off
+ true
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ 0.8.15
- package
- shade
+ prepare-agent
+
+
+
+ coverage-report
+ verify
+
+ report
+
+
+
+ coverage-check
+ verify
+
+ check
- false
-
-
- com.zaxxer.hikari
- net.tfminecraft.codex.libs.hikaricp
+
+
+
+ PACKAGE
- com.zaxxer.hikari.**
+ net.tfminecraft.codex.character
-
-
+
+
+ LINE
+ MISSEDCOUNT
+ 0
+
+
+
+
diff --git a/src/main/java/net/tfminecraft/codex/Codex.java b/src/main/java/net/tfminecraft/codex/Codex.java
index d516d12..07df9e2 100644
--- a/src/main/java/net/tfminecraft/codex/Codex.java
+++ b/src/main/java/net/tfminecraft/codex/Codex.java
@@ -1,10 +1,11 @@
package net.tfminecraft.codex;
import net.tfminecraft.codex.api.CodexAPI;
+import net.tfminecraft.codex.character.CharacterJournals;
+import net.tfminecraft.codex.character.CharacterStore;
import net.tfminecraft.codex.api.ExpansionCodex;
import net.tfminecraft.codex.commands.MainCommand;
import net.tfminecraft.codex.config.ConfigsManager;
-import net.tfminecraft.codex.database.MySQLConnection;
import net.tfminecraft.codex.listeners.InventoryListener;
import net.tfminecraft.codex.listeners.PlayerListener;
import net.tfminecraft.codex.listeners.dependencies.EliteMobsListener;
@@ -37,8 +38,8 @@ public class Codex extends JavaPlugin {
private DiscoveryManager discoveryManager;
private DependencyManager dependencyManager;
private PlayerDataManager playerDataManager;
+ private CharacterJournals characterJournals;
- private MySQLConnection mySQLConnection;
private PlayerDataSaveTask playerDataSaveTask;
private VerifyManager verifyManager;
@@ -61,8 +62,10 @@ public void onEnable(){
return;
}
+ characterJournals = new CharacterJournals(playerDataManager, new CharacterStore(getDataFolder().toPath()), getLogger());
registerEvents();
registerCommands();
+ characterJournals.start();
reloadPlayerDataSaveTask();
@@ -74,16 +77,13 @@ public void onEnable(){
this.verifyManager = new VerifyManager(this);
this.verifyManager.verify();
- if(configsManager.getMainConfigManager().isMySQL()){
- mySQLConnection = new MySQLConnection(this);
- mySQLConnection.setupMySql();
- }
-
Bukkit.getConsoleSender().sendMessage(prefix+MessagesManager.getLegacyColoredMessage("&eHas been enabled! &fVersion: "+version));
}
public void onDisable(){
- configsManager.getPlayersConfigManager().saveConfigs();
+ if(characterJournals != null){
+ characterJournals.stop();
+ }
Bukkit.getConsoleSender().sendMessage(prefix+MessagesManager.getLegacyColoredMessage("&eHas been disabled! &fVersion: "+version));
}
@@ -95,6 +95,7 @@ public void registerEvents(){
PluginManager pm = getServer().getPluginManager();
pm.registerEvents(new InventoryListener(this), this);
pm.registerEvents(new PlayerListener(this), this);
+ pm.registerEvents(characterJournals, this);
if(dependencyManager.getWorldGuardManager() != null){
pm.registerEvents(new WorldGuardListener(this), this);
}
@@ -210,12 +211,12 @@ public DependencyManager getDependencyManager() {
return dependencyManager;
}
- public PlayerDataManager getPlayerDataManager() {
- return playerDataManager;
+ public CharacterJournals getCharacterJournals() {
+ return characterJournals;
}
- public MySQLConnection getMySQLConnection() {
- return mySQLConnection;
+ public PlayerDataManager getPlayerDataManager() {
+ return playerDataManager;
}
public CategoryManager getCategoryManager() {
diff --git a/src/main/java/net/tfminecraft/codex/api/CodexAPI.java b/src/main/java/net/tfminecraft/codex/api/CodexAPI.java
index d96a9c7..cf6ab91 100644
--- a/src/main/java/net/tfminecraft/codex/api/CodexAPI.java
+++ b/src/main/java/net/tfminecraft/codex/api/CodexAPI.java
@@ -24,7 +24,7 @@ public static boolean hasCompletedCategory(String playerName, String categoryNam
}
public static PlayerData getPlayerData(Player player){
- return plugin.getPlayerDataManager().getPlayer(player,false);
+ return plugin.getPlayerDataManager().getPlayer(player);
}
public static PlayerData getPlayerData(String playerName){
diff --git a/src/main/java/net/tfminecraft/codex/character/AccountKnowledge.java b/src/main/java/net/tfminecraft/codex/character/AccountKnowledge.java
new file mode 100644
index 0000000..fb249d4
--- /dev/null
+++ b/src/main/java/net/tfminecraft/codex/character/AccountKnowledge.java
@@ -0,0 +1,85 @@
+package net.tfminecraft.codex.character;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import net.tfminecraft.codex.model.data.PlayerData;
+import net.tfminecraft.codex.model.data.PlayerDataCategory;
+import net.tfminecraft.codex.model.data.PlayerDataDiscovery;
+
+/**
+ * What the account knows beyond the played character, read when a character's session starts: its
+ * other characters' journals and its journal from before characters had their own. Every such entry
+ * stays readable for out-of-character reference; only living characters who found it themselves can
+ * use it in character. The played character's own journal is live, so it is checked separately.
+ */
+public final class AccountKnowledge {
+ /** A discovery known elsewhere on the account: a date it was found, and which living characters know it. */
+ public record Known(String date, List livingNames) {
+ }
+
+ private final String characterName;
+ private final Map others;
+
+ AccountKnowledge(String characterName, Map others) {
+ this.characterName = characterName;
+ this.others = others;
+ }
+
+ public static AccountKnowledge empty(String characterName) {
+ return new AccountKnowledge(characterName, Map.of());
+ }
+
+ public String characterName() {
+ return characterName;
+ }
+
+ /** The other characters' discoveries, keyed {@code category:discovery}. */
+ public Known other(String category, String discovery) {
+ return others.get(key(category, discovery));
+ }
+
+ /** Living characters who know the entry in character, the played character first when it does. */
+ public List knownInCharacterBy(PlayerData current, String category, String discovery) {
+ List names = new ArrayList<>();
+ if (current != null && current.hasDiscovery(category, discovery)) {
+ names.add(characterName);
+ }
+ Known known = other(category, discovery);
+ if (known != null) {
+ names.addAll(known.livingNames());
+ }
+ return names;
+ }
+
+ static String key(String category, String discovery) {
+ return category + ":" + discovery;
+ }
+
+ /** Collects journals. Each is added once; dead characters and the old account journal count but are not named. */
+ static final class Builder {
+ private final Map dates = new HashMap<>();
+ private final Map> names = new HashMap<>();
+
+ void add(List categories, String name, boolean alive) {
+ for (PlayerDataCategory category : categories) {
+ for (PlayerDataDiscovery discovery : category.getDiscoveries()) {
+ String key = key(category.getName(), discovery.getDiscoveryName());
+ dates.putIfAbsent(key, discovery.getDiscoverDate());
+ List known = names.computeIfAbsent(key, ignored -> new ArrayList<>());
+ if (alive) {
+ known.add(name);
+ }
+ }
+ }
+ }
+
+ AccountKnowledge build(String characterName) {
+ Map others = new HashMap<>();
+ names.forEach((key, known) -> others.put(key, new Known(dates.get(key), List.copyOf(known))));
+ return new AccountKnowledge(characterName, Map.copyOf(others));
+ }
+ }
+}
diff --git a/src/main/java/net/tfminecraft/codex/character/CharacterJournals.java b/src/main/java/net/tfminecraft/codex/character/CharacterJournals.java
new file mode 100644
index 0000000..72cf26c
--- /dev/null
+++ b/src/main/java/net/tfminecraft/codex/character/CharacterJournals.java
@@ -0,0 +1,178 @@
+package net.tfminecraft.codex.character;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.PlayerQuitEvent;
+
+import net.tfminecraft.codex.managers.PlayerDataManager;
+import net.tfminecraft.codex.model.data.PlayerData;
+import net.tfminecraft.tlibs.character.CharacterInfo;
+import net.tfminecraft.tlibs.character.CharacterRef;
+import net.tfminecraft.tlibs.character.CharacterSessionEndEvent;
+import net.tfminecraft.tlibs.character.CharacterSessionStartEvent;
+import net.tfminecraft.tlibs.character.Characters;
+
+/**
+ * Gives each character its own journal: loaded when a character session starts and saved when it
+ * ends. Between characters a player has no journal, so nothing can be discovered. Alongside it sits
+ * what the account knows from its other characters and from before journals were per character, so
+ * entries stay readable out of character. Main thread only.
+ */
+public final class CharacterJournals implements Listener {
+ private final PlayerDataManager players;
+ private final CharacterStore store;
+ private final Logger logger;
+ private final Map knowledge = new HashMap<>();
+ /** Journals of characters no longer in play whose last save failed, by character; retried with every save. */
+ private final Map unsaved = new HashMap<>();
+
+ public CharacterJournals(PlayerDataManager players, CharacterStore store, Logger logger) {
+ this.players = players;
+ this.store = store;
+ this.logger = logger;
+ }
+
+ /** Opens journals for characters already in play, for example when the plugin enables late. */
+ public void start() {
+ for (Player player : Bukkit.getOnlinePlayers()) {
+ Characters.currentSession(player).ifPresent(session -> open(player, session.ref()));
+ }
+ }
+
+ public void stop() {
+ saveAll();
+ }
+
+ @EventHandler(priority = EventPriority.LOWEST)
+ public void onStart(CharacterSessionStartEvent event) {
+ open(event.getPlayer(), event.getSession().ref());
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR)
+ public void onEnd(CharacterSessionEndEvent event) {
+ close(event.getPlayer(), event.getSession().ref().characterId());
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR)
+ public void onQuit(PlayerQuitEvent event) {
+ close(event.getPlayer(), null);
+ }
+
+ void open(Player player, CharacterRef ref) {
+ close(player, null);
+ try {
+ PlayerData pending = unsaved.remove(ref.characterId());
+ players.addPlayer(pending != null ? pending : store.load(ref.ownerId(), ref.characterId(), player.getName()));
+ knowledge.put(ref.ownerId(), knowledgeOf(ref, player.getName()));
+ } catch (IOException ex) {
+ logger.log(Level.SEVERE, "Codex for character " + ref.characterId() + " of " + player.getName()
+ + " could not be read; nothing can be discovered until it is fixed", ex);
+ }
+ }
+
+ /** Saves and unloads the player's journal; with a character ID, only that character's. */
+ void close(Player player, UUID characterId) {
+ PlayerData data = players.getPlayerByUUID(player.getUniqueId());
+ if (data == null || characterId != null && !characterId.equals(data.getCharacterId())) {
+ return;
+ }
+ if (!save(data)) {
+ unsaved.put(data.getCharacterId(), data);
+ }
+ players.removePlayer(data);
+ knowledge.remove(player.getUniqueId());
+ }
+
+ /** Journals of characters out of play that still wait to be saved; staff resets apply to them too. */
+ public List unsavedJournals() {
+ return List.copyOf(unsaved.values());
+ }
+
+ /** What the player's account knows beyond the played character, or null between characters. */
+ public AccountKnowledge knowledge(Player player) {
+ return knowledge.get(player.getUniqueId());
+ }
+
+ /** Rereads the other journals, for example after staff reset them. */
+ public void refreshKnowledge() {
+ for (PlayerData data : players.getPlayers().values()) {
+ knowledge.put(data.getUuid(), knowledgeOf(new CharacterRef(data.getUuid(), data.getCharacterId()), data.getName()));
+ }
+ }
+
+ public void saveAll() {
+ for (PlayerData data : new ArrayList<>(players.getPlayers().values())) {
+ save(data);
+ }
+ unsaved.values().removeIf(this::save);
+ }
+
+ /** Whether the journal is on disk; a failed save leaves it modified. */
+ private boolean save(PlayerData data) {
+ if (!data.isModified()) {
+ return true;
+ }
+ try {
+ store.save(data);
+ return true;
+ } catch (IOException ex) {
+ logger.log(Level.SEVERE, "Could not save Codex for character " + data.getCharacterId()
+ + "; keeping it in memory and retrying with the next save", ex);
+ return false;
+ }
+ }
+
+ private AccountKnowledge knowledgeOf(CharacterRef ref, String fallbackName) {
+ try {
+ return readKnowledge(ref, fallbackName);
+ } catch (RuntimeException ex) {
+ logger.log(Level.WARNING, "Could not read the other characters of account " + ref.ownerId()
+ + "; only this character's own Codex is shown", ex);
+ return AccountKnowledge.empty(fallbackName);
+ }
+ }
+
+ private AccountKnowledge readKnowledge(CharacterRef ref, String fallbackName) {
+ String name = fallbackName;
+ AccountKnowledge.Builder builder = new AccountKnowledge.Builder();
+ // What the account found before journals were per character: readable, but no character knows it.
+ if (store.hasLegacy(ref.ownerId())) {
+ try {
+ builder.add(store.readLegacy(ref.ownerId()).categories(), null, false);
+ } catch (IOException ex) {
+ logger.log(Level.WARNING, "The account Codex of " + ref.ownerId() + " could not be read; "
+ + "its old discoveries are left out of its reference", ex);
+ }
+ }
+ List roster = Characters.characters(ref.ownerId()).orElse(List.of());
+ for (CharacterInfo info : roster) {
+ UUID id = info.ref().characterId();
+ if (id.equals(ref.characterId())) {
+ name = info.name();
+ } else if (unsaved.containsKey(id)) {
+ builder.add(unsaved.get(id).getCategories(), info.name(), info.alive());
+ } else if (Files.exists(store.characterFile(id))) {
+ try {
+ builder.add(store.load(ref.ownerId(), id, info.name()).getCategories(), info.name(), info.alive());
+ } catch (IOException ex) {
+ logger.log(Level.WARNING, "Codex for character " + id + " could not be read; "
+ + "its discoveries are left out of its account's reference", ex);
+ }
+ }
+ }
+ return builder.build(name);
+ }
+}
diff --git a/src/main/java/net/tfminecraft/codex/character/CharacterStore.java b/src/main/java/net/tfminecraft/codex/character/CharacterStore.java
new file mode 100644
index 0000000..44f9443
--- /dev/null
+++ b/src/main/java/net/tfminecraft/codex/character/CharacterStore.java
@@ -0,0 +1,127 @@
+package net.tfminecraft.codex.character;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.configuration.InvalidConfigurationException;
+import org.bukkit.configuration.file.YamlConfiguration;
+
+import net.tfminecraft.codex.model.data.PlayerData;
+import net.tfminecraft.codex.model.data.PlayerDataCategory;
+import net.tfminecraft.codex.model.data.PlayerDataDiscovery;
+
+/**
+ * Discovery files. Each character has {@code characters/.yml}. {@code players/.yml}
+ * holds what an account found before characters had their own; it stays as the account's
+ * out-of-character reference and no character inherits it. Main thread only.
+ */
+public final class CharacterStore {
+ public static final String CHARACTERS = "characters";
+ public static final String LEGACY = "players";
+
+ private final Path characters;
+ private final Path legacy;
+
+ public CharacterStore(Path dataFolder) {
+ this.characters = dataFolder.resolve(CHARACTERS);
+ this.legacy = dataFolder.resolve(LEGACY);
+ }
+
+ /** What an account found before characters had their own journals, with the account name it was saved under. */
+ public record LegacyJournal(String name, List categories) {
+ }
+
+ public Path characterFile(UUID characterId) {
+ return characters.resolve(characterId + ".yml");
+ }
+
+ /** The character's journal. A new journal is marked modified, so every played character gets a file. */
+ public PlayerData load(UUID owner, UUID characterId, String name) throws IOException {
+ PlayerData data = new PlayerData(owner, characterId, name);
+ Path file = characterFile(characterId);
+ if (Files.exists(file)) {
+ data.setCategories(categories(read(file)));
+ } else {
+ data.setModified(true);
+ }
+ return data;
+ }
+
+ /** Writes the journal through a temporary file, so a failed write leaves the old file whole. */
+ public void save(PlayerData data) throws IOException {
+ YamlConfiguration yaml = new YamlConfiguration();
+ yaml.set("owner", data.getUuid().toString());
+ yaml.set("name", data.getName());
+ for (PlayerDataCategory category : data.getCategories()) {
+ String path = "categories." + category.getName();
+ yaml.set(path + ".completed", category.isCompleted());
+ yaml.set(path + ".discoveries", category.getDiscoveriesToStringList());
+ }
+ Files.createDirectories(characters);
+ Path target = characterFile(data.getCharacterId());
+ Path temporary = characters.resolve(data.getCharacterId() + ".yml.tmp");
+ Files.writeString(temporary, yaml.saveToString(), StandardCharsets.UTF_8);
+ Files.move(temporary, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
+ data.setModified(false);
+ }
+
+ public boolean hasLegacy(UUID owner) {
+ return Files.isRegularFile(legacyFile(owner));
+ }
+
+ public LegacyJournal readLegacy(UUID owner) throws IOException {
+ YamlConfiguration yaml = read(legacyFile(owner));
+ return new LegacyJournal(yaml.getString("name"), categories(yaml));
+ }
+
+ private Path legacyFile(UUID owner) {
+ return legacy.resolve(owner + ".yml");
+ }
+
+ private static YamlConfiguration read(Path file) throws IOException {
+ YamlConfiguration yaml = new YamlConfiguration();
+ try {
+ yaml.loadFromString(Files.readString(file, StandardCharsets.UTF_8));
+ } catch (InvalidConfigurationException ex) {
+ throw new IOException("Unreadable discovery file " + file, ex);
+ }
+ return yaml;
+ }
+
+ /** Discoveries are saved as {@code id;date;millis}; older files may lack the millis. */
+ private static List categories(YamlConfiguration yaml) {
+ List categories = new ArrayList<>();
+ ConfigurationSection section = yaml.getConfigurationSection("categories");
+ if (section == null) {
+ return categories;
+ }
+ for (String name : section.getKeys(false)) {
+ ArrayList discoveries = new ArrayList<>();
+ for (String entry : section.getStringList(name + ".discoveries")) {
+ String[] parts = entry.split(";", -1);
+ if (parts[0].isBlank()) {
+ continue;
+ }
+ String date = parts.length > 1 ? parts[1] : "";
+ discoveries.add(new PlayerDataDiscovery(parts[0], date, parts.length > 2 ? millis(parts[2]) : 0));
+ }
+ categories.add(new PlayerDataCategory(name, section.getBoolean(name + ".completed"), discoveries));
+ }
+ return categories;
+ }
+
+ private static long millis(String text) {
+ try {
+ return Long.parseLong(text.trim());
+ } catch (NumberFormatException ex) {
+ return 0;
+ }
+ }
+}
diff --git a/src/main/java/net/tfminecraft/codex/config/ConfigsManager.java b/src/main/java/net/tfminecraft/codex/config/ConfigsManager.java
index 175de2f..f2b90e5 100644
--- a/src/main/java/net/tfminecraft/codex/config/ConfigsManager.java
+++ b/src/main/java/net/tfminecraft/codex/config/ConfigsManager.java
@@ -1,5 +1,6 @@
package net.tfminecraft.codex.config;
+import net.tfminecraft.codex.character.CharacterStore;
import net.tfminecraft.codex.Codex;
@@ -16,7 +17,7 @@ public ConfigsManager(Codex plugin) {
this.plugin = plugin;
this.mainConfigManager = new MainConfigManager(plugin);
this.categoriesConfigManager = new CategoriesConfigManager(plugin,"categories");
- this.playersConfigManager = new PlayersConfigManager(plugin,"players");
+ this.playersConfigManager = new PlayersConfigManager(plugin,CharacterStore.CHARACTERS);
this.messagesConfigManager = new MessagesConfigManager(plugin);
this.inventoryConfigManager = new InventoryConfigManager(plugin);
}
diff --git a/src/main/java/net/tfminecraft/codex/config/DataFolderConfigManager.java b/src/main/java/net/tfminecraft/codex/config/DataFolderConfigManager.java
index 7cc4f4e..ab14d4a 100644
--- a/src/main/java/net/tfminecraft/codex/config/DataFolderConfigManager.java
+++ b/src/main/java/net/tfminecraft/codex/config/DataFolderConfigManager.java
@@ -48,15 +48,21 @@ public CommonConfig getConfigFile(String pathName,boolean create) {
}
public ArrayList getConfigs(){
+ return getConfigs(folderName);
+ }
+
+ /** Every file in a folder of the data folder; none if the folder is missing. */
+ public ArrayList getConfigs(String folder){
ArrayList configs = new ArrayList<>();
- String pathFile = plugin.getDataFolder() + File.separator + folderName;
- File folder = new File(pathFile);
- File[] listOfFiles = folder.listFiles();
+ File[] listOfFiles = new File(plugin.getDataFolder() + File.separator + folder).listFiles();
+ if(listOfFiles == null){
+ return configs;
+ }
for (File file : listOfFiles) {
if (file.isFile()) {
String pathName = file.getName();
- CommonConfig commonConfig = new CommonConfig(pathName, plugin, folderName, true);
+ CommonConfig commonConfig = new CommonConfig(pathName, plugin, folder, true);
commonConfig.registerConfig();
configs.add(commonConfig);
}
diff --git a/src/main/java/net/tfminecraft/codex/config/MainConfigManager.java b/src/main/java/net/tfminecraft/codex/config/MainConfigManager.java
index b8b715f..b3d3c0f 100644
--- a/src/main/java/net/tfminecraft/codex/config/MainConfigManager.java
+++ b/src/main/java/net/tfminecraft/codex/config/MainConfigManager.java
@@ -13,7 +13,6 @@ public class MainConfigManager {
private Codex plugin;
private CommonConfig configFile;
- private boolean isMySQL;
private String discoveriesDateFormat;
private int playerDataSave;
private String progressBarPlaceholderFillSymbol;
@@ -31,7 +30,6 @@ public MainConfigManager(Codex plugin){
public void configure() {
FileConfiguration config = configFile.getConfig();
- isMySQL = config.getBoolean("mysql_database.enabled");
discoveriesDateFormat = config.getString("discoveries_date_format");
playerDataSave = config.getInt("player_data_save");
progressBarPlaceholderFillSymbol = config.getString("progress_bar_placeholder.filled_symbol");
@@ -57,13 +55,6 @@ public void checkUpdate(){
getConfig().set("use_minimessage",false);
configFile.saveConfig();
}
- if(!text.contains("verifyServerCertificate:")){
- getConfig().set("mysql_database.pool.connectionTimeout",5000);
- getConfig().set("mysql_database.advanced.verifyServerCertificate",false);
- getConfig().set("mysql_database.advanced.useSSL",true);
- getConfig().set("mysql_database.advanced.allowPublicKeyRetrieval",true);
- configFile.saveConfig();
- }
}catch(IOException e){
e.printStackTrace();
}
@@ -77,10 +68,6 @@ public FileConfiguration getConfig(){
return configFile.getConfig();
}
- public boolean isMySQL() {
- return isMySQL;
- }
-
public String getDiscoveriesDateFormat() {
return discoveriesDateFormat;
}
diff --git a/src/main/java/net/tfminecraft/codex/config/MessagesConfigManager.java b/src/main/java/net/tfminecraft/codex/config/MessagesConfigManager.java
index d63cc77..ff9a16c 100644
--- a/src/main/java/net/tfminecraft/codex/config/MessagesConfigManager.java
+++ b/src/main/java/net/tfminecraft/codex/config/MessagesConfigManager.java
@@ -10,6 +10,11 @@
import java.nio.file.Paths;
public class MessagesConfigManager {
+ public static final String KNOWN_BY = "&7%characters% can use this in character.";
+ public static final String KNOWN_BY_NOBODY = "&7No character can use this in character.";
+ public static final String OUT_OF_CHARACTER_ONLY = "&8&o%character% knows this only out of character.";
+ public static final String NO_CHARACTER_IN_PLAY = "&cChoose a character to open the Codex.";
+
private Codex plugin;
private CommonConfig configFile;
@@ -52,6 +57,16 @@ public void checkUpdates(){
String text = new String(Files.readAllBytes(pathConfig));
FileConfiguration config = configFile.getConfig();
+ if(!text.contains("knownInCharacterBy:")){
+ config.set("knownInCharacterBy",KNOWN_BY);
+ config.set("knownInCharacterByNobody",KNOWN_BY_NOBODY);
+ config.set("knownOutOfCharacterOnly",OUT_OF_CHARACTER_ONLY);
+ configFile.saveConfig();
+ }
+ if(!text.contains("noCharacterInPlay:")){
+ config.set("noCharacterInPlay",NO_CHARACTER_IN_PLAY);
+ configFile.saveConfig();
+ }
if(!text.contains("clickActionsCooldown:")){
config.set("clickActionsCooldown","&cYou must wait &7%time% &cbefore using this item again.");
config.set("seconds","s");
diff --git a/src/main/java/net/tfminecraft/codex/config/PlayersConfigManager.java b/src/main/java/net/tfminecraft/codex/config/PlayersConfigManager.java
index b551d9f..6c88736 100644
--- a/src/main/java/net/tfminecraft/codex/config/PlayersConfigManager.java
+++ b/src/main/java/net/tfminecraft/codex/config/PlayersConfigManager.java
@@ -1,19 +1,15 @@
package net.tfminecraft.codex.config;
import net.tfminecraft.codex.Codex;
-import net.tfminecraft.codex.model.data.PlayerData;
-import net.tfminecraft.codex.model.data.PlayerDataCategory;
-import net.tfminecraft.codex.model.data.PlayerDataDiscovery;
-import net.tfminecraft.codex.model.internal.GenericCallback;
+import net.tfminecraft.codex.character.CharacterStore;
import org.bukkit.configuration.file.FileConfiguration;
-import org.bukkit.scheduler.BukkitRunnable;
import java.io.File;
import java.util.*;
+/** The per-character discovery files in {@code characters/}; see {@link net.tfminecraft.codex.character.CharacterStore}. */
public class PlayersConfigManager extends DataFolderConfigManager {
-
public PlayersConfigManager(Codex plugin, String folderName) {
super(plugin, folderName);
}
@@ -23,86 +19,15 @@ public void loadConfigs(){
// No use for player config
}
- public void loadConfig(UUID uuid, GenericCallback callback){
- new BukkitRunnable(){
- @Override
- public void run() {
- PlayerData playerData = null;
- CommonConfig playerConfig = getConfigFile(uuid+".yml",false);
- if(playerConfig != null){
- // If config exists
- FileConfiguration config = playerConfig.getConfig();
- String name = config.getString("name");
- ArrayList playerDataCategories = new ArrayList<>();
- if(config.contains("categories")){
- for(String key : config.getConfigurationSection("categories").getKeys(false)){
- List discoveriesStringList = config.getStringList("categories."+key+".discoveries");
- boolean completed = config.getBoolean("categories."+key+".completed");
-
- ArrayList discoveries = new ArrayList<>();
- for(String d : discoveriesStringList){
- String[] sep = d.split(";");
- long millisActionsExecuted = 0;
- if(sep.length == 3){
- millisActionsExecuted = Long.parseLong(sep[2]);
- }
- discoveries.add(new PlayerDataDiscovery(sep[0],sep[1],millisActionsExecuted));
- }
- playerDataCategories.add(new PlayerDataCategory(key,completed,discoveries));
- }
- }
-
- playerData = new PlayerData(uuid,name);
- playerData.setCategories(playerDataCategories);
- }
-
- PlayerData finalPlayer = playerData;
-
- new BukkitRunnable(){
- @Override
- public void run() {
- callback.onDone(finalPlayer);
- }
- }.runTask(plugin);
- }
- }.runTaskAsynchronously(plugin);
- }
-
- public void saveConfig(PlayerData playerData){
- String playerName = playerData.getName();
- CommonConfig playerConfig = getConfigFile(playerData.getUuid()+".yml",true);
- FileConfiguration config = playerConfig.getConfig();
-
- config.set("name", playerName);
- config.set("categories",null);
- List categories = playerData.getCategories();
- for(PlayerDataCategory category : categories){
- String path = "categories."+category.getName();
- config.set(path+".completed",category.isCompleted());
- config.set(path+".discoveries",category.getDiscoveriesToStringList());
- }
-
- playerConfig.saveConfig();
- }
-
@Override
public void saveConfigs(){
- Map players = plugin.getPlayerDataManager().getPlayers();
- boolean isMySQL = plugin.getConfigsManager().getMainConfigManager().isMySQL();
- if(!isMySQL){
- for(Map.Entry entry : players.entrySet()) {
- PlayerData playerData = entry.getValue();
- if(playerData.isModified()){
- saveConfig(playerData);
- }
- playerData.setModified(false);
- }
- }
-
+ plugin.getCharacterJournals().saveAll();
}
+ /** Resets every character's file and every account's file from before journals were per character. */
public void resetDataForAllPlayers(String categoryName,String discoveryName){
ArrayList configs = getConfigs();
+ configs.addAll(getConfigs(CharacterStore.LEGACY));
for(CommonConfig commonConfig : configs) {
FileConfiguration config = commonConfig.getConfig();
diff --git a/src/main/java/net/tfminecraft/codex/database/HikariConnection.java b/src/main/java/net/tfminecraft/codex/database/HikariConnection.java
deleted file mode 100644
index 42f4f1e..0000000
--- a/src/main/java/net/tfminecraft/codex/database/HikariConnection.java
+++ /dev/null
@@ -1,57 +0,0 @@
-package net.tfminecraft.codex.database;
-
-import com.zaxxer.hikari.HikariConfig;
-import com.zaxxer.hikari.HikariDataSource;
-import org.bukkit.configuration.file.FileConfiguration;
-
-public class HikariConnection {
-
- private HikariDataSource hikari;
-
- public HikariConnection(FileConfiguration config) {
- HikariConfig hikariConfig = new HikariConfig();
- String path = "mysql_database";
-
- String host = config.getString(path+".host");
- int port = config.getInt(path+".port");
- String database = config.getString(path+".database");
- String username = config.getString(path+".username");
- String password = config.getString(path+".password");
-
- hikariConfig.setJdbcUrl("jdbc:mysql://" + host + ":" + port + "/" + database);
- hikariConfig.setUsername(username);
- hikariConfig.setPassword(password);
-
- if(config.contains(path+".pool.connectionTimeout")){
- hikariConfig.setConnectionTimeout(config.getLong(path+".pool.connectionTimeout"));
- }
- if(config.contains(path+".pool.maximumPoolSize")){
- hikariConfig.setMaximumPoolSize(config.getInt(path+".pool.maximumPoolSize"));
- }
- if(config.contains(path+".pool.connectionTimeout")){
- hikariConfig.setKeepaliveTime(config.getLong(path+".pool.keepaliveTime"));
- }
- if(config.contains(path+".pool.idleTimeout")){
- hikariConfig.setIdleTimeout(config.getLong(path+".pool.idleTimeout"));
- }
- if(config.contains(path+".pool.maxLifetime")){
- hikariConfig.setMaxLifetime(config.getLong(path+".pool.maxLifetime"));
- }
-
- for(String key : config.getConfigurationSection(path+".advanced").getKeys(false)){
- hikariConfig.addDataSourceProperty(key,config.get(path+".advanced."+key));
- }
-
- hikari = new HikariDataSource(hikariConfig);
- }
-
- public HikariDataSource getHikari() {
- return this.hikari;
- }
-
- public void disable() {
- if(hikari != null) {
- hikari.close();
- }
- }
-}
diff --git a/src/main/java/net/tfminecraft/codex/database/MySQLConnection.java b/src/main/java/net/tfminecraft/codex/database/MySQLConnection.java
deleted file mode 100644
index 499cb91..0000000
--- a/src/main/java/net/tfminecraft/codex/database/MySQLConnection.java
+++ /dev/null
@@ -1,321 +0,0 @@
-package net.tfminecraft.codex.database;
-
-import net.tfminecraft.codex.Codex;
-import net.tfminecraft.codex.managers.MessagesManager;
-import net.tfminecraft.codex.model.data.PlayerData;
-import net.tfminecraft.codex.model.data.PlayerDataCategory;
-import net.tfminecraft.codex.model.data.PlayerDataDiscovery;
-import org.bukkit.Bukkit;
-import org.bukkit.configuration.file.FileConfiguration;
-import org.bukkit.scheduler.BukkitRunnable;
-
-import java.sql.*;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.UUID;
-
-public class MySQLConnection {
-
- private Codex plugin;
- private HikariConnection connection;
-
- public MySQLConnection(Codex plugin){
- this.plugin = plugin;
- }
-
- public void setupMySql(){
- FileConfiguration config = plugin.getConfigsManager().getMainConfigManager().getConfig();
- try {
- connection = new HikariConnection(config);
- connection.getHikari().getConnection();
- createTables();
- Bukkit.getConsoleSender().sendMessage(MessagesManager.getLegacyColoredMessage(plugin.prefix+"&aSuccessfully connected to the Database."));
- }catch(Exception e) {
- Bukkit.getConsoleSender().sendMessage(MessagesManager.getLegacyColoredMessage(plugin.prefix+"&cError while connecting to the Database."));
- }
- }
-
- public Connection getConnection() {
- try {
- return connection.getHikari().getConnection();
- } catch (Exception e) {
- e.printStackTrace();
- return null;
- }
- }
-
- public void createTables() {
- try(Connection connection = getConnection()){
- PreparedStatement statement1 = connection.prepareStatement(
- "CREATE TABLE IF NOT EXISTS codex_players" +
- " (UUID varchar(36) NOT NULL, " +
- " PLAYER_NAME varchar(50), " +
- " PRIMARY KEY ( UUID ))"
- );
- statement1.executeUpdate();
- PreparedStatement statement2 = connection.prepareStatement(
- "CREATE TABLE IF NOT EXISTS codex_players_discoveries" +
- " (ID int NOT NULL AUTO_INCREMENT, " +
- " UUID varchar(36) NOT NULL, " +
- " CATEGORY varchar(100), " +
- " DISCOVERY varchar(100), " +
- " DATE varchar(100), " +
- " MILLIS_ACTIONS_EXECUTED bigint, " +
- " PRIMARY KEY ( ID ), " +
- " FOREIGN KEY (UUID) REFERENCES codex_players(UUID))");
- statement2.executeUpdate();
- PreparedStatement statement3 = connection.prepareStatement(
- "CREATE TABLE IF NOT EXISTS codex_players_completed_categories" +
- " (ID int NOT NULL AUTO_INCREMENT, " +
- " UUID varchar(36) NOT NULL, " +
- " CATEGORY varchar(100), " +
- " PRIMARY KEY ( ID ), " +
- " FOREIGN KEY (UUID) REFERENCES codex_players(UUID))");
- statement3.executeUpdate();
-
- DatabaseMetaData dbm = connection.getMetaData();
- ResultSet rs = dbm.getColumns(null, null, "codex_players_discoveries", "MILLIS_ACTIONS_EXECUTED");
- if (!rs.next()) {
- // Column doesn't exist.
- PreparedStatement alterStatement = connection.prepareStatement(
- "ALTER TABLE codex_players_discoveries ADD COLUMN MILLIS_ACTIONS_EXECUTED bigint");
- alterStatement.executeUpdate();
- alterStatement.close();
- }
- } catch (SQLException e) {
- e.printStackTrace();
- }
- }
-
- public void getPlayer(String uuid,PlayerCallback callback){
- new BukkitRunnable(){
- @Override
- public void run() {
- PlayerData player = null;
- try(Connection connection = getConnection()){
- PreparedStatement statement = connection.prepareStatement(
- "SELECT codex_players.UUID, codex_players.PLAYER_NAME, " +
- "(SELECT GROUP_CONCAT(cc.Category) FROM codex_players_completed_categories cc WHERE cc.UUID = codex_players.UUID) AS COMPLETED_CATEGORIES, " +
- "codex_players_discoveries.CATEGORY AS DISCOVERY_CATEGORY, " +
- "codex_players_discoveries.DISCOVERY, " +
- "codex_players_discoveries.DATE, " +
- "codex_players_discoveries.MILLIS_ACTIONS_EXECUTED " +
- "FROM codex_players " +
- "LEFT JOIN codex_players_discoveries ON codex_players.UUID = codex_players_discoveries.UUID " +
- "WHERE codex_players.UUID = ?");
-
- statement.setString(1, uuid);
- ResultSet result = statement.executeQuery();
-
- while(result.next()){
- UUID uuid = UUID.fromString(result.getString("UUID"));
- String playerName = result.getString("PLAYER_NAME");
- String completedCategories = result.getString("COMPLETED_CATEGORIES");
- String discoveryCategoryName = result.getString("DISCOVERY_CATEGORY");
- String discoveryName = result.getString("DISCOVERY");
- String discoveryDate = result.getString("DATE");
- long discoveryMillisActionsExecuted = result.getLong("MILLIS_ACTIONS_EXECUTED");
-
- if(player == null){
- player = new PlayerData(uuid,playerName);
- }
-
- if(discoveryCategoryName != null){
- boolean hasCompletedCategory = completedCategories != null && completedCategories.contains(discoveryCategoryName);
- PlayerDataCategory playerDataCategory = player.getCategory(discoveryCategoryName);
- if(playerDataCategory == null){
- playerDataCategory = new PlayerDataCategory(discoveryCategoryName,hasCompletedCategory,new ArrayList<>());
- player.getCategories().add(playerDataCategory);
- }
-
- playerDataCategory.getDiscoveries().add(new PlayerDataDiscovery(discoveryName,discoveryDate,discoveryMillisActionsExecuted));
- }
- }
-
- PlayerData finalPlayer = player;
- new BukkitRunnable(){
- @Override
- public void run() {
- callback.onDone(finalPlayer);
- }
- }.runTask(plugin);
- } catch (SQLException e) {
- e.printStackTrace();
- }
- }
- }.runTaskAsynchronously(plugin);
- }
-
- public void createPlayer(PlayerData player){
- new BukkitRunnable(){
- @Override
- public void run() {
- try(Connection connection = getConnection()){
- PreparedStatement statement = connection.prepareStatement(
- "INSERT INTO codex_players " +
- "(UUID, PLAYER_NAME) VALUE (?,?)");
-
- statement.setString(1, player.getUuid().toString());
- statement.setString(2, player.getName());
- statement.executeUpdate();
- } catch (SQLException e) {
- e.printStackTrace();
- }
- }
- }.runTaskAsynchronously(plugin);
- }
-
- public void updatePlayerName(PlayerData player){
- new BukkitRunnable(){
- @Override
- public void run() {
- try(Connection connection = getConnection()){
- PreparedStatement statement = connection.prepareStatement(
- "UPDATE codex_players SET " +
- "PLAYER_NAME=? WHERE UUID=?");
-
- statement.setString(1, player.getName());
- statement.setString(2, player.getUuid().toString());
- statement.executeUpdate();
- } catch (SQLException e) {
- e.printStackTrace();
- }
- }
- }.runTaskAsynchronously(plugin);
- }
-
- public void addDiscovery(String uuid,String categoryName,String discoveryName,String discoveryDate){
- new BukkitRunnable(){
- @Override
- public void run() {
- try(Connection connection = getConnection()){
- PreparedStatement statement = null;
- statement = connection.prepareStatement(
- "INSERT INTO codex_players_discoveries " +
- "(UUID, CATEGORY, DISCOVERY, DATE, MILLIS_ACTIONS_EXECUTED) VALUE (?,?,?,?,?)");
-
- statement.setString(1, uuid);
- statement.setString(2, categoryName);
- statement.setString(3, discoveryName);
- statement.setString(4, discoveryDate);
- statement.setLong(5, 0);
- statement.executeUpdate();
- } catch (SQLException e) {
- e.printStackTrace();
- }
- }
- }.runTaskAsynchronously(plugin);
- }
-
- public void updateMillisActionsExecuted(String uuid, String categoryName, String discoveryName) {
- new BukkitRunnable(){
- @Override
- public void run() {
- try(Connection connection = getConnection()){
- PreparedStatement statement = connection.prepareStatement(
- "UPDATE codex_players_discoveries SET " +
- "MILLIS_ACTIONS_EXECUTED=? WHERE UUID=? AND CATEGORY=? AND DISCOVERY=?");
-
- statement.setLong(1, System.currentTimeMillis());
- statement.setString(2, uuid);
- statement.setString(3, categoryName);
- statement.setString(4, discoveryName);
- statement.executeUpdate();
- } catch (SQLException e) {
- e.printStackTrace();
- }
- }
- }.runTaskAsynchronously(plugin);
- }
-
- public void updateCompletedCategories(String uuid,String categoryName){
- new BukkitRunnable(){
- @Override
- public void run() {
- try(Connection connection = getConnection()){
- PreparedStatement statement = null;
- statement = connection.prepareStatement(
- "INSERT INTO codex_players_completed_categories " +
- "(UUID, CATEGORY) VALUE (?,?)");
-
- statement.setString(1, uuid);
- statement.setString(2, categoryName);
- statement.executeUpdate();
- } catch (SQLException e) {
- e.printStackTrace();
- }
- }
- }.runTaskAsynchronously(plugin);
- }
-
- public void resetDataPlayer(String uuid, String categoryName, String discoveryName){
- new BukkitRunnable(){
- @Override
- public void run() {
- try(Connection connection = getConnection()){
- PreparedStatement statement = null;
- PreparedStatement statement2 = null;
-
- if(categoryName == null) {
- if (uuid.equals("*")) {
- // Reset all data for all players
- statement = connection.prepareStatement(
- "DELETE FROM codex_players_discoveries");
- statement2 = connection.prepareStatement(
- "DELETE FROM codex_players_completed_categories");
- }else{
- // Reset all data for player
- statement = connection.prepareStatement(
- "DELETE FROM codex_players_discoveries WHERE UUID=?");
- statement.setString(1, uuid);
- statement2 = connection.prepareStatement(
- "DELETE FROM codex_players_completed_categories WHERE UUID=?");
- statement2.setString(1, uuid);
- }
- }else{
- if(discoveryName == null) {
- if (uuid.equals("*")) {
- // Reset category for all players
- statement = connection.prepareStatement(
- "DELETE FROM codex_players_discoveries WHERE CATEGORY=?");
- statement.setString(1, categoryName);
- statement2 = connection.prepareStatement(
- "DELETE FROM codex_players_completed_categories WHERE CATEGORY=?");
- statement2.setString(1, categoryName);
- }else{
- // Reset category for player
- statement = connection.prepareStatement(
- "DELETE FROM codex_players_discoveries WHERE UUID=? AND CATEGORY=?");
- statement.setString(1, uuid);statement.setString(2, categoryName);
- statement2 = connection.prepareStatement(
- "DELETE FROM codex_players_completed_categories WHERE UUID=? AND CATEGORY=?");
- statement2.setString(1, uuid);statement2.setString(2, categoryName);
- }
- }else{
- if (uuid.equals("*")) {
- // Reset discovery for all players
- statement = connection.prepareStatement(
- "DELETE FROM codex_players_discoveries WHERE CATEGORY=? AND DISCOVERY=?");
- statement.setString(1, categoryName);statement.setString(2, discoveryName);
- }else{
- // Reset discovery for player
- statement = connection.prepareStatement(
- "DELETE FROM codex_players_discoveries WHERE UUID=? AND CATEGORY=? AND DISCOVERY=?");
- statement.setString(1, uuid);statement.setString(2, categoryName);
- statement.setString(3, discoveryName);
- }
- }
- }
-
- statement.executeUpdate();
- if(statement2 != null){
- statement2.executeUpdate();
- }
- } catch (SQLException e) {
- e.printStackTrace();
- }
- }
- }.runTaskAsynchronously(plugin);
- }
-}
diff --git a/src/main/java/net/tfminecraft/codex/database/PlayerCallback.java b/src/main/java/net/tfminecraft/codex/database/PlayerCallback.java
deleted file mode 100644
index aa6d694..0000000
--- a/src/main/java/net/tfminecraft/codex/database/PlayerCallback.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package net.tfminecraft.codex.database;
-
-
-import net.tfminecraft.codex.model.data.PlayerData;
-
-public interface PlayerCallback {
-
- void onDone(PlayerData player);
-}
diff --git a/src/main/java/net/tfminecraft/codex/listeners/PlayerListener.java b/src/main/java/net/tfminecraft/codex/listeners/PlayerListener.java
index 4827984..10e4b7f 100644
--- a/src/main/java/net/tfminecraft/codex/listeners/PlayerListener.java
+++ b/src/main/java/net/tfminecraft/codex/listeners/PlayerListener.java
@@ -3,12 +3,9 @@
import net.tfminecraft.codex.Codex;
import org.bukkit.ChatColor;
import org.bukkit.entity.LivingEntity;
-import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDeathEvent;
-import org.bukkit.event.player.PlayerJoinEvent;
-import org.bukkit.event.player.PlayerQuitEvent;
public class PlayerListener implements Listener {
@@ -17,11 +14,6 @@ public PlayerListener(Codex plugin){
this.plugin = plugin;
}
- @EventHandler
- public void onJoin(PlayerJoinEvent event){
- plugin.getPlayerDataManager().manageJoin(event.getPlayer());
- }
-
@EventHandler
public void onMobKill(EntityDeathEvent event){
LivingEntity e = event.getEntity();
@@ -30,9 +22,4 @@ public void onMobKill(EntityDeathEvent event){
plugin.getDiscoveryManager().onMobKill(e.getKiller(),e.getType().name(),customName);
}
}
-
- @EventHandler
- public void onLeave(PlayerQuitEvent event){
- plugin.getPlayerDataManager().manageLeave(event.getPlayer());
- }
}
diff --git a/src/main/java/net/tfminecraft/codex/managers/DiscoveryManager.java b/src/main/java/net/tfminecraft/codex/managers/DiscoveryManager.java
index f4126fb..cec107a 100644
--- a/src/main/java/net/tfminecraft/codex/managers/DiscoveryManager.java
+++ b/src/main/java/net/tfminecraft/codex/managers/DiscoveryManager.java
@@ -137,7 +137,8 @@ public boolean onDiscover(Player player,String categoryName,String discoveryName
Discovery discovery = category.getDiscovery(discoveryName);
PlayerDataManager playerDataManager = plugin.getPlayerDataManager();
- if(playerDataManager.hasDiscovery(player,categoryName,discoveryName)){
+ // Between characters there is no journal to record the find, so no rewards either.
+ if(!playerDataManager.isLoaded(player) || playerDataManager.hasDiscovery(player,categoryName,discoveryName)){
return false;
}
playerDataManager.addDiscovery(player,categoryName,discoveryName);
diff --git a/src/main/java/net/tfminecraft/codex/managers/InventoryManager.java b/src/main/java/net/tfminecraft/codex/managers/InventoryManager.java
index bfd3984..203c619 100644
--- a/src/main/java/net/tfminecraft/codex/managers/InventoryManager.java
+++ b/src/main/java/net/tfminecraft/codex/managers/InventoryManager.java
@@ -1,7 +1,10 @@
package net.tfminecraft.codex.managers;
import net.tfminecraft.codex.Codex;
+import net.tfminecraft.codex.character.AccountKnowledge;
+import net.tfminecraft.codex.config.MessagesConfigManager;
import net.tfminecraft.codex.config.MainConfigManager;
+import net.tfminecraft.codex.model.data.PlayerData;
import net.tfminecraft.codex.model.data.PlayerDataDiscovery;
import net.tfminecraft.codex.model.internal.CommonVariable;
import net.tfminecraft.codex.model.inventory.CommonInventory;
@@ -69,6 +72,12 @@ public void removeInventoryPlayer(Player player){
}
public void openInventory(InventoryPlayer inventoryPlayer){
+ // The menus show the played character's journal, so there is nothing to show between characters.
+ if(!plugin.getPlayerDataManager().isLoaded(inventoryPlayer.getPlayer())){
+ plugin.getMessagesManager().sendMessage(inventoryPlayer.getPlayer(),
+ plugin.getMessagesConfig().getString("noCharacterInPlay",MessagesConfigManager.NO_CHARACTER_IN_PLAY),true);
+ return;
+ }
CommonInventory inventory = getInventory(inventoryPlayer.getInventoryName());
MainConfigManager mainConfigManager = plugin.getConfigsManager().getMainConfigManager();
@@ -148,9 +157,11 @@ private void clickOnDiscoveryItem(InventoryPlayer inventoryPlayer,String discove
Player player = inventoryPlayer.getPlayer();
PlayerDataManager playerDataManager = plugin.getPlayerDataManager();
- boolean hasDiscovered = playerDataManager.hasDiscovery(player,category.getName(),discoveryName);
-
- if(!hasDiscovered){
+ // Entries any of the account's characters found stay readable, sub-pages included. Click actions
+ // with a cooldown can carry rewards, and the cooldown lives on the character's own entry, so
+ // those need the character's own discovery.
+ boolean ownDiscovery = playerDataManager.hasDiscovery(player,category.getName(),discoveryName);
+ if(!ownDiscovery && (!isSeen(player,category.getName(),discoveryName) || discovery.getClickActionsCooldown() != 0)){
return;
}
@@ -205,6 +216,10 @@ private void clickActionsItem(InventoryPlayer inventoryPlayer,String itemCommand
}
public void clickInventory(InventoryPlayer inventoryPlayer, ItemStack item, ClickType clickType){
+ if(!plugin.getPlayerDataManager().isLoaded(inventoryPlayer.getPlayer())){
+ inventoryPlayer.getPlayer().closeInventory();
+ return;
+ }
String itemActions = ItemUtils.getTagStringItem(plugin,item,"codex_item_actions");
if(itemActions != null){
clickActionsItem(inventoryPlayer,itemActions);
@@ -280,13 +295,16 @@ public ItemStack setDiscovery(String discoveryName,InventoryPlayer inventoryPlay
CommonItem commonItem;
variables.add(new CommonVariable("%name%",discovery.getName()));
- if(playerDataDiscovery != null){
+ AccountKnowledge knowledge = plugin.getCharacterJournals().knowledge(player);
+ AccountKnowledge.Known other = knowledge == null ? null : knowledge.other(category.getName(),discoveryName);
+ boolean seen = playerDataDiscovery != null || other != null;
+ if(seen){
if(discovery.getCustomLevelUnlockedItem() != null){
commonItem = discovery.getCustomLevelUnlockedItem();
}else{
commonItem = category.getDefaultLevelUnlockedItem();
}
- variables.add(new CommonVariable("%date%",playerDataDiscovery.getDiscoverDate()));
+ variables.add(new CommonVariable("%date%",playerDataDiscovery != null ? playerDataDiscovery.getDiscoverDate() : other.date()));
}else{
if(discovery.getCustomLevelBlockedItem() != null){
commonItem = discovery.getCustomLevelBlockedItem();
@@ -308,6 +326,9 @@ public ItemStack setDiscovery(String discoveryName,InventoryPlayer inventoryPlay
newLore.add(s);
}
}
+ if(seen && knowledge != null){
+ newLore.addAll(inCharacterLore(knowledge,playerDataManager.getPlayer(player),category.getName(),discoveryName));
+ }
commonItem.setLore(newLore);
commonItemManager.replaceVariables(commonItem,variables,player);
@@ -316,4 +337,31 @@ public ItemStack setDiscovery(String discoveryName,InventoryPlayer inventoryPlay
item = ItemUtils.setTagStringItem(plugin,item,"codex_discovery",discoveryName);
return item;
}
+
+ private boolean isSeen(Player player,String categoryName,String discoveryName){
+ if(plugin.getPlayerDataManager().hasDiscovery(player,categoryName,discoveryName)){
+ return true;
+ }
+ AccountKnowledge knowledge = plugin.getCharacterJournals().knowledge(player);
+ return knowledge != null && knowledge.other(categoryName,discoveryName) != null;
+ }
+
+ /** Hover lines saying which living characters can use the entry in character. */
+ private List inCharacterLore(AccountKnowledge knowledge,PlayerData current,String categoryName,String discoveryName){
+ FileConfiguration messages = plugin.getMessagesConfig();
+ List lines = new ArrayList<>();
+ lines.add("");
+ List names = knowledge.knownInCharacterBy(current,categoryName,discoveryName);
+ if(names.isEmpty()){
+ lines.add(messages.getString("knownInCharacterByNobody",MessagesConfigManager.KNOWN_BY_NOBODY));
+ }else{
+ lines.add(messages.getString("knownInCharacterBy",MessagesConfigManager.KNOWN_BY)
+ .replace("%characters%",OtherUtils.joinNames(names)));
+ }
+ if(current == null || !current.hasDiscovery(categoryName,discoveryName)){
+ lines.add(messages.getString("knownOutOfCharacterOnly",MessagesConfigManager.OUT_OF_CHARACTER_ONLY)
+ .replace("%character%",knowledge.characterName()));
+ }
+ return lines;
+ }
}
diff --git a/src/main/java/net/tfminecraft/codex/managers/PlayerDataManager.java b/src/main/java/net/tfminecraft/codex/managers/PlayerDataManager.java
index b08a553..2653b17 100644
--- a/src/main/java/net/tfminecraft/codex/managers/PlayerDataManager.java
+++ b/src/main/java/net/tfminecraft/codex/managers/PlayerDataManager.java
@@ -1,8 +1,6 @@
package net.tfminecraft.codex.managers;
import net.tfminecraft.codex.Codex;
-import net.tfminecraft.codex.config.PlayersConfigManager;
-import net.tfminecraft.codex.database.MySQLConnection;
import net.tfminecraft.codex.model.data.PlayerData;
import net.tfminecraft.codex.model.data.PlayerDataCategory;
import net.tfminecraft.codex.model.data.PlayerDataDiscovery;
@@ -13,7 +11,6 @@
import org.bukkit.Bukkit;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;
-import org.bukkit.scheduler.BukkitRunnable;
import java.util.*;
@@ -38,20 +35,13 @@ public void addPlayer(PlayerData p){
playerNames.put(p.getName(), p.getUuid());
}
- public PlayerData getPlayer(Player player, boolean create){
- PlayerData playerData = players.get(player.getUniqueId());
- if(playerData == null && create){
- playerData = new PlayerData(player.getUniqueId(),player.getName());
- addPlayer(playerData);
- }
- return playerData;
+ /** The journal of the character the player is playing, or null between characters. */
+ public PlayerData getPlayer(Player player){
+ return players.get(player.getUniqueId());
}
- private void updatePlayerName(String oldName,String newName,UUID uuid){
- if(oldName != null){
- playerNames.remove(oldName);
- }
- playerNames.put(newName,uuid);
+ public boolean isLoaded(Player player){
+ return players.containsKey(player.getUniqueId());
}
public PlayerData getPlayerByUUID(UUID uuid){
@@ -76,73 +66,19 @@ public void removePlayerByUUID(UUID uuid){
players.remove(uuid);
}
- public void manageJoin(Player player){
- if(plugin.getMySQLConnection() != null) {
- MySQLConnection mySQLConnection = plugin.getMySQLConnection();
- UUID uuid = player.getUniqueId();
- mySQLConnection.getPlayer(uuid.toString(), playerData -> {
- if (playerData != null) {
- addPlayer(playerData);
- //Update name if different
- if (!playerData.getName().equals(player.getName())) {
- updatePlayerName(playerData.getName(), player.getName(), player.getUniqueId());
- playerData.setName(player.getName());
- mySQLConnection.updatePlayerName(playerData);
- }
- } else {
- playerData = new PlayerData(uuid, player.getName());
- addPlayer(playerData);
- //Create if it doesn't exist
- mySQLConnection.createPlayer(playerData);
- }
- });
- }else{
- // Load player data from file if exists
- plugin.getConfigsManager().getPlayersConfigManager().loadConfig(player.getUniqueId(), playerData -> {
- if(playerData != null){
- addPlayer(playerData);
- if(playerData.getName() == null || !playerData.getName().equals(player.getName())){
- updatePlayerName(playerData.getName(),player.getName(),player.getUniqueId());
- playerData.setName(player.getName());
- playerData.setModified(true);
- }
- }
- });
- }
- }
-
- public void manageLeave(Player player){
- // Save player data into file and remove from map
- PlayerData playerData = getPlayer(player,false);
- if(playerData != null){
- if(plugin.getMySQLConnection() == null) {
- if(playerData.isModified()){
- new BukkitRunnable(){
- @Override
- public void run() {
- plugin.getConfigsManager().getPlayersConfigManager().saveConfig(playerData);
- }
- }.runTaskAsynchronously(plugin);
- }
- }
-
- removePlayer(playerData);
- }
- }
-
public void addDiscovery(Player player,String categoryName,String discoveryName){
- PlayerData playerData = getPlayer(player,true);
+ PlayerData playerData = getPlayer(player);
+ if(playerData == null){
+ return;
+ }
playerData.setModified(true);
String date = OtherUtils.getDate(plugin.getConfigsManager().getMainConfigManager().getDiscoveriesDateFormat());
playerData.addDiscovery(categoryName,discoveryName,date);
- if(plugin.getMySQLConnection() != null){
- plugin.getMySQLConnection().addDiscovery(player.getUniqueId().toString(),categoryName,discoveryName,date);
- }
}
public boolean hasDiscovery(Player player,String categoryName,String discoveryName){
- PlayerData playerData = getPlayer(player,false);
+ PlayerData playerData = getPlayer(player);
if(playerData == null){
return false;
}
@@ -150,7 +86,7 @@ public boolean hasDiscovery(Player player,String categoryName,String discoveryNa
}
public PlayerDataDiscovery getDiscovery(Player player,String categoryName,String discoveryName){
- PlayerData playerData = getPlayer(player,false);
+ PlayerData playerData = getPlayer(player);
if(playerData == null){
return null;
}
@@ -158,7 +94,7 @@ public PlayerDataDiscovery getDiscovery(Player player,String categoryName,String
}
public int getTotalDiscoveries(Player player,String categoryName){
- PlayerData playerData = getPlayer(player,false);
+ PlayerData playerData = getPlayer(player);
if(playerData == null){
return 0;
}
@@ -166,7 +102,7 @@ public int getTotalDiscoveries(Player player,String categoryName){
}
public int getTotalDiscoveries(Player player){
- PlayerData playerData = getPlayer(player,false);
+ PlayerData playerData = getPlayer(player);
if(playerData == null){
return 0;
}
@@ -184,16 +120,16 @@ public int getTotalDiscoveriesPercentage(Player player,int max){
}
public void completeCategory(Player player,String categoryName){
- PlayerData playerData = getPlayer(player,true);
- playerData.completeCategory(categoryName);
-
- if(plugin.getMySQLConnection() != null){
- plugin.getMySQLConnection().updateCompletedCategories(player.getUniqueId().toString(),categoryName);
+ PlayerData playerData = getPlayer(player);
+ if(playerData == null){
+ return;
}
+ playerData.setModified(true);
+ playerData.completeCategory(categoryName);
}
public boolean hasAllDiscoveries(Player player,String categoryName,int totalDiscoveries){
- PlayerData playerData = getPlayer(player,false);
+ PlayerData playerData = getPlayer(player);
if(playerData == null){
return false;
}
@@ -202,7 +138,7 @@ public boolean hasAllDiscoveries(Player player,String categoryName,int totalDisc
}
public List getCategories(Player player){
- PlayerData playerData = getPlayer(player,false);
+ PlayerData playerData = getPlayer(player);
if(playerData == null){
return new ArrayList<>();
}
@@ -211,7 +147,7 @@ public List getCategories(Player player){
}
public boolean hasCompletedCategory(Player player,String categoryName){
- PlayerData playerData = getPlayer(player,false);
+ PlayerData playerData = getPlayer(player);
if(playerData == null){
return false;
}
@@ -227,17 +163,16 @@ public boolean hasCompletedCategory(String playerName,String categoryName){
}
public void setMillisActionsExecuted(Player player,String categoryName,String discoveryName){
- PlayerData playerData = getPlayer(player,true);
+ PlayerData playerData = getPlayer(player);
+ if(playerData == null){
+ return;
+ }
playerData.setModified(true);
playerData.setMillisActionsExecuted(categoryName,discoveryName);
-
- if(plugin.getMySQLConnection() != null){
- plugin.getMySQLConnection().updateMillisActionsExecuted(player.getUniqueId().toString(),categoryName,discoveryName);
- }
}
public long getMillisActionsExecuted(Player player,String categoryName,String discoveryName){
- PlayerData playerData = getPlayer(player,false);
+ PlayerData playerData = getPlayer(player);
if(playerData == null){
return 0;
}
@@ -266,68 +201,44 @@ public String resetDataForPlayer(String playerName, String categoryName, String
}
}
- MySQLConnection mySQLConnection = plugin.getMySQLConnection();
if(categoryName == null){
// Reset all data for player
playerData.resetData();
- if(mySQLConnection != null) mySQLConnection.resetDataPlayer(playerData.getUuid().toString(),null,null);
return messagesConfig.getString("commandResetAllForPlayer").replace("%player%",playerName);
}
if(discoveryName == null){
// Reset category for player
playerData.resetCategory(categoryName);
- if(mySQLConnection != null) mySQLConnection.resetDataPlayer(playerData.getUuid().toString(),categoryName,null);
return messagesConfig.getString("commandResetCategoryForPlayer").replace("%player%",playerName)
.replace("%category%",categoryName);
}else{
// Reset discovery for player
playerData.resetDiscovery(categoryName,discoveryName);
- if(mySQLConnection != null) mySQLConnection.resetDataPlayer(playerData.getUuid().toString(),categoryName,discoveryName);
return messagesConfig.getString("commandResetDiscoveryForPlayer").replace("%player%",playerName)
.replace("%category%",categoryName).replace("%discovery%",discoveryName);
}
}
public void resetDataForAllPlayers(String categoryName, String discoveryName, FileConfiguration messagesConfig, GenericCallback callback){
- new BukkitRunnable(){
- @Override
- public void run() {
- MySQLConnection mySQLConnection = plugin.getMySQLConnection();
- if(mySQLConnection == null){
- PlayersConfigManager playersConfigsManager = plugin.getConfigsManager().getPlayersConfigManager();
- playersConfigsManager.resetDataForAllPlayers(categoryName,discoveryName);
- }
-
- new BukkitRunnable(){
- @Override
- public void run() {
-
- String result;
- if(categoryName == null){
- // Reset all data for all players (online and/or sql)
- players.values().forEach(PlayerData::resetData);
- if(mySQLConnection != null) mySQLConnection.resetDataPlayer("*",null,null);
- result = messagesConfig.getString("commandResetAllForAllPlayers");
- }else{
- if(discoveryName == null){
- // Reset category for all players (online and/or sql)
- players.values().forEach(p -> p.resetCategory(categoryName));
- if(mySQLConnection != null) mySQLConnection.resetDataPlayer("*",categoryName,null);
- result = messagesConfig.getString("commandResetCategoryForAllPlayers").replace("%category%",categoryName);
- }else{
- // Reset discovery for all players
- players.values().forEach(p -> p.resetDiscovery(categoryName,discoveryName));
- if(mySQLConnection != null) mySQLConnection.resetDataPlayer("*",categoryName,discoveryName);
- result = messagesConfig.getString("commandResetDiscoveryForAllPlayers").replace("%category%",categoryName)
- .replace("%discovery%",discoveryName);
- }
- }
-
- callback.onDone(result);
- }
- }.runTask(plugin);
- }
- }.runTaskAsynchronously(plugin);
+ // Offline characters are reset in their files, played characters in memory and saved later.
+ plugin.getConfigsManager().getPlayersConfigManager().resetDataForAllPlayers(categoryName,discoveryName);
+
+ List journals = new ArrayList<>(players.values());
+ journals.addAll(plugin.getCharacterJournals().unsavedJournals());
+ String result;
+ if(categoryName == null){
+ journals.forEach(PlayerData::resetData);
+ result = messagesConfig.getString("commandResetAllForAllPlayers");
+ }else if(discoveryName == null){
+ journals.forEach(p -> p.resetCategory(categoryName));
+ result = messagesConfig.getString("commandResetCategoryForAllPlayers").replace("%category%",categoryName);
+ }else{
+ journals.forEach(p -> p.resetDiscovery(categoryName,discoveryName));
+ result = messagesConfig.getString("commandResetDiscoveryForAllPlayers").replace("%category%",categoryName)
+ .replace("%discovery%",discoveryName);
+ }
+ plugin.getCharacterJournals().refreshKnowledge();
+ callback.onDone(result);
}
}
diff --git a/src/main/java/net/tfminecraft/codex/model/data/PlayerData.java b/src/main/java/net/tfminecraft/codex/model/data/PlayerData.java
index e0c3286..cbcb3fd 100644
--- a/src/main/java/net/tfminecraft/codex/model/data/PlayerData.java
+++ b/src/main/java/net/tfminecraft/codex/model/data/PlayerData.java
@@ -4,14 +4,17 @@
import java.util.List;
import java.util.UUID;
+/** One character's discoveries, held while its owner plays it. {@code uuid} is the owner's account. */
public class PlayerData {
private UUID uuid;
+ private UUID characterId;
private String name;
private List categories;
private boolean modified;
- public PlayerData(UUID uuid, String name){
+ public PlayerData(UUID uuid, UUID characterId, String name){
this.uuid = uuid;
+ this.characterId = characterId;
this.name = name;
this.modified = false;
this.categories = new ArrayList<>();
@@ -25,6 +28,10 @@ public void setUuid(UUID uuid) {
this.uuid = uuid;
}
+ public UUID getCharacterId() {
+ return characterId;
+ }
+
public String getName() {
return name;
}
diff --git a/src/main/java/net/tfminecraft/codex/tasks/PlayerDataSaveTask.java b/src/main/java/net/tfminecraft/codex/tasks/PlayerDataSaveTask.java
index 4872b6e..444b24b 100644
--- a/src/main/java/net/tfminecraft/codex/tasks/PlayerDataSaveTask.java
+++ b/src/main/java/net/tfminecraft/codex/tasks/PlayerDataSaveTask.java
@@ -29,7 +29,7 @@ public void run() {
}
}
- }.runTaskTimerAsynchronously(plugin, 0L, ticks);
+ }.runTaskTimer(plugin, ticks, ticks);
}
public void execute() {
diff --git a/src/main/java/net/tfminecraft/codex/utils/OtherUtils.java b/src/main/java/net/tfminecraft/codex/utils/OtherUtils.java
index 3d1edac..178acaf 100644
--- a/src/main/java/net/tfminecraft/codex/utils/OtherUtils.java
+++ b/src/main/java/net/tfminecraft/codex/utils/OtherUtils.java
@@ -1,5 +1,7 @@
package net.tfminecraft.codex.utils;
+import java.util.List;
+
import net.tfminecraft.codex.Codex;
import net.tfminecraft.codex.config.MainConfigManager;
import me.clip.placeholderapi.PlaceholderAPI;
@@ -94,4 +96,12 @@ public static String getDate(String format){
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format);
return currentDate.format(formatter);
}
+
+ /** "A", "A and B", "A, B and C". */
+ public static String joinNames(List names){
+ if(names.size() == 1){
+ return names.get(0);
+ }
+ return String.join(", ",names.subList(0,names.size()-1))+" and "+names.get(names.size()-1);
+ }
}
diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml
index eecb603..9ae8754 100644
--- a/src/main/resources/config.yml
+++ b/src/main/resources/config.yml
@@ -15,21 +15,6 @@ progress_bar_placeholder:
# Only works on Paper 1.19.4+.
use_minimessage: false
-# MySQL support credentials.
-mysql_database:
- enabled: false
- host: localhost
- port: 3306
- username: root
- password: root
- database: database
- pool:
- connectionTimeout: 5000
- advanced:
- verifyServerCertificate: false
- useSSL: true
- allowPublicKeyRetrieval: true
-
# Don't modify.
config_version: 2
diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml
index 694b7fb..01c7dab 100644
--- a/src/main/resources/messages.yml
+++ b/src/main/resources/messages.yml
@@ -25,6 +25,10 @@ playerOpenInventory: "&aInventory &7%inventory% &aopened for &e%player%&a!"
commandDoesNotExist: "&cThat command doesn't exist."
pluginCriticalErrors: "&cThe plugin has detected some errors. Check them using &7/codex verify"
clickActionsCooldown: "&cYou must wait &7%time% &cbefore using this item again."
+knownInCharacterBy: "&7%characters% can use this in character."
+knownInCharacterByNobody: "&7No character can use this in character."
+knownOutOfCharacterOnly: "&8&o%character% knows this only out of character."
+noCharacterInPlay: "&cChoose a character to open the Codex."
seconds: s
minutes: m
hours: h
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
index 4b02c4a..934935f 100644
--- a/src/main/resources/plugin.yml
+++ b/src/main/resources/plugin.yml
@@ -3,6 +3,7 @@ version: ${project.version}
name: Codex
api-version: '1.21.10'
author: Ajneb97
+depend: [TLibs]
softdepend: [WorldGuard,WorldEdit,PlaceholderAPI,MythicMobs,Residence]
commands:
diff --git a/src/test/java/net/tfminecraft/codex/character/AccountKnowledgeTest.java b/src/test/java/net/tfminecraft/codex/character/AccountKnowledgeTest.java
new file mode 100644
index 0000000..51ee2ad
--- /dev/null
+++ b/src/test/java/net/tfminecraft/codex/character/AccountKnowledgeTest.java
@@ -0,0 +1,55 @@
+package net.tfminecraft.codex.character;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import org.junit.jupiter.api.Test;
+
+import net.tfminecraft.codex.model.data.PlayerData;
+import net.tfminecraft.codex.model.data.PlayerDataCategory;
+import net.tfminecraft.codex.model.data.PlayerDataDiscovery;
+
+class AccountKnowledgeTest {
+ @Test
+ void namesLivingCharactersWithThePlayedOneFirst() {
+ AccountKnowledge.Builder builder = new AccountKnowledge.Builder();
+ builder.add(journal("aurea", "01/10/2026"), "Mira", true);
+ builder.add(journal("aurea", "05/10/2026"), "Old Tom", false);
+ builder.add(journal("aurea", "06/10/2026"), "Hazel", true);
+ AccountKnowledge knowledge = builder.build("Aldric");
+ PlayerData current = new PlayerData(UUID.randomUUID(), UUID.randomUUID(), "Steve");
+ current.addDiscovery("poi", "aurea", "14/10/2026");
+
+ assertEquals("Aldric", knowledge.characterName());
+ assertEquals("01/10/2026", knowledge.other("poi", "aurea").date());
+ assertEquals(List.of("Aldric", "Mira", "Hazel"), knowledge.knownInCharacterBy(current, "poi", "aurea"));
+ assertEquals(List.of("Mira", "Hazel"), knowledge.knownInCharacterBy(null, "poi", "aurea"));
+ }
+
+ @Test
+ void entriesOnlyDeadCharactersFoundAreSeenButNamedByNobody() {
+ AccountKnowledge.Builder builder = new AccountKnowledge.Builder();
+ builder.add(journal("vard", "01/10/2026"), "Old Tom", false);
+ AccountKnowledge knowledge = builder.build("Aldric");
+
+ assertEquals(List.of(), knowledge.other("poi", "vard").livingNames());
+ assertEquals(List.of(), knowledge.knownInCharacterBy(null, "poi", "vard"));
+ }
+
+ @Test
+ void anEmptyAccountKnowsNothingElse() {
+ AccountKnowledge knowledge = AccountKnowledge.empty("Aldric");
+
+ assertNull(knowledge.other("poi", "aurea"));
+ assertEquals(List.of(), knowledge.knownInCharacterBy(null, "poi", "aurea"));
+ }
+
+ private static List journal(String discovery, String date) {
+ ArrayList found = new ArrayList<>(List.of(new PlayerDataDiscovery(discovery, date, 0)));
+ return List.of(new PlayerDataCategory("poi", false, found));
+ }
+}
diff --git a/src/test/java/net/tfminecraft/codex/character/CharacterJournalsTest.java b/src/test/java/net/tfminecraft/codex/character/CharacterJournalsTest.java
new file mode 100644
index 0000000..37febfe
--- /dev/null
+++ b/src/test/java/net/tfminecraft/codex/character/CharacterJournalsTest.java
@@ -0,0 +1,321 @@
+package net.tfminecraft.codex.character;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+import org.bukkit.event.player.PlayerQuitEvent;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.MockedStatic;
+
+import net.tfminecraft.codex.Codex;
+import net.tfminecraft.codex.managers.PlayerDataManager;
+import net.tfminecraft.codex.model.data.PlayerData;
+import net.tfminecraft.tlibs.character.CharacterInfo;
+import net.tfminecraft.tlibs.character.CharacterRef;
+import net.tfminecraft.tlibs.character.CharacterSession;
+import net.tfminecraft.tlibs.character.CharacterSessionEndEvent;
+import net.tfminecraft.tlibs.character.CharacterSessionStartEvent;
+import net.tfminecraft.tlibs.character.CharacterStatus;
+import net.tfminecraft.tlibs.character.Characters;
+
+class CharacterJournalsTest {
+ @TempDir
+ Path folder;
+
+ private final UUID owner = UUID.randomUUID();
+ private final UUID first = UUID.randomUUID();
+ private final UUID second = UUID.randomUUID();
+ private final List logs = new ArrayList<>();
+ private MockedStatic characters;
+ private PlayerDataManager players;
+ private CharacterStore store;
+ private CharacterJournals journals;
+ private Player player;
+
+ @BeforeEach
+ void setUp() {
+ characters = mockStatic(Characters.class);
+ players = new PlayerDataManager(mock(Codex.class));
+ store = new CharacterStore(folder);
+ Logger logger = Logger.getAnonymousLogger();
+ logger.setUseParentHandlers(false);
+ logger.addHandler(new Handler() {
+ @Override
+ public void publish(LogRecord record) {
+ logs.add(record);
+ }
+
+ @Override
+ public void flush() {
+ }
+
+ @Override
+ public void close() {
+ }
+ });
+ journals = new CharacterJournals(players, store, logger);
+ player = mock(Player.class);
+ when(player.getUniqueId()).thenReturn(owner);
+ when(player.getName()).thenReturn("Steve");
+ }
+
+ @AfterEach
+ void tearDown() {
+ characters.close();
+ }
+
+ @Test
+ void startingACharacterOpensItsOwnJournal() throws IOException {
+ saved(second, "aurea");
+
+ journals.onStart(start(second));
+
+ PlayerData data = players.getPlayer(player);
+ assertEquals(second, data.getCharacterId());
+ assertEquals("Steve", data.getName());
+ assertTrue(data.hasDiscovery("poi", "aurea"));
+ }
+
+ @Test
+ void switchingSavesTheOutgoingJournalFirst() throws IOException {
+ journals.onStart(start(first));
+ players.getPlayer(player).addDiscovery("poi", "vard", "14/10/2026");
+
+ journals.onStart(start(second));
+
+ assertEquals(second, players.getPlayer(player).getCharacterId());
+ assertFalse(players.getPlayer(player).hasDiscovery("poi", "vard"));
+ assertTrue(store.load(owner, first, "Steve").hasDiscovery("poi", "vard"));
+ }
+
+ @Test
+ void theAccountJournalIsReferenceOnlyAndStaysInPlace() throws IOException {
+ legacy("syrindell");
+
+ journals.onStart(start(first));
+
+ assertFalse(players.getPlayer(player).hasDiscovery("poi", "syrindell"));
+ AccountKnowledge.Known old = journals.knowledge(player).other("poi", "syrindell");
+ assertEquals("24/09/2026", old.date());
+ assertEquals(List.of(), old.livingNames());
+ assertTrue(store.hasLegacy(owner));
+ }
+
+ @Test
+ void anUnreadableAccountJournalIsLeftOut() throws IOException {
+ Files.createDirectories(folder.resolve("players"));
+ Files.writeString(folder.resolve("players").resolve(owner + ".yml"), "name: [unclosed");
+
+ journals.onStart(start(first));
+
+ assertNotNull(players.getPlayer(player));
+ assertLogged(Level.WARNING, "The account Codex of " + owner + " could not be read");
+ }
+
+ @Test
+ void anUnreadableJournalStaysClosed() throws IOException {
+ Files.createDirectories(folder.resolve("characters"));
+ Files.writeString(store.characterFile(first), "categories: [unclosed");
+
+ journals.onStart(start(first));
+
+ assertNull(players.getPlayer(player));
+ assertLogged(Level.SEVERE, "could not be read");
+ }
+
+ @Test
+ void endingASessionSavesOnlyThatCharacter() throws IOException {
+ journals.onStart(start(first));
+ players.getPlayer(player).addDiscovery("poi", "vard", "14/10/2026");
+
+ journals.onEnd(end(second));
+ assertNotNull(players.getPlayer(player));
+
+ journals.onEnd(end(first));
+ assertNull(players.getPlayer(player));
+ assertNull(journals.knowledge(player));
+ assertTrue(store.load(owner, first, "Steve").hasDiscovery("poi", "vard"));
+ }
+
+ @Test
+ void quittingSavesAndClosesAnyJournal() {
+ journals.onQuit(new PlayerQuitEvent(player, "bye"));
+ journals.onStart(start(first));
+
+ journals.onQuit(new PlayerQuitEvent(player, "bye"));
+
+ assertNull(players.getPlayer(player));
+ assertTrue(Files.exists(store.characterFile(first)));
+ }
+
+ @Test
+ void unchangedJournalsAreNotRewritten() throws IOException {
+ saved(first, "aurea");
+ journals.onStart(start(first));
+ Files.delete(store.characterFile(first));
+
+ journals.saveAll();
+
+ assertFalse(Files.exists(store.characterFile(first)));
+ }
+
+ @Test
+ void aFailedSaveIsLogged() throws IOException {
+ journals.onStart(start(first));
+ Files.writeString(folder.resolve("characters"), "not a folder");
+
+ journals.saveAll();
+
+ assertLogged(Level.SEVERE, "Could not save Codex for character " + first);
+ }
+
+ @Test
+ void aJournalThatFailedToSaveIsKeptUntilItSaves() throws IOException {
+ characters.when(() -> Characters.characters(owner)).thenReturn(Optional.of(List.of(
+ info(first, "Aldric", CharacterStatus.ALIVE), info(second, "Mira", CharacterStatus.ALIVE))));
+ journals.onStart(start(first));
+ players.getPlayer(player).addDiscovery("poi", "vard", "14/10/2026");
+ Path characters = folder.resolve("characters");
+ Files.writeString(characters, "not a folder");
+
+ journals.onStart(start(second));
+
+ assertEquals(second, players.getPlayer(player).getCharacterId());
+ assertEquals(1, journals.unsavedJournals().size());
+ assertEquals(List.of("Aldric"), journals.knowledge(player).other("poi", "vard").livingNames());
+ journals.saveAll();
+ assertEquals(1, journals.unsavedJournals().size());
+
+ journals.onStart(start(first));
+ assertTrue(players.getPlayer(player).hasDiscovery("poi", "vard"));
+ assertEquals(List.of(second), journals.unsavedJournals().stream().map(PlayerData::getCharacterId).toList());
+
+ Files.delete(characters);
+ journals.saveAll();
+ assertTrue(journals.unsavedJournals().isEmpty());
+ assertTrue(store.load(owner, first, "Steve").hasDiscovery("poi", "vard"));
+ assertTrue(Files.exists(store.characterFile(second)));
+ }
+
+ @Test
+ void startOpensCharactersAlreadyInPlayAndStopSaves() {
+ characters.when(() -> Characters.currentSession(player))
+ .thenReturn(Optional.of(new CharacterSession(new CharacterRef(owner, second), 1)));
+ try (MockedStatic bukkit = mockStatic(Bukkit.class)) {
+ bukkit.when(Bukkit::getOnlinePlayers).thenReturn(List.of(player));
+
+ journals.start();
+ }
+ assertEquals(second, players.getPlayer(player).getCharacterId());
+
+ journals.stop();
+ assertTrue(Files.exists(store.characterFile(second)));
+ }
+
+ @Test
+ void otherCharactersDiscoveriesStayReadable() throws IOException {
+ UUID dead = UUID.randomUUID();
+ UUID unplayed = UUID.randomUUID();
+ UUID broken = UUID.randomUUID();
+ saved(second, "aurea");
+ saved(dead, "vard");
+ Files.writeString(store.characterFile(broken), "categories: [unclosed");
+ characters.when(() -> Characters.characters(owner)).thenReturn(Optional.of(List.of(
+ info(first, "Aldric", CharacterStatus.ALIVE), info(second, "Mira", CharacterStatus.ALIVE),
+ info(dead, "Old Tom", CharacterStatus.DEAD), info(unplayed, "Hazel", CharacterStatus.ALIVE),
+ info(broken, "Bram", CharacterStatus.ALIVE))));
+
+ journals.onStart(start(first));
+
+ AccountKnowledge knowledge = journals.knowledge(player);
+ assertEquals("Aldric", knowledge.characterName());
+ assertEquals(List.of("Mira"), knowledge.other("poi", "aurea").livingNames());
+ assertEquals(List.of(), knowledge.other("poi", "vard").livingNames());
+ assertLogged(Level.WARNING, "Codex for character " + broken + " could not be read");
+ }
+
+ @Test
+ void aBrokenRosterLookupStillOpensTheJournal() {
+ characters.when(() -> Characters.characters(owner)).thenThrow(new IllegalStateException("boom"));
+
+ journals.onStart(start(first));
+
+ assertNotNull(players.getPlayer(player));
+ assertEquals("Steve", journals.knowledge(player).characterName());
+ assertLogged(Level.WARNING, "Could not read the other characters of account " + owner);
+ }
+
+ @Test
+ void refreshingRereadsOtherCharacters() throws IOException {
+ characters.when(() -> Characters.characters(owner)).thenReturn(Optional.of(List.of(
+ info(first, "Aldric", CharacterStatus.ALIVE), info(second, "Mira", CharacterStatus.ALIVE))));
+ journals.onStart(start(first));
+ assertNull(journals.knowledge(player).other("poi", "aurea"));
+
+ saved(second, "aurea");
+ journals.refreshKnowledge();
+
+ assertEquals(List.of("Mira"), journals.knowledge(player).other("poi", "aurea").livingNames());
+ assertEquals("Aldric", journals.knowledge(player).characterName());
+ }
+
+ private CharacterInfo info(UUID character, String name, CharacterStatus status) {
+ return new CharacterInfo(new CharacterRef(owner, character), name, status, null, Set.of(), 0, null);
+ }
+
+ private CharacterSessionStartEvent start(UUID character) {
+ return new CharacterSessionStartEvent(player, new CharacterSession(new CharacterRef(owner, character), 1));
+ }
+
+ private CharacterSessionEndEvent end(UUID character) {
+ return new CharacterSessionEndEvent(player, new CharacterSession(new CharacterRef(owner, character), 1),
+ CharacterSessionEndEvent.Reason.SWITCH);
+ }
+
+ private void saved(UUID character, String discovery) throws IOException {
+ PlayerData data = new PlayerData(owner, character, "Steve");
+ data.addDiscovery("poi", discovery, "01/10/2026");
+ store.save(data);
+ }
+
+ private void legacy(String discovery) throws IOException {
+ Files.createDirectories(folder.resolve("players"));
+ Files.writeString(folder.resolve("players").resolve(owner + ".yml"), """
+ name: Steve
+ categories:
+ poi:
+ completed: false
+ discoveries:
+ - %s;24/09/2026;0
+ """.formatted(discovery));
+ }
+
+ private void assertLogged(Level level, String text) {
+ assertTrue(logs.stream().anyMatch(record -> record.getLevel().equals(level) && record.getMessage().contains(text)),
+ () -> "missing " + level + " log containing: " + text);
+ }
+}
diff --git a/src/test/java/net/tfminecraft/codex/character/CharacterStoreTest.java b/src/test/java/net/tfminecraft/codex/character/CharacterStoreTest.java
new file mode 100644
index 0000000..cba8ee5
--- /dev/null
+++ b/src/test/java/net/tfminecraft/codex/character/CharacterStoreTest.java
@@ -0,0 +1,148 @@
+package net.tfminecraft.codex.character;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import net.tfminecraft.codex.model.data.PlayerData;
+import net.tfminecraft.codex.model.data.PlayerDataCategory;
+import net.tfminecraft.codex.model.data.PlayerDataDiscovery;
+
+class CharacterStoreTest {
+ @TempDir
+ Path folder;
+
+ private CharacterStore store;
+ private final UUID owner = UUID.randomUUID();
+ private final UUID character = UUID.randomUUID();
+
+ @BeforeEach
+ void setUp() {
+ store = new CharacterStore(folder);
+ }
+
+ @Test
+ void newCharacterStartsEmptyAndIsSavedLater() throws IOException {
+ PlayerData data = store.load(owner, character, "Steve");
+
+ assertEquals(owner, data.getUuid());
+ assertEquals(character, data.getCharacterId());
+ assertEquals("Steve", data.getName());
+ assertTrue(data.getCategories().isEmpty());
+ assertTrue(data.isModified());
+ }
+
+ @Test
+ void savedJournalLoadsBackWithEveryField() throws IOException {
+ PlayerData data = store.load(owner, character, "Steve");
+ data.addDiscovery("poi", "syrindell", "24/09/2026");
+ data.getCategory("poi").getDiscovery("syrindell").setMillisActionsExecuted(42);
+ data.completeCategory("poi");
+
+ store.save(data);
+
+ assertFalse(data.isModified());
+ String text = Files.readString(store.characterFile(character));
+ assertTrue(text.contains("owner: " + owner));
+ assertTrue(text.contains("name: Steve"));
+ assertFalse(Files.exists(folder.resolve("characters").resolve(character + ".yml.tmp")));
+
+ PlayerData loaded = store.load(owner, character, "Alex");
+ assertFalse(loaded.isModified());
+ assertEquals("Alex", loaded.getName());
+ PlayerDataCategory poi = loaded.getCategory("poi");
+ assertTrue(poi.isCompleted());
+ PlayerDataDiscovery found = poi.getDiscovery("syrindell");
+ assertEquals("24/09/2026", found.getDiscoverDate());
+ assertEquals(42, found.getMillisActionsExecuted());
+ }
+
+ @Test
+ void oldEntriesWithoutMillisOrDateStillLoad() throws IOException {
+ write("characters", character, """
+ categories:
+ poi:
+ discoveries:
+ - aurea;01/10/2026
+ - vard
+ - thalendor;02/10/2026;soon
+ - ;03/10/2026;0
+ """);
+
+ PlayerDataCategory poi = store.load(owner, character, "Steve").getCategory("poi");
+
+ assertFalse(poi.isCompleted());
+ assertEquals(3, poi.getDiscoveries().size());
+ assertEquals(0, poi.getDiscovery("aurea").getMillisActionsExecuted());
+ assertEquals("", poi.getDiscovery("vard").getDiscoverDate());
+ assertEquals(0, poi.getDiscovery("thalendor").getMillisActionsExecuted());
+ }
+
+ @Test
+ void unreadableFilesAreErrorsNotEmptyJournals() throws IOException {
+ write("characters", character, "categories: [unclosed");
+ write("players", owner, "name: [unclosed");
+
+ assertThrows(IOException.class, () -> store.load(owner, character, "Steve"));
+ assertThrows(IOException.class, () -> store.readLegacy(owner));
+ }
+
+ @Test
+ void failedSaveKeepsTheJournalModified() throws IOException {
+ Files.writeString(folder.resolve("characters"), "not a folder");
+ PlayerData data = new PlayerData(owner, character, "Steve");
+ data.setModified(true);
+
+ assertThrows(IOException.class, () -> store.save(data));
+ assertTrue(data.isModified());
+ }
+
+ @Test
+ void legacyJournalIsReadInPlace() throws IOException {
+ assertFalse(store.hasLegacy(owner));
+ write("players", owner, """
+ name: Steve
+ categories:
+ achievements:
+ completed: true
+ discoveries:
+ - die;20/09/2026;0
+ """);
+
+ CharacterStore.LegacyJournal legacy = store.readLegacy(owner);
+
+ assertTrue(store.hasLegacy(owner));
+ assertEquals("Steve", legacy.name());
+ assertTrue(legacy.categories().get(0).isCompleted());
+ assertTrue(legacy.categories().get(0).hasDiscovery("die"));
+ }
+
+ @Test
+ void legacyJournalWithoutCategoriesIsEmpty() throws IOException {
+ write("players", owner, "x: 1\n");
+
+ CharacterStore.LegacyJournal legacy = store.readLegacy(owner);
+
+ assertNull(legacy.name());
+ assertEquals(new ArrayList<>(), legacy.categories());
+ }
+
+ private void write(String directory, UUID id, String text) throws IOException {
+ Path dir = folder.resolve(directory);
+ Files.createDirectories(dir);
+ Files.writeString(dir.resolve(id + ".yml"), text);
+ }
+}
diff --git a/src/test/java/net/tfminecraft/codex/utils/OtherUtilsTest.java b/src/test/java/net/tfminecraft/codex/utils/OtherUtilsTest.java
new file mode 100644
index 0000000..7f7ccb6
--- /dev/null
+++ b/src/test/java/net/tfminecraft/codex/utils/OtherUtilsTest.java
@@ -0,0 +1,16 @@
+package net.tfminecraft.codex.utils;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+class OtherUtilsTest {
+ @Test
+ void joinsNamesForAHoverLine() {
+ assertEquals("Aldric", OtherUtils.joinNames(List.of("Aldric")));
+ assertEquals("Aldric and Mira", OtherUtils.joinNames(List.of("Aldric", "Mira")));
+ assertEquals("Aldric, Mira and Hazel", OtherUtils.joinNames(List.of("Aldric", "Mira", "Hazel")));
+ }
+}