From df285cf6e79b940180f5267c7b9be26423507dbd Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 26 Apr 2026 05:16:59 +0000 Subject: [PATCH 1/2] Merge HolyScript feature and implement DevoteMe API - Integrated Bible reading feature from HolyScript. - Added Bible model, API client, and caching. - Implemented `/bible` and `/bibledefault` commands. - Exposed `DevoteMeAPI` for other plugins to access VOTD, Devotions, and Bible content. - Updated player data storage to persist Bible version preferences. - Added Bible-related configuration to config.yml. Co-authored-by: benrobson <15405528+benrobson@users.noreply.github.com> --- .../devoteme/minecraft/DevoteMePlugin.java | 37 ++++ .../devoteme/minecraft/api/DevoteMeAPI.java | 36 ++++ .../minecraft/api/DevoteMeAPIImpl.java | 34 +++ .../minecraft/api/DevoteMeAPIProvider.java | 38 ++++ .../minecraft/api/bible/BibleApiClient.java | 132 ++++++++++++ .../minecraft/cache/BibleCacheManager.java | 127 +++++++++++ .../minecraft/commands/BibleCommand.java | 144 +++++++++++++ .../commands/BibleDefaultCommand.java | 76 +++++++ .../minecraft/model/bible/BibleBook.java | 114 ++++++++++ .../minecraft/model/bible/BibleVersion.java | 32 +++ .../minecraft/storage/PlayerDataStore.java | 43 +++- .../minecraft/util/BibleBookBuilder.java | 204 ++++++++++++++++++ src/main/resources/config.yml | 32 +++ src/main/resources/plugin.yml | 12 ++ .../minecraft/model/bible/BibleModelTest.java | 28 +++ 15 files changed, 1081 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/devoteme/minecraft/api/DevoteMeAPI.java create mode 100644 src/main/java/com/devoteme/minecraft/api/DevoteMeAPIImpl.java create mode 100644 src/main/java/com/devoteme/minecraft/api/DevoteMeAPIProvider.java create mode 100644 src/main/java/com/devoteme/minecraft/api/bible/BibleApiClient.java create mode 100644 src/main/java/com/devoteme/minecraft/cache/BibleCacheManager.java create mode 100644 src/main/java/com/devoteme/minecraft/commands/BibleCommand.java create mode 100644 src/main/java/com/devoteme/minecraft/commands/BibleDefaultCommand.java create mode 100644 src/main/java/com/devoteme/minecraft/model/bible/BibleBook.java create mode 100644 src/main/java/com/devoteme/minecraft/model/bible/BibleVersion.java create mode 100644 src/main/java/com/devoteme/minecraft/util/BibleBookBuilder.java create mode 100644 src/test/java/com/devoteme/minecraft/model/bible/BibleModelTest.java diff --git a/src/main/java/com/devoteme/minecraft/DevoteMePlugin.java b/src/main/java/com/devoteme/minecraft/DevoteMePlugin.java index c76de87..609aee5 100644 --- a/src/main/java/com/devoteme/minecraft/DevoteMePlugin.java +++ b/src/main/java/com/devoteme/minecraft/DevoteMePlugin.java @@ -1,8 +1,14 @@ package com.devoteme.minecraft; +import com.devoteme.minecraft.api.DevoteMeAPIImpl; +import com.devoteme.minecraft.api.DevoteMeAPIProvider; import com.devoteme.minecraft.api.DevoteMeApiClient; import com.devoteme.minecraft.api.DevoteMeParser; +import com.devoteme.minecraft.api.bible.BibleApiClient; +import com.devoteme.minecraft.cache.BibleCacheManager; import com.devoteme.minecraft.cache.ContentCache; +import com.devoteme.minecraft.commands.BibleCommand; +import com.devoteme.minecraft.commands.BibleDefaultCommand; import com.devoteme.minecraft.commands.DevotionCommand; import com.devoteme.minecraft.commands.VotdCommand; import com.devoteme.minecraft.holograms.DecentHologramsService; @@ -23,6 +29,8 @@ public class DevoteMePlugin extends JavaPlugin { private DevoteMeApiClient api; private ContentCache cache; + private BibleCacheManager bibleCacheManager; + private BibleApiClient bibleApiClient; private VotdLocationStore locationStore; private PlayerDataStore playerDataStore; private HologramService holograms; @@ -38,6 +46,11 @@ public void onEnable() { ); this.locationStore = new VotdLocationStore(this); this.playerDataStore = new PlayerDataStore(this); + this.bibleApiClient = new BibleApiClient(this); + this.bibleCacheManager = new BibleCacheManager(this, bibleApiClient); + + // Register API + DevoteMeAPIProvider.register(new DevoteMeAPIImpl(this)); // Holograms (soft) this.holograms = new DecentHologramsService(this, cache, locationStore); @@ -59,6 +72,20 @@ public void onEnable() { devotion.setExecutor(cmd); } + PluginCommand bible = getCommand("bible"); + if (bible != null) { + BibleCommand cmd = new BibleCommand(this); + bible.setExecutor(cmd); + bible.setTabCompleter(cmd); + } + + PluginCommand bibleDefault = getCommand("bibledefault"); + if (bibleDefault != null) { + BibleDefaultCommand cmd = new BibleDefaultCommand(this); + bibleDefault.setExecutor(cmd); + bibleDefault.setTabCompleter(cmd); + } + // Load stored data locationStore.load(); playerDataStore.load(); @@ -112,11 +139,21 @@ public void sendVotdTo(Player p) { } } + @Override + public void onDisable() { + DevoteMeAPIProvider.unregister(); + } + public void sendNoPerm(org.bukkit.command.CommandSender sender) { sender.sendMessage(Text.render(getConfig().getString("messages.noPermission", "No permission."))); } + public String getMessage(String path) { + return getConfig().getString("messages." + path, "").replace("&", "§"); + } + public ContentCache getCache() { return cache; } + public BibleCacheManager getBibleCacheManager() { return bibleCacheManager; } public VotdLocationStore getLocationStore() { return locationStore; } public PlayerDataStore getPlayerDataStore() { return playerDataStore; } public HologramService getHolograms() { return holograms; } diff --git a/src/main/java/com/devoteme/minecraft/api/DevoteMeAPI.java b/src/main/java/com/devoteme/minecraft/api/DevoteMeAPI.java new file mode 100644 index 0000000..c358813 --- /dev/null +++ b/src/main/java/com/devoteme/minecraft/api/DevoteMeAPI.java @@ -0,0 +1,36 @@ +package com.devoteme.minecraft.api; + +import com.devoteme.minecraft.api.bible.BibleApiClient; +import com.devoteme.minecraft.model.Devotion; +import com.devoteme.minecraft.model.Votd; +import com.devoteme.minecraft.model.bible.BibleBook; +import com.devoteme.minecraft.model.bible.BibleVersion; + +import java.util.concurrent.CompletableFuture; + +/** + * Public API for DevoteMe plugin. + */ +public interface DevoteMeAPI { + + /** + * Get the current Verse of the Day. + * @return A CompletableFuture containing the VOTD. + */ + Votd getVotd(); + + /** + * Get today's devotional. + * @return A CompletableFuture containing the Devotion. + */ + Devotion getDevotion(); + + /** + * Fetch a Bible chapter from the configured API. + * @param version The Bible version. + * @param book The Bible book. + * @param chapter The chapter number. + * @return A CompletableFuture containing the chapter content. + */ + CompletableFuture getBibleChapter(BibleVersion version, BibleBook book, int chapter); +} diff --git a/src/main/java/com/devoteme/minecraft/api/DevoteMeAPIImpl.java b/src/main/java/com/devoteme/minecraft/api/DevoteMeAPIImpl.java new file mode 100644 index 0000000..ffd9ce2 --- /dev/null +++ b/src/main/java/com/devoteme/minecraft/api/DevoteMeAPIImpl.java @@ -0,0 +1,34 @@ +package com.devoteme.minecraft.api; + +import com.devoteme.minecraft.DevoteMePlugin; +import com.devoteme.minecraft.api.bible.BibleApiClient; +import com.devoteme.minecraft.model.Devotion; +import com.devoteme.minecraft.model.Votd; +import com.devoteme.minecraft.model.bible.BibleBook; +import com.devoteme.minecraft.model.bible.BibleVersion; + +import java.util.concurrent.CompletableFuture; + +public class DevoteMeAPIImpl implements DevoteMeAPI { + + private final DevoteMePlugin plugin; + + public DevoteMeAPIImpl(DevoteMePlugin plugin) { + this.plugin = plugin; + } + + @Override + public Votd getVotd() { + return plugin.getCache().getVotd(); + } + + @Override + public Devotion getDevotion() { + return plugin.getCache().getDevotion(); + } + + @Override + public CompletableFuture getBibleChapter(BibleVersion version, BibleBook book, int chapter) { + return plugin.getBibleCacheManager().getChapter(version, book, chapter); + } +} diff --git a/src/main/java/com/devoteme/minecraft/api/DevoteMeAPIProvider.java b/src/main/java/com/devoteme/minecraft/api/DevoteMeAPIProvider.java new file mode 100644 index 0000000..84e5b13 --- /dev/null +++ b/src/main/java/com/devoteme/minecraft/api/DevoteMeAPIProvider.java @@ -0,0 +1,38 @@ +package com.devoteme.minecraft.api; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * Provider for the DevoteMe API. + */ +public final class DevoteMeAPIProvider { + + private static DevoteMeAPI api; + + private DevoteMeAPIProvider() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); + } + + /** + * Get the DevoteMe API instance. + * @return The API instance. + * @throws IllegalStateException If the API has not been initialized. + */ + public static @NotNull DevoteMeAPI get() { + if (api == null) { + throw new IllegalStateException("DevoteMe API has not been initialized yet!"); + } + return api; + } + + @ApiStatus.Internal + public static void register(DevoteMeAPI instance) { + api = instance; + } + + @ApiStatus.Internal + public static void unregister() { + api = null; + } +} diff --git a/src/main/java/com/devoteme/minecraft/api/bible/BibleApiClient.java b/src/main/java/com/devoteme/minecraft/api/bible/BibleApiClient.java new file mode 100644 index 0000000..659f382 --- /dev/null +++ b/src/main/java/com/devoteme/minecraft/api/bible/BibleApiClient.java @@ -0,0 +1,132 @@ +package com.devoteme.minecraft.api.bible; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.devoteme.minecraft.model.bible.BibleBook; +import com.devoteme.minecraft.model.bible.BibleVersion; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.concurrent.CompletableFuture; + +public class BibleApiClient { + + private final JavaPlugin plugin; + private final String baseUrl; + private final String apiKey; + private final int timeout; + + public BibleApiClient(JavaPlugin plugin) { + this.plugin = plugin; + this.baseUrl = plugin.getConfig().getString("bible.api.base-url", "https://api.scripture.api.bible"); + this.apiKey = plugin.getConfig().getString("bible.api.api-key", ""); + this.timeout = plugin.getConfig().getInt("bible.api.timeout", 10) * 1000; + + if (apiKey.isEmpty() || apiKey.equals("YOUR_API_KEY_HERE")) { + plugin.getLogger().severe("=========================================================="); + plugin.getLogger().severe("BIBLE API KEY NOT CONFIGURED!"); + plugin.getLogger().severe("Please get a free API key from https://scripture.api.bible"); + plugin.getLogger().severe("and set it in config.yml under 'bible.api.api-key'"); + plugin.getLogger().severe("=========================================================="); + } + } + + public CompletableFuture fetchChapter(BibleVersion version, BibleBook book, int chapter) { + return CompletableFuture.supplyAsync(() -> { + try { + String chapterId = book.getAbbreviation() + "." + chapter; + String urlString = baseUrl + "/v1/bibles/" + version.getApiId() + "/chapters/" + chapterId; + + URL url = new URL(urlString); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(timeout); + conn.setReadTimeout(timeout); + conn.setRequestProperty("Accept", "application/json"); + conn.setRequestProperty("api-key", apiKey); + + int responseCode = conn.getResponseCode(); + if (responseCode != 200) { + if (responseCode == 401) { + plugin.getLogger().severe("Bible API Authentication failed! Check your API key in config.yml"); + plugin.getLogger().severe("Current Bible API key starts with: " + + (apiKey.length() > 10 ? apiKey.substring(0, 10) + "..." : "EMPTY OR TOO SHORT")); + plugin.getLogger().severe("Get a valid key from https://scripture.api.bible"); + } + throw new RuntimeException("Bible API returned status code: " + responseCode); + } + + BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); + StringBuilder response = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + response.append(line); + } + reader.close(); + + JsonObject json = JsonParser.parseString(response.toString()).getAsJsonObject(); + JsonObject data = json.getAsJsonObject("data"); + String content = data.get("content").getAsString(); + + // Clean HTML content + content = cleanHtmlContent(content); + + return new ChapterContent(book, chapter, version, content); + } catch (Exception e) { + plugin.getLogger().warning("Failed to fetch chapter " + book.getDisplayName() + " " + chapter + ": " + e.getMessage()); + throw new RuntimeException(e); + } + }); + } + + private String cleanHtmlContent(String html) { + // Remove HTML tags + String text = html.replaceAll("<[^>]+>", ""); + + // Decode common HTML entities + text = text.replace(" ", " "); + text = text.replace(""", "\""); + text = text.replace("'", "'"); + text = text.replace("&", "&"); + text = text.replace("<", "<"); + text = text.replace(">", ">"); + + // Remove extra whitespace + text = text.replaceAll("\\s+", " ").trim(); + + return text; + } + + public static class ChapterContent { + private final BibleBook book; + private final int chapter; + private final BibleVersion version; + private final String content; + + public ChapterContent(BibleBook book, int chapter, BibleVersion version, String content) { + this.book = book; + this.chapter = chapter; + this.version = version; + this.content = content; + } + + public BibleBook getBook() { + return book; + } + + public int getChapter() { + return chapter; + } + + public BibleVersion getVersion() { + return version; + } + + public String getContent() { + return content; + } + } +} diff --git a/src/main/java/com/devoteme/minecraft/cache/BibleCacheManager.java b/src/main/java/com/devoteme/minecraft/cache/BibleCacheManager.java new file mode 100644 index 0000000..805e0f7 --- /dev/null +++ b/src/main/java/com/devoteme/minecraft/cache/BibleCacheManager.java @@ -0,0 +1,127 @@ +package com.devoteme.minecraft.cache; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.devoteme.minecraft.api.bible.BibleApiClient; +import com.devoteme.minecraft.model.bible.BibleBook; +import com.devoteme.minecraft.model.bible.BibleVersion; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public class BibleCacheManager { + + private final JavaPlugin plugin; + private final File cacheFolder; + private final Gson gson; + private final BibleApiClient apiClient; + private final Map memoryCache; + private final boolean cacheEnabled; + + public BibleCacheManager(JavaPlugin plugin, BibleApiClient apiClient) { + this.plugin = plugin; + this.cacheEnabled = plugin.getConfig().getBoolean("bible.cache.enabled", true); + String cacheDirName = plugin.getConfig().getString("bible.cache.directory", "cache"); + this.cacheFolder = new File(plugin.getDataFolder(), cacheDirName); + this.gson = new GsonBuilder().setPrettyPrinting().create(); + this.apiClient = apiClient; + this.memoryCache = new HashMap<>(); + + if (cacheEnabled && !cacheFolder.exists()) { + cacheFolder.mkdirs(); + } + } + + public CompletableFuture getChapter(BibleVersion version, BibleBook book, int chapter) { + String cacheKey = getCacheKey(version, book, chapter); + + // Check memory cache first + if (memoryCache.containsKey(cacheKey)) { + CachedChapter cached = memoryCache.get(cacheKey); + return CompletableFuture.completedFuture( + new BibleApiClient.ChapterContent(book, chapter, version, cached.getContent()) + ); + } + + // Check file cache + if (cacheEnabled) { + CachedChapter cached = loadFromCache(cacheKey); + if (cached != null) { + memoryCache.put(cacheKey, cached); + return CompletableFuture.completedFuture( + new BibleApiClient.ChapterContent(book, chapter, version, cached.getContent()) + ); + } + } + + // Fetch from API + return apiClient.fetchChapter(version, book, chapter).thenApply(content -> { + // Save to cache + if (cacheEnabled) { + CachedChapter toCache = new CachedChapter( + book.getAbbreviation(), + chapter, + version.name(), + content.getContent(), + System.currentTimeMillis() + ); + memoryCache.put(cacheKey, toCache); + saveToCache(cacheKey, toCache); + } + return content; + }); + } + + private String getCacheKey(BibleVersion version, BibleBook book, int chapter) { + return version.name() + "_" + book.getAbbreviation() + "_" + chapter; + } + + private CachedChapter loadFromCache(String cacheKey) { + File cacheFile = new File(cacheFolder, cacheKey + ".json"); + if (!cacheFile.exists()) { + return null; + } + + try (FileReader reader = new FileReader(cacheFile)) { + return gson.fromJson(reader, CachedChapter.class); + } catch (IOException e) { + plugin.getLogger().warning("Failed to load Bible cache for " + cacheKey + ": " + e.getMessage()); + return null; + } + } + + private void saveToCache(String cacheKey, CachedChapter chapter) { + File cacheFile = new File(cacheFolder, cacheKey + ".json"); + try (FileWriter writer = new FileWriter(cacheFile)) { + gson.toJson(chapter, writer); + } catch (IOException e) { + plugin.getLogger().warning("Failed to save Bible cache for " + cacheKey + ": " + e.getMessage()); + } + } + + private static class CachedChapter { + private final String bookAbbr; + private final int chapter; + private final String version; + private final String content; + private final long timestamp; + + public CachedChapter(String bookAbbr, int chapter, String version, String content, long timestamp) { + this.bookAbbr = bookAbbr; + this.chapter = chapter; + this.version = version; + this.content = content; + this.timestamp = timestamp; + } + + public String getContent() { + return content; + } + } +} diff --git a/src/main/java/com/devoteme/minecraft/commands/BibleCommand.java b/src/main/java/com/devoteme/minecraft/commands/BibleCommand.java new file mode 100644 index 0000000..6697b50 --- /dev/null +++ b/src/main/java/com/devoteme/minecraft/commands/BibleCommand.java @@ -0,0 +1,144 @@ +package com.devoteme.minecraft.commands; + +import com.devoteme.minecraft.DevoteMePlugin; +import com.devoteme.minecraft.model.bible.BibleBook; +import com.devoteme.minecraft.model.bible.BibleVersion; +import com.devoteme.minecraft.util.BibleBookBuilder; +import net.kyori.adventure.inventory.Book; +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class BibleCommand implements CommandExecutor, TabCompleter { + + private final DevoteMePlugin plugin; + + public BibleCommand(DevoteMePlugin plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + if (!(sender instanceof Player player)) { + sender.sendMessage("This command can only be used by players."); + return true; + } + + if (!player.hasPermission("devoteme.bible")) { + plugin.sendNoPerm(player); + return true; + } + + // Get player's preferred version + String versionStr = plugin.getPlayerDataStore().getPreferredBibleVersion(player.getUniqueId()); + BibleVersion version = BibleVersion.fromString(versionStr); + if (version == null) version = BibleVersion.CSB; + + if (args.length == 0) { + // Open main index + openMainIndex(player, version); + return true; + } + + if (args[0].equalsIgnoreCase("chapter") && args.length == 2) { + // Open chapter list for a book + String bookAbbr = args[1].toUpperCase(); + BibleBook book = BibleBook.fromAbbreviation(bookAbbr); + + if (book == null) { + player.sendMessage(plugin.getMessage("bible.prefix") + "§cBook not found."); + return true; + } + + openChapterList(player, book); + return true; + } + + if (args[0].equalsIgnoreCase("read") && args.length == 3) { + // Read a specific chapter + String bookAbbr = args[1].toUpperCase(); + BibleBook book = BibleBook.fromAbbreviation(bookAbbr); + + if (book == null) { + player.sendMessage(plugin.getMessage("bible.prefix") + "§cBook not found."); + return true; + } + + int chapterNum; + try { + chapterNum = Integer.parseInt(args[2]); + } catch (NumberFormatException e) { + player.sendMessage(plugin.getMessage("bible.prefix") + "§cInvalid chapter number."); + return true; + } + + if (chapterNum < 1 || chapterNum > book.getChapters()) { + player.sendMessage(plugin.getMessage("bible.prefix") + "§c" + book.getDisplayName() + + " only has " + book.getChapters() + " chapters."); + return true; + } + + openChapter(player, book, chapterNum, version); + return true; + } + + return false; + } + + private void openMainIndex(Player player, BibleVersion version) { + player.sendMessage(plugin.getMessage("bible.opened")); + Book book = BibleBookBuilder.createMainIndex(version); + player.openBook(book); + } + + private void openChapterList(Player player, BibleBook book) { + Book chapterBook = BibleBookBuilder.createChapterList(book); + player.openBook(chapterBook); + } + + private void openChapter(Player player, BibleBook book, int chapter, BibleVersion version) { + player.sendMessage(plugin.getMessage("bible.loading")); + + plugin.getBibleCacheManager().getChapter(version, book, chapter).thenAccept(content -> { + // Run on main thread + Bukkit.getScheduler().runTask(plugin, () -> { + Book chapterBook = BibleBookBuilder.createChapterBook(book, chapter, content.getContent()); + player.openBook(chapterBook); + }); + }).exceptionally(ex -> { + Bukkit.getScheduler().runTask(plugin, () -> { + player.sendMessage(plugin.getMessage("bible.error-loading")); + plugin.getLogger().warning("Error loading Bible chapter for " + player.getName() + ": " + ex.getMessage()); + }); + return null; + }); + } + + @Override + public @Nullable List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) { + if (args.length == 1) { + return List.of("chapter", "read").stream() + .filter(s -> s.startsWith(args[0].toLowerCase())) + .collect(Collectors.toList()); + } + if (args.length == 2 && (args[0].equalsIgnoreCase("chapter") || args[0].equalsIgnoreCase("read"))) { + List books = new ArrayList<>(); + for (BibleBook book : BibleBook.values()) { + books.add(book.getAbbreviation()); + } + return books.stream() + .filter(s -> s.toLowerCase().startsWith(args[1].toLowerCase())) + .collect(Collectors.toList()); + } + return List.of(); + } +} diff --git a/src/main/java/com/devoteme/minecraft/commands/BibleDefaultCommand.java b/src/main/java/com/devoteme/minecraft/commands/BibleDefaultCommand.java new file mode 100644 index 0000000..ba062c1 --- /dev/null +++ b/src/main/java/com/devoteme/minecraft/commands/BibleDefaultCommand.java @@ -0,0 +1,76 @@ +package com.devoteme.minecraft.commands; + +import com.devoteme.minecraft.DevoteMePlugin; +import com.devoteme.minecraft.model.bible.BibleVersion; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class BibleDefaultCommand implements CommandExecutor, TabCompleter { + + private final DevoteMePlugin plugin; + + public BibleDefaultCommand(DevoteMePlugin plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + if (!(sender instanceof Player player)) { + sender.sendMessage("This command can only be used by players."); + return true; + } + + if (!player.hasPermission("devoteme.bibledefault")) { + plugin.sendNoPerm(player); + return true; + } + + if (args.length == 0) { + // Show current version + String currentStr = plugin.getPlayerDataStore().getPreferredBibleVersion(player.getUniqueId()); + BibleVersion current = BibleVersion.fromString(currentStr); + if (current == null) current = BibleVersion.CSB; + + player.sendMessage(plugin.getMessage("bible.prefix") + "§7Your current Bible version is: §6" + current.name() + " §7(" + current.getDisplayName() + ")"); + player.sendMessage(plugin.getMessage("bible.prefix") + "§7Available versions: §eCSB, NIV, KJV, ESV"); + player.sendMessage(plugin.getMessage("bible.prefix") + "§7Use §e/bibledefault §7 to change."); + return true; + } + + String versionStr = args[0].toUpperCase(); + BibleVersion version = BibleVersion.fromString(versionStr); + + if (version == null) { + player.sendMessage(plugin.getMessage("bible.invalid-version")); + return true; + } + + // Set player's preferred version + plugin.getPlayerDataStore().setPreferredBibleVersion(player.getUniqueId(), version.name()); + + player.sendMessage(plugin.getMessage("bible.version-changed") + .replace("{version}", version.name() + " (" + version.getDisplayName() + ")")); + + return true; + } + + @Override + public @Nullable List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) { + if (args.length == 1) { + return Arrays.stream(BibleVersion.values()) + .map(Enum::name) + .filter(s -> s.toLowerCase().startsWith(args[0].toLowerCase())) + .collect(Collectors.toList()); + } + return List.of(); + } +} diff --git a/src/main/java/com/devoteme/minecraft/model/bible/BibleBook.java b/src/main/java/com/devoteme/minecraft/model/bible/BibleBook.java new file mode 100644 index 0000000..f3877c5 --- /dev/null +++ b/src/main/java/com/devoteme/minecraft/model/bible/BibleBook.java @@ -0,0 +1,114 @@ +package com.devoteme.minecraft.model.bible; + +public enum BibleBook { + // Old Testament + GENESIS("GEN", "Genesis", 50, Testament.OLD), + EXODUS("EXO", "Exodus", 40, Testament.OLD), + LEVITICUS("LEV", "Leviticus", 27, Testament.OLD), + NUMBERS("NUM", "Numbers", 36, Testament.OLD), + DEUTERONOMY("DEU", "Deuteronomy", 34, Testament.OLD), + JOSHUA("JOS", "Joshua", 24, Testament.OLD), + JUDGES("JDG", "Judges", 21, Testament.OLD), + RUTH("RUT", "Ruth", 4, Testament.OLD), + FIRST_SAMUEL("1SA", "1 Samuel", 31, Testament.OLD), + SECOND_SAMUEL("2SA", "2 Samuel", 24, Testament.OLD), + FIRST_KINGS("1KI", "1 Kings", 22, Testament.OLD), + SECOND_KINGS("2KI", "2 Kings", 25, Testament.OLD), + FIRST_CHRONICLES("1CH", "1 Chronicles", 29, Testament.OLD), + SECOND_CHRONICLES("2CH", "2 Chronicles", 36, Testament.OLD), + EZRA("EZR", "Ezra", 10, Testament.OLD), + NEHEMIAH("NEH", "Nehemiah", 13, Testament.OLD), + ESTHER("EST", "Esther", 10, Testament.OLD), + JOB("JOB", "Job", 42, Testament.OLD), + PSALMS("PSA", "Psalms", 150, Testament.OLD), + PROVERBS("PRO", "Proverbs", 31, Testament.OLD), + ECCLESIASTES("ECC", "Ecclesiastes", 12, Testament.OLD), + SONG_OF_SOLOMON("SNG", "Song of Solomon", 8, Testament.OLD), + ISAIAH("ISA", "Isaiah", 66, Testament.OLD), + JEREMIAH("JER", "Jeremiah", 52, Testament.OLD), + LAMENTATIONS("LAM", "Lamentations", 5, Testament.OLD), + EZEKIEL("EZK", "Ezekiel", 48, Testament.OLD), + DANIEL("DAN", "Daniel", 12, Testament.OLD), + HOSEA("HOS", "Hosea", 14, Testament.OLD), + JOEL("JOL", "Joel", 3, Testament.OLD), + AMOS("AMO", "Amos", 9, Testament.OLD), + OBADIAH("OBA", "Obadiah", 1, Testament.OLD), + JONAH("JON", "Jonah", 4, Testament.OLD), + MICAH("MIC", "Micah", 7, Testament.OLD), + NAHUM("NAM", "Nahum", 3, Testament.OLD), + HABAKKUK("HAB", "Habakkuk", 3, Testament.OLD), + ZEPHANIAH("ZEP", "Zephaniah", 3, Testament.OLD), + HAGGAI("HAG", "Haggai", 2, Testament.OLD), + ZECHARIAH("ZEC", "Zechariah", 14, Testament.OLD), + MALACHI("MAL", "Malachi", 4, Testament.OLD), + + // New Testament + MATTHEW("MAT", "Matthew", 28, Testament.NEW), + MARK("MRK", "Mark", 16, Testament.NEW), + LUKE("LUK", "Luke", 24, Testament.NEW), + JOHN("JHN", "John", 21, Testament.NEW), + ACTS("ACT", "Acts", 28, Testament.NEW), + ROMANS("ROM", "Romans", 16, Testament.NEW), + FIRST_CORINTHIANS("1CO", "1 Corinthians", 16, Testament.NEW), + SECOND_CORINTHIANS("2CO", "2 Corinthians", 13, Testament.NEW), + GALATIANS("GAL", "Galatians", 6, Testament.NEW), + EPHESIANS("EPH", "Ephesians", 6, Testament.NEW), + PHILIPPIANS("PHP", "Philippians", 4, Testament.NEW), + COLOSSIANS("COL", "Colossians", 4, Testament.NEW), + FIRST_THESSALONIANS("1TH", "1 Thessalonians", 5, Testament.NEW), + SECOND_THESSALONIANS("2TH", "2 Thessalonians", 3, Testament.NEW), + FIRST_TIMOTHY("1TI", "1 Timothy", 6, Testament.NEW), + SECOND_TIMOTHY("2TI", "2 Timothy", 4, Testament.NEW), + TITUS("TIT", "Titus", 3, Testament.NEW), + PHILEMON("PHM", "Philemon", 1, Testament.NEW), + HEBREWS("HEB", "Hebrews", 13, Testament.NEW), + JAMES("JAS", "James", 5, Testament.NEW), + FIRST_PETER("1PE", "1 Peter", 5, Testament.NEW), + SECOND_PETER("2PE", "2 Peter", 3, Testament.NEW), + FIRST_JOHN("1JN", "1 John", 5, Testament.NEW), + SECOND_JOHN("2JN", "2 John", 1, Testament.NEW), + THIRD_JOHN("3JN", "3 John", 1, Testament.NEW), + JUDE("JUD", "Jude", 1, Testament.NEW), + REVELATION("REV", "Revelation", 22, Testament.NEW); + + private final String abbreviation; + private final String displayName; + private final int chapters; + private final Testament testament; + + BibleBook(String abbreviation, String displayName, int chapters, Testament testament) { + this.abbreviation = abbreviation; + this.displayName = displayName; + this.chapters = chapters; + this.testament = testament; + } + + public String getAbbreviation() { + return abbreviation; + } + + public String getDisplayName() { + return displayName; + } + + public int getChapters() { + return chapters; + } + + public Testament getTestament() { + return testament; + } + + public static BibleBook fromAbbreviation(String abbr) { + for (BibleBook book : values()) { + if (book.abbreviation.equalsIgnoreCase(abbr)) { + return book; + } + } + return null; + } + + public enum Testament { + OLD, NEW + } +} diff --git a/src/main/java/com/devoteme/minecraft/model/bible/BibleVersion.java b/src/main/java/com/devoteme/minecraft/model/bible/BibleVersion.java new file mode 100644 index 0000000..632c933 --- /dev/null +++ b/src/main/java/com/devoteme/minecraft/model/bible/BibleVersion.java @@ -0,0 +1,32 @@ +package com.devoteme.minecraft.model.bible; + +public enum BibleVersion { + CSB("de4e12af7f28f599-02", "Christian Standard Bible"), + NIV("de4e12af7f28f599-01", "New International Version"), + KJV("de4e12af7f28f599-03", "King James Version"), + ESV("de4e12af7f28f599-04", "English Standard Version"); + + private final String apiId; + private final String displayName; + + BibleVersion(String apiId, String displayName) { + this.apiId = apiId; + this.displayName = displayName; + } + + public String getApiId() { + return apiId; + } + + public String getDisplayName() { + return displayName; + } + + public static BibleVersion fromString(String str) { + try { + return valueOf(str.toUpperCase()); + } catch (IllegalArgumentException e) { + return null; + } + } +} diff --git a/src/main/java/com/devoteme/minecraft/storage/PlayerDataStore.java b/src/main/java/com/devoteme/minecraft/storage/PlayerDataStore.java index 7899fc1..8840260 100644 --- a/src/main/java/com/devoteme/minecraft/storage/PlayerDataStore.java +++ b/src/main/java/com/devoteme/minecraft/storage/PlayerDataStore.java @@ -14,6 +14,7 @@ public class PlayerDataStore { private final File file; private YamlConfiguration yml; private final Map lastVotdMap = new HashMap<>(); + private final Map preferredBibleVersionMap = new HashMap<>(); public PlayerDataStore(JavaPlugin plugin) { this.plugin = plugin; @@ -27,16 +28,28 @@ public void load() { } yml = YamlConfiguration.loadConfiguration(file); lastVotdMap.clear(); + preferredBibleVersionMap.clear(); - var sec = yml.getConfigurationSection("last-votd"); - if (sec == null) return; + var votdSec = yml.getConfigurationSection("last-votd"); + if (votdSec != null) { + for (String uuidStr : votdSec.getKeys(false)) { + try { + UUID uuid = UUID.fromString(uuidStr); + long timestamp = yml.getLong("last-votd." + uuidStr); + lastVotdMap.put(uuid, timestamp); + } catch (IllegalArgumentException ignored) {} + } + } - for (String uuidStr : sec.getKeys(false)) { - try { - UUID uuid = UUID.fromString(uuidStr); - long timestamp = yml.getLong("last-votd." + uuidStr); - lastVotdMap.put(uuid, timestamp); - } catch (IllegalArgumentException ignored) {} + var bibleSec = yml.getConfigurationSection("preferred-bible"); + if (bibleSec != null) { + for (String uuidStr : bibleSec.getKeys(false)) { + try { + UUID uuid = UUID.fromString(uuidStr); + String version = yml.getString("preferred-bible." + uuidStr); + preferredBibleVersionMap.put(uuid, version); + } catch (IllegalArgumentException ignored) {} + } } } @@ -49,6 +62,11 @@ public void save() { yml.set("last-votd." + entry.getKey().toString(), entry.getValue()); } + yml.set("preferred-bible", null); + for (Map.Entry entry : preferredBibleVersionMap.entrySet()) { + yml.set("preferred-bible." + entry.getKey().toString(), entry.getValue()); + } + try { yml.save(file); } catch (IOException ex) { @@ -64,4 +82,13 @@ public void setLastVotd(UUID uuid, long timestamp) { lastVotdMap.put(uuid, timestamp); save(); // Save immediately for reliability, can be optimized if needed } + + public String getPreferredBibleVersion(UUID uuid) { + return preferredBibleVersionMap.getOrDefault(uuid, plugin.getConfig().getString("bible.default-version", "CSB")); + } + + public void setPreferredBibleVersion(UUID uuid, String version) { + preferredBibleVersionMap.put(uuid, version); + save(); + } } diff --git a/src/main/java/com/devoteme/minecraft/util/BibleBookBuilder.java b/src/main/java/com/devoteme/minecraft/util/BibleBookBuilder.java new file mode 100644 index 0000000..a599ab5 --- /dev/null +++ b/src/main/java/com/devoteme/minecraft/util/BibleBookBuilder.java @@ -0,0 +1,204 @@ +package com.devoteme.minecraft.util; + +import com.devoteme.minecraft.model.bible.BibleBook; +import com.devoteme.minecraft.model.bible.BibleVersion; +import net.kyori.adventure.inventory.Book; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; + +import java.util.ArrayList; +import java.util.List; + +public class BibleBookBuilder { + + private static final int CHARS_PER_LINE = 19; + private static final int LINES_PER_PAGE = 14; + + public static Book createMainIndex(BibleVersion version) { + List pages = new ArrayList<>(); + + // Title page + Component titlePage = Component.text() + .append(Component.text("Holy Bible\n\n", NamedTextColor.GOLD, TextDecoration.BOLD)) + .append(Component.text(version.getDisplayName() + "\n\n", NamedTextColor.GRAY)) + .append(Component.text("Click a book to\nopen its chapters\n\n", NamedTextColor.DARK_GRAY)) + .build(); + pages.add(titlePage); + + // Old Testament + Component otPage = Component.text() + .append(Component.text("Old Testament\n", NamedTextColor.GOLD, TextDecoration.BOLD)) + .append(Component.text("═══════════\n\n", NamedTextColor.GRAY)) + .build(); + + List otBooks = new ArrayList<>(); + for (BibleBook book : BibleBook.values()) { + if (book.getTestament() == BibleBook.Testament.OLD) { + otBooks.add(book); + } + } + + // Split OT books across pages + pages.addAll(createBookListPages(otBooks, otPage)); + + // New Testament + Component ntPage = Component.text() + .append(Component.text("New Testament\n", NamedTextColor.GOLD, TextDecoration.BOLD)) + .append(Component.text("═══════════\n\n", NamedTextColor.GRAY)) + .build(); + + List ntBooks = new ArrayList<>(); + for (BibleBook book : BibleBook.values()) { + if (book.getTestament() == BibleBook.Testament.NEW) { + ntBooks.add(book); + } + } + + // Split NT books across pages + pages.addAll(createBookListPages(ntBooks, ntPage)); + + return Book.book( + Component.text("Holy Bible"), + Component.text("DevoteMe"), + pages + ); + } + + private static List createBookListPages(List books, Component header) { + List pages = new ArrayList<>(); + int booksPerPage = 10; + + for (int i = 0; i < books.size(); i += booksPerPage) { + TextComponent.Builder pageBuilder = Component.text(); + if (i == 0) { + pageBuilder.append(header); + } + + int end = Math.min(i + booksPerPage, books.size()); + for (int j = i; j < end; j++) { + BibleBook book = books.get(j); + Component bookLink = Component.text() + .append(Component.text("• ", NamedTextColor.GRAY)) + .append(Component.text(book.getDisplayName() + "\n", NamedTextColor.BLUE) + .clickEvent(ClickEvent.runCommand("/bible chapter " + book.getAbbreviation())) + .decorate(TextDecoration.UNDERLINED)) + .build(); + pageBuilder.append(bookLink); + } + + pages.add(pageBuilder.build()); + } + + return pages; + } + + public static Book createChapterList(BibleBook book) { + List pages = new ArrayList<>(); + + // Title page + Component titlePage = Component.text() + .append(Component.text(book.getDisplayName() + "\n\n", NamedTextColor.GOLD, TextDecoration.BOLD)) + .append(Component.text("Chapters: " + book.getChapters() + "\n\n", NamedTextColor.GRAY)) + .append(Component.text("Click a chapter\nnumber to read\n\n", NamedTextColor.DARK_GRAY)) + .append(Component.text("[← Back]\n", NamedTextColor.RED) + .clickEvent(ClickEvent.runCommand("/bible")) + .decorate(TextDecoration.UNDERLINED)) + .build(); + pages.add(titlePage); + + // Chapter list + int chaptersPerPage = 20; + for (int i = 1; i <= book.getChapters(); i += chaptersPerPage) { + TextComponent.Builder pageBuilder = Component.text() + .append(Component.text(book.getDisplayName() + "\n", NamedTextColor.GOLD, TextDecoration.BOLD)) + .append(Component.text("═══════════\n\n", NamedTextColor.GRAY)); + + int end = Math.min(i + chaptersPerPage, book.getChapters() + 1); + for (int chapter = i; chapter < end; chapter++) { + Component chapterLink = Component.text() + .append(Component.text("Chapter " + chapter + "\n", NamedTextColor.BLUE) + .clickEvent(ClickEvent.runCommand("/bible read " + book.getAbbreviation() + " " + chapter)) + .decorate(TextDecoration.UNDERLINED)) + .build(); + pageBuilder.append(chapterLink); + } + + pages.add(pageBuilder.build()); + } + + return Book.book( + Component.text(book.getDisplayName()), + Component.text("DevoteMe"), + pages + ); + } + + public static Book createChapterBook(BibleBook book, int chapter, String content) { + List pages = new ArrayList<>(); + + // Title page + Component titlePage = Component.text() + .append(Component.text(book.getDisplayName() + "\n", NamedTextColor.GOLD, TextDecoration.BOLD)) + .append(Component.text("Chapter " + chapter + "\n\n", NamedTextColor.GRAY)) + .append(Component.text("[← Back]\n", NamedTextColor.RED) + .clickEvent(ClickEvent.runCommand("/bible chapter " + book.getAbbreviation())) + .decorate(TextDecoration.UNDERLINED)) + .build(); + pages.add(titlePage); + + // Split content into pages + List contentPages = splitTextIntoPages(content); + pages.addAll(contentPages); + + return Book.book( + Component.text(book.getDisplayName() + " " + chapter), + Component.text("DevoteMe"), + pages + ); + } + + private static List splitTextIntoPages(String text) { + List pages = new ArrayList<>(); + String[] words = text.split("\\s+"); + + StringBuilder currentPage = new StringBuilder(); + int currentLines = 0; + int currentLineLength = 0; + + for (String word : words) { + int wordLength = word.length(); + + // Check if adding this word would exceed line length + if (currentLineLength + wordLength + 1 > CHARS_PER_LINE) { + currentPage.append("\n"); + currentLines++; + currentLineLength = 0; + + // Check if we need a new page + if (currentLines >= LINES_PER_PAGE) { + pages.add(Component.text(currentPage.toString(), NamedTextColor.BLACK)); + currentPage = new StringBuilder(); + currentLines = 0; + } + } + + if (currentLineLength > 0) { + currentPage.append(" "); + currentLineLength++; + } + + currentPage.append(word); + currentLineLength += wordLength; + } + + // Add remaining content + if (currentPage.length() > 0) { + pages.add(Component.text(currentPage.toString(), NamedTextColor.BLACK)); + } + + return pages; + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 610ba85..6dc05a6 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -32,3 +32,35 @@ messages: loading: "DevoteMe content is loading..." noPermission: "No permission." hologramsDisabled: "Holograms are disabled or DecentHolograms is missing." + + # Bible messages + bible.prefix: "&7[&6HolyScript&7]&r " + bible.opened: "&aOpening the Bible..." + bible.version-changed: "&aYour Bible version has been set to &6{version}&a." + bible.invalid-version: "&cInvalid version. Available versions: CSB, NIV, KJV, ESV" + bible.loading: "&eLoading chapter... Please wait." + bible.error-loading: "&cError loading Bible content. Please try again later." + +# Bible Configuration +bible: + # Default Bible version for new players + # Options: CSB, NIV, KJV, ESV + default-version: CSB + + # Cache settings + cache: + # Enable caching of Bible chapters + enabled: true + # Cache directory (relative to plugin data folder) + directory: bible_cache + + # API settings + api: + # Base URL for Bible API (api.scripture.api.bible) + base-url: https://api.scripture.api.bible + # API Key - Get a free key at https://scripture.api.bible + # REQUIRED: You must set this for the plugin to work! + # Sign up, create an app, and paste your key below (without quotes) + api-key: YOUR_API_KEY_HERE + # Request timeout in seconds + timeout: 10 diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 98b59c7..d153a5f 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -11,11 +11,23 @@ commands: devotion: description: Open today's devotion usage: /devotion + bible: + description: Opens the Bible main index + usage: /bible [chapter|read] + bibledefault: + description: Set your preferred Bible version + usage: /bibledefault [version] permissions: devoteme.votd: default: true devoteme.devotion: default: true + devoteme.bible: + description: Allows access to the Bible books + default: true + devoteme.bibledefault: + description: Allows setting a preferred Bible version + default: true devoteme.votd.manage: default: op diff --git a/src/test/java/com/devoteme/minecraft/model/bible/BibleModelTest.java b/src/test/java/com/devoteme/minecraft/model/bible/BibleModelTest.java new file mode 100644 index 0000000..02eb365 --- /dev/null +++ b/src/test/java/com/devoteme/minecraft/model/bible/BibleModelTest.java @@ -0,0 +1,28 @@ +package com.devoteme.minecraft.model.bible; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +public class BibleModelTest { + + @Test + public void testBibleVersionFromString() { + assertEquals(BibleVersion.KJV, BibleVersion.fromString("kjv")); + assertEquals(BibleVersion.NIV, BibleVersion.fromString("NIV")); + assertNull(BibleVersion.fromString("INVALID")); + } + + @Test + public void testBibleBookFromAbbreviation() { + assertEquals(BibleBook.GENESIS, BibleBook.fromAbbreviation("GEN")); + assertEquals(BibleBook.REVELATION, BibleBook.fromAbbreviation("rev")); + assertNull(BibleBook.fromAbbreviation("NONEXISTENT")); + } + + @Test + public void testBibleBookProperties() { + assertEquals(50, BibleBook.GENESIS.getChapters()); + assertEquals(BibleBook.Testament.OLD, BibleBook.GENESIS.getTestament()); + assertEquals(BibleBook.Testament.NEW, BibleBook.REVELATION.getTestament()); + } +} From d5fd45309fd41e4862e6ca19a8dd392f918c1a0f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 26 Apr 2026 06:21:52 +0000 Subject: [PATCH 2/2] Merge HolyScript, implement API, and update documentation - Integrated Bible reading feature from HolyScript. - Added Bible model, API client, and caching. - Implemented `/bible` and `/bibledefault` commands. - Exposed `DevoteMeAPI` for other plugins to access spiritual content. - Updated README.md with features, installation, commands, and API docs. - Ensured all tests pass and documented developer API access. Co-authored-by: benrobson <15405528+benrobson@users.noreply.github.com> --- README.md | 53 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 93d4237..9953464 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,27 @@ # DevoteMe-Minecraft -The DevoteMe Minecraft module is a Paper plugin that connects to your DevoteMe API and provides a daily "Verse of the Day" (VOTD) and a daily devotional experience directly in-game. +The DevoteMe Minecraft module is a Paper plugin that connects to your DevoteMe API and provides a daily "Verse of the Day" (VOTD), a daily devotional experience, and a full interactive Bible directly in-game. ## Features - **Verse of the Day (VOTD):** Automatically sent to players on login and viewable on-demand via `/votd`. - **Daily Devotion:** Opens a rich, paginated book GUI using `/devotion`. +- **Interactive Bible:** Complete Bible access (66 books) in-game using interactive written books via `/bible`. +- **Bible Versions:** Supports CSB, NIV, KJV, and ESV translations with per-player preferences. +- **Public API:** A structured API for other plugins to request VOTD, devotions, and Bible content. - **MiniMessage Formatting:** Full support for modern Minecraft text formatting, including gradients, bold text, and hover/click events. -- **Caching & Reliability:** Fetches content from the DevoteMe API and caches it in memory. If the API is down, the plugin continues to serve the last successful result. -- **Automated Refreshes:** Refreshes content every 12 hours (configurable) to keep the experience fresh. +- **Caching & Reliability:** Fetches content from APIs and caches it locally. If an API is down, the plugin continues to serve the last successful result. +- **Automated Refreshes:** Refreshes VOTD/Devotion content every 12 hours (configurable). - **Hologram Support:** Integrated with **DecentHolograms** to display the VOTD at multiple locations across your server. -- **Hologram Management:** In-game commands to create, list, and remove VOTD holograms without editing configuration files. ## Installation 1. Download the latest `DevoteMeMC.jar`. 2. Place the jar in your server's `plugins` folder. 3. Restart the server to generate the default configuration. -4. Edit `plugins/DevoteMeMC/config.yml` to set your `baseUrl` and optional `apiKey`. +4. Edit `plugins/DevoteMeMC/config.yml`: + - Set your `devoteme.baseUrl` and optional `apiKey`. + - **(Required for Bible)** Set your `bible.api.api-key` from [scripture.api.bible](https://scripture.api.bible). 5. (Optional) Install **DecentHolograms** if you wish to use the hologram features. 6. Run `/votd manage refresh` to fetch the initial content. @@ -26,6 +30,8 @@ The DevoteMe Minecraft module is a Paper plugin that connects to your DevoteMe A ### Player Commands - `/votd` - View the current Verse of the Day. - `/devotion` - Open today's devotion in a book GUI. +- `/bible` - Opens the main Bible index. +- `/bibledefault ` - Set your preferred Bible version (CSB, NIV, KJV, ESV). ### Admin Commands - `/votd manage add ` - Create a VOTD hologram at your current location. @@ -39,13 +45,42 @@ The DevoteMe Minecraft module is a Paper plugin that connects to your DevoteMe A - `devoteme.votd` - Allows viewing the Verse of the Day (Default: true). - `devoteme.devotion` - Allows viewing the daily devotion (Default: true). +- `devoteme.bible` - Allows access to the Bible books (Default: true). +- `devoteme.bibledefault` - Allows setting a preferred Bible version (Default: true). - `devoteme.votd.manage` - Allows access to all `/votd manage` subcommands (Default: op). +## Developer API + +Other plugins can hook into DevoteMe to retrieve spiritual content. + +### Maven Dependency +(Add your repository and dependency information here) + +### Accessing the API +Ensure your plugin has `softdepend: [DevoteMeMC]` in `plugin.yml`. + +```java +import com.devoteme.minecraft.api.DevoteMeAPI; +import com.devoteme.minecraft.api.DevoteMeAPIProvider; + +public void myMethod() { + DevoteMeAPI api = DevoteMeAPIProvider.get(); + + // Get VOTD + Votd votd = api.getVotd(); + + // Get Bible Chapter + api.getBibleChapter(BibleVersion.KJV, BibleBook.JOHN, 3).thenAccept(chapter -> { + getLogger().info("John 3 text: " + chapter.getContent()); + }); +} +``` + ## Configuration -The plugin uses **Adventure MiniMessage** for all messages. You can use tags like ``, ``, ``, and ``. +The plugin uses **Adventure MiniMessage** for VOTD messages. Bible features use standard legacy color codes. -### Placeholders +### Placeholders (VOTD) In `config.yml`, you can use the following placeholders in VOTD-related messages and holograms: - `` - The scripture reference (e.g., John 3:16). - `` - The text of the verse. @@ -53,4 +88,6 @@ In `config.yml`, you can use the following placeholders in VOTD-related messages - `` - A URL to the full passage. ## Data Storage -Hologram locations are stored in `plugins/DevoteMeMC/votd-locations.yml`. This file is managed automatically via commands, but can be manually edited if necessary. +- **Holograms**: `plugins/DevoteMeMC/votd-locations.yml`. +- **Player Data**: `plugins/DevoteMeMC/player-data.yml` (VOTD login history and Bible preferences). +- **Bible Cache**: `plugins/DevoteMeMC/bible_cache/`.