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/`.