Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
target/
*.jar
.maven-docker-cache/
56 changes: 55 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,56 @@
# DevoteMe-Minecraft
The DevoteMe Minecraft module for the DevoteMe product suite.

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.

## 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`.
- **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.
- **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`.
5. (Optional) Install **DecentHolograms** if you wish to use the hologram features.
6. Run `/votd manage refresh` to fetch the initial content.

## Commands

### Player Commands
- `/votd` - View the current Verse of the Day.
- `/devotion` - Open today's devotion in a book GUI.

### Admin Commands
- `/votd manage add <name>` - Create a VOTD hologram at your current location.
- `/votd manage remove <name>` - Remove a specific VOTD hologram.
- `/votd manage removeNearest [radius]` - Remove the closest VOTD hologram within a radius (default 5 blocks).
- `/votd manage list` - List all saved VOTD hologram locations.
- `/votd manage reset` - Delete all VOTD holograms and clear saved locations.
- `/votd manage refresh` - Manually force a refresh of the VOTD and Devotion from the API.

## Permissions

- `devoteme.votd` - Allows viewing the Verse of the Day (Default: true).
- `devoteme.devotion` - Allows viewing the daily devotion (Default: true).
- `devoteme.votd.manage` - Allows access to all `/votd manage` subcommands (Default: op).

## Configuration

The plugin uses **Adventure MiniMessage** for all messages. You can use tags like `<gold>`, `<bold>`, `<hover:show_text:'Tooltip'>`, and `<click:open_url:'https://...'>`.

### Placeholders
In `config.yml`, you can use the following placeholders in VOTD-related messages and holograms:
- `<reference>` - The scripture reference (e.g., John 3:16).
- `<verse>` - The text of the verse.
- `<date>` - The display date for the content.
- `<link>` - 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.
57 changes: 57 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.devoteme</groupId>
<artifactId>DevoteMeMC</artifactId>
<version>1.0.0</version>
<name>DevoteMeMC</name>

<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<repositories>
<repository>
<id>papermc</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
</repositories>

<dependencies>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>1.20.6-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.11.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
118 changes: 118 additions & 0 deletions src/main/java/com/devoteme/minecraft/DevoteMePlugin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package com.devoteme.minecraft;

import com.devoteme.minecraft.api.DevoteMeApiClient;
import com.devoteme.minecraft.api.DevoteMeParser;
import com.devoteme.minecraft.cache.ContentCache;
import com.devoteme.minecraft.commands.DevotionCommand;
import com.devoteme.minecraft.commands.VotdCommand;
import com.devoteme.minecraft.holograms.DecentHologramsService;
import com.devoteme.minecraft.holograms.HologramService;
import com.devoteme.minecraft.listeners.LoginListener;
import com.devoteme.minecraft.model.Votd;
import com.devoteme.minecraft.storage.VotdLocationStore;
import com.devoteme.minecraft.util.Text;
import org.bukkit.Bukkit;
import org.bukkit.command.PluginCommand;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;

import java.util.concurrent.CompletableFuture;

public class DevoteMePlugin extends JavaPlugin {

private DevoteMeApiClient api;
private ContentCache cache;
private VotdLocationStore locationStore;
private HologramService holograms;

@Override
public void onEnable() {
saveDefaultConfig();

this.cache = new ContentCache();
this.api = new DevoteMeApiClient(
getConfig().getString("devoteme.baseUrl", ""),
getConfig().getString("devoteme.apiKey", "")
);
this.locationStore = new VotdLocationStore(this);

// Holograms (soft)
this.holograms = new DecentHologramsService(this, cache, locationStore);

// listeners
Bukkit.getPluginManager().registerEvents(new LoginListener(this), this);

// commands
PluginCommand votd = getCommand("votd");
if (votd != null) {
VotdCommand cmd = new VotdCommand(this, cache, locationStore, holograms);
votd.setExecutor(cmd);
votd.setTabCompleter(cmd);
}

PluginCommand devotion = getCommand("devotion");
if (devotion != null) {
DevotionCommand cmd = new DevotionCommand(this, cache);
devotion.setExecutor(cmd);
}

// Load stored hologram locations
locationStore.load();

// Initial refresh now
refreshAllAsync().whenComplete((v, ex) -> {
if (ex != null) getLogger().warning("Initial refresh failed: " + ex.getMessage());
Bukkit.getScheduler().runTask(this, () -> holograms.updateAllFromCache());
});

// Schedule repeating refresh every refreshHours
long hours = getConfig().getLong("devoteme.refreshHours", 12);
if (hours < 1) hours = 12;
long ticks = hours * 60L * 60L * 20L;

Bukkit.getScheduler().runTaskTimerAsynchronously(this, () -> {
refreshAllAsync().whenComplete((v, ex) -> {
if (ex != null) getLogger().warning("Scheduled refresh failed: " + ex.getMessage());
Bukkit.getScheduler().runTask(this, () -> holograms.updateAllFromCache());
});
}, ticks, ticks);
}

public CompletableFuture<Void> refreshAllAsync() {
CompletableFuture<Void> a = refreshVotdAsync();
CompletableFuture<Void> b = refreshDevotionAsync();
return CompletableFuture.allOf(a, b);
}

public CompletableFuture<Void> refreshVotdAsync() {
return api.getJson("/votd/get")
.thenApply(DevoteMeParser::parseVotd)
.thenAccept(cache::setVotd);
}

public CompletableFuture<Void> refreshDevotionAsync() {
return api.getJson("/devotion/get")
.thenApply(DevoteMeParser::parseDevotion)
.thenAccept(cache::setDevotion);
}

public void sendVotdTo(Player p) {
Votd v = cache.getVotd();
if (v == null) {
p.sendMessage(Text.render(getConfig().getString("messages.loading", "<yellow>Loading...</yellow>")));
return;
}

for (String line : getConfig().getStringList("votd.chatFormat")) {
p.sendMessage(Text.render(line, v));
}
}

public void sendNoPerm(org.bukkit.command.CommandSender sender) {
sender.sendMessage(Text.render(getConfig().getString("messages.noPermission", "<red>No permission.</red>")));
}

public ContentCache getCache() { return cache; }
public VotdLocationStore getLocationStore() { return locationStore; }
public HologramService getHolograms() { return holograms; }
}
41 changes: 41 additions & 0 deletions src/main/java/com/devoteme/minecraft/api/DevoteMeApiClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.devoteme.minecraft.api;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.concurrent.CompletableFuture;

public class DevoteMeApiClient {
private final HttpClient client = HttpClient.newHttpClient();
private final String baseUrl;
private final String apiKeyOrNull;

public DevoteMeApiClient(String baseUrl, String apiKey) {
String b = baseUrl == null ? "" : baseUrl.trim();
this.baseUrl = b.endsWith("/") ? b.substring(0, b.length() - 1) : b;
this.apiKeyOrNull = (apiKey == null || apiKey.isBlank()) ? null : apiKey.trim();
}

public CompletableFuture<String> getJson(String path) {
if (baseUrl.isEmpty()) {
return CompletableFuture.failedFuture(new IllegalStateException("devoteme.baseUrl is empty"));
}

HttpRequest.Builder req = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + path))
.GET()
.header("Accept", "application/json");

if (apiKeyOrNull != null) {
req.header("x-access-token", apiKeyOrNull);
}

return client.sendAsync(req.build(), HttpResponse.BodyHandlers.ofString())
.thenApply(resp -> {
int sc = resp.statusCode();
if (sc < 200 || sc >= 300) throw new RuntimeException("HTTP " + sc + " for " + path);
return resp.body();
});
}
}
79 changes: 79 additions & 0 deletions src/main/java/com/devoteme/minecraft/api/DevoteMeParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.devoteme.minecraft.api;

import com.google.gson.*;
import com.devoteme.minecraft.model.Devotion;
import com.devoteme.minecraft.model.Votd;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

public class DevoteMeParser {
public static Votd parseVotd(String json) {
JsonElement el = JsonParser.parseString(json);
JsonObject o = el.isJsonObject() ? el.getAsJsonObject() : new JsonObject();

String reference = pick(o, "reference", "ref", "scriptureReference", "title");
String verse = pick(o, "verse", "content", "text", "scripture", "passage");
String link = pick(o, "link", "url", "passageUrl");
String date = pick(o, "date", "day", "displayDate");

if (date == null || date.isBlank()) {
date = LocalDate.now().toString();
}

if (reference == null) reference = "Verse of the Day";
if (verse == null) verse = "(No verse content returned by API)";
if (link == null) link = "";

return new Votd(reference, verse, link, date);
}

public static Devotion parseDevotion(String json) {
JsonElement el = JsonParser.parseString(json);
JsonObject o = el.isJsonObject() ? el.getAsJsonObject() : new JsonObject();

String title = pick(o, "title", "heading");
String reading = pick(o, "reading", "themeVerse", "verse", "scripture");
String bible = pick(o, "bibleInOneYear", "soulFood", "bibleReadingPlan", "plan");

List<String> content = new ArrayList<>();

// content as array of paragraphs
JsonElement c = o.get("content");
if (c != null && c.isJsonArray()) {
for (JsonElement p : c.getAsJsonArray()) {
if (p != null && p.isJsonPrimitive()) content.add(clean(p.getAsString()));
}
} else if (c != null && c.isJsonPrimitive()) {
content.add(clean(c.getAsString()));
} else {
// fallback possible keys
String body = pick(o, "body", "text");
if (body != null) content.add(body);
}

if (title == null) title = "Today's Devotion";
if (reading == null) reading = "";
if (bible == null) bible = "";

return new Devotion(title, reading, content, bible);
}

private static String pick(JsonObject o, String... keys) {
for (String k : keys) {
if (!o.has(k)) continue;
JsonElement e = o.get(k);
if (e == null || e.isJsonNull()) continue;
if (e.isJsonPrimitive()) return clean(e.getAsString());
}
return null;
}

private static String clean(String text) {
if (text == null) return "";
// Strips common Discord-style timestamps that might be accidentally included
// e.g. [10:44 AM]Wednesday, 18 February 2026 10:44 AM
return text.replaceAll("\\[\\d{1,2}:\\d{2}\\s?[AP]M\\].*?(\\d{4}.*?\\d{1,2}:\\d{2}\\s?[AP]M|\\d{4})", "").trim();
}
}
15 changes: 15 additions & 0 deletions src/main/java/com/devoteme/minecraft/cache/ContentCache.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.devoteme.minecraft.cache;

import com.devoteme.minecraft.model.Devotion;
import com.devoteme.minecraft.model.Votd;

public class ContentCache {
private volatile Votd votd;
private volatile Devotion devotion;

public Votd getVotd() { return votd; }
public Devotion getDevotion() { return devotion; }

public void setVotd(Votd v) { this.votd = v; }
public void setDevotion(Devotion d) { this.devotion = d; }
}
Loading