diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..e0f15db
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "java.configuration.updateBuildConfiguration": "automatic"
+}
\ No newline at end of file
diff --git a/dependency-reduced-pom.xml b/dependency-reduced-pom.xml
new file mode 100644
index 0000000..8df9a76
--- /dev/null
+++ b/dependency-reduced-pom.xml
@@ -0,0 +1,65 @@
+
+
+ 4.0.0
+ com.adzel.addyauthreborn
+ AddyAuthReborn
+ AddyAuth Reborn
+ 0.1-pre
+
+
+
+ maven-compiler-plugin
+ 3.11.0
+
+ ${java.version}
+ ${java.version}
+
+
+
+ maven-shade-plugin
+ 3.5.0
+
+
+ package
+
+ shade
+
+
+ true
+
+
+ at.favre.lib
+ com.adzel.addyauthreborn.shaded.bcrypt
+
+
+
+
+
+
+
+
+
+
+ papermc-repo
+ https://repo.papermc.io/repository/maven-public/
+
+
+ sonatype-oss
+ https://oss.sonatype.org/content/repositories/releases/
+
+
+
+
+ io.papermc.paper
+ paper-api
+ 1.20.4-R0.1-SNAPSHOT
+ provided
+
+
+
+ 0.9.0
+ 17
+ 1.20.4-R0.1-SNAPSHOT
+ 3.3.3
+
+
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..abd2e3e
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,103 @@
+
+ 4.0.0
+
+ com.adzel.addyauthreborn
+ AddyAuthReborn
+ 0.1-pre
+ AddyAuth Reborn
+
+
+ 17
+ 1.20.4-R0.1-SNAPSHOT
+ 0.9.0
+ 3.3.3
+
+
+
+
+ papermc-repo
+ https://repo.papermc.io/repository/maven-public/
+
+
+ sonatype-oss
+ https://oss.sonatype.org/content/repositories/releases/
+
+
+
+
+
+
+ io.papermc.paper
+ paper-api
+ ${paper.version}
+ provided
+
+
+
+
+ at.favre.lib
+ bcrypt
+ ${bcrypt.version}
+
+
+
+
+ org.mariadb.jdbc
+ mariadb-java-client
+ ${mariadb.version}
+
+
+
+
+ net.kyori
+ adventure-platform-bukkit
+ 4.3.2
+
+
+
+
+ com.zaxxer
+ HikariCP
+ 5.1.0
+
+
+
+
+
+
+
+ maven-compiler-plugin
+ 3.11.0
+
+ ${java.version}
+ ${java.version}
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.5.0
+
+
+ package
+ shade
+
+ true
+
+
+
+ at.favre.lib
+ com.adzel.addyauthreborn.shaded.bcrypt
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/com/adzel/addyauthreborn/AddyAuthReborn.java b/src/main/java/com/adzel/addyauthreborn/AddyAuthReborn.java
new file mode 100644
index 0000000..dcfe16c
--- /dev/null
+++ b/src/main/java/com/adzel/addyauthreborn/AddyAuthReborn.java
@@ -0,0 +1,117 @@
+package com.adzel.addyauthreborn;
+
+import com.adzel.addyauthreborn.auth.AuthManager;
+import com.adzel.addyauthreborn.commands.LoginCommand;
+import com.adzel.addyauthreborn.commands.LockCommand;
+import com.adzel.addyauthreborn.commands.RegisterCommand;
+import com.adzel.addyauthreborn.commands.AddyAuthCommand;
+import com.adzel.addyauthreborn.listeners.LockListeners;
+import com.adzel.addyauthreborn.listeners.AdminJoinListener;
+import com.adzel.addyauthreborn.listeners.ConnectionListener;
+import com.adzel.addyauthreborn.lock.AFKMonitor;
+import com.adzel.addyauthreborn.lock.OfflineLockManager;
+import com.adzel.addyauthreborn.updates.UpdateChecker;
+import com.adzel.addyauthreborn.utils.DatabaseUtil;
+import com.zaxxer.hikari.HikariDataSource;
+import net.kyori.adventure.platform.bukkit.BukkitAudiences;
+import org.bukkit.plugin.java.JavaPlugin;
+
+public class AddyAuthReborn extends JavaPlugin {
+
+ private static AddyAuthReborn instance;
+ private AuthManager authManager;
+ private AFKMonitor afkMonitor;
+ private OfflineLockManager offlineLockManager;
+ private BukkitAudiences adventure;
+ private boolean updateAvailable = false;
+
+ @Override
+ public void onEnable() {
+ instance = this;
+
+ saveDefaultConfig();
+
+ adventure = BukkitAudiences.create(this);
+
+ if (!DatabaseUtil.initDatabase()) {
+ getLogger().severe("Failed to initialize database. Disabling plugin.");
+ getServer().getPluginManager().disablePlugin(this);
+ return;
+ }
+
+ authManager = new AuthManager();
+ afkMonitor = new AFKMonitor(this);
+ offlineLockManager = new OfflineLockManager(this);
+
+ // Commands
+ getCommand("register").setExecutor(new RegisterCommand(this));
+ getCommand("login").setExecutor(new LoginCommand(this));
+ getCommand("lock").setExecutor(new LockCommand(this));
+ getCommand("addyauth").setExecutor(new AddyAuthCommand(this));
+
+ // Listeners
+ getServer().getPluginManager().registerEvents(new LockListeners(this), this);
+ getServer().getPluginManager().registerEvents(new AdminJoinListener(this), this);
+ getServer().getPluginManager().registerEvents(new ConnectionListener(this), this); // PlayerJoin/Leave for tracking AFK state
+
+ // Update check
+ new UpdateChecker(this, "https://git.adzeldevelops.site/api/v1/repos/AdzelFirestar/AddyAuthReborn/releases/latest")
+ .getLatestVersion(latest -> {
+ String current = getDescription().getVersion();
+ if (!current.equalsIgnoreCase(latest)) {
+ updateAvailable = true;
+ getLogger().warning("A new version of AddyAuth Reborn is available: " + latest);
+ }
+ });
+
+ getLogger().info("AddyAuth Reborn enabled successfully.");
+ }
+
+ @Override
+ public void onDisable() {
+ if (adventure != null) {
+ adventure.close();
+ }
+ if (afkMonitor != null) {
+ afkMonitor.shutdown(); // 🧼 clean up task
+ }
+ DatabaseUtil.shutdown();
+ getLogger().info("AddyAuth Reborn has been disabled.");
+ }
+
+ public static AddyAuthReborn getInstance() {
+ return instance;
+ }
+
+ public AuthManager getAuthManager() {
+ return authManager;
+ }
+
+ public AFKMonitor getAFKMonitor() {
+ return afkMonitor;
+ }
+
+ public OfflineLockManager getOfflineLockManager() {
+ return offlineLockManager;
+ }
+
+ public HikariDataSource getDataSource() {
+ return DatabaseUtil.getDataSource();
+ }
+
+ public boolean isUpdateAvailable() {
+ return updateAvailable;
+ }
+
+ public int getMinPasswordLength() {
+ return getConfig().getInt("General Settings.Minimum Password Length", 5);
+ }
+
+ public int getMaxPasswordLength() {
+ return getConfig().getInt("General Settings.Maximum Password Length", 15);
+ }
+
+ public BukkitAudiences adventure() {
+ return adventure;
+ }
+}
diff --git a/src/main/java/com/adzel/addyauthreborn/auth/AuthManager.java b/src/main/java/com/adzel/addyauthreborn/auth/AuthManager.java
new file mode 100644
index 0000000..2c76b9d
--- /dev/null
+++ b/src/main/java/com/adzel/addyauthreborn/auth/AuthManager.java
@@ -0,0 +1,61 @@
+package com.adzel.addyauthreborn.auth;
+
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+import at.favre.lib.crypto.bcrypt.BCrypt;
+
+public class AuthManager {
+
+ private final AuthStorage storage;
+ private final Set lockedPlayers = ConcurrentHashMap.newKeySet();
+
+ public AuthManager() {
+ this.storage = new AuthStorage(); // will handle DB interactions
+ }
+
+ public boolean isRegistered(UUID uuid) {
+ return storage.isRegistered(uuid);
+ }
+
+ public boolean registerPassword(UUID uuid, String plainPassword) {
+ if (isRegistered(uuid)) return false;
+
+ String hash = BCrypt.withDefaults().hashToString(12, plainPassword.toCharArray());
+ return storage.savePassword(uuid, hash);
+ }
+
+ public boolean verifyPassword(UUID uuid, String attempt) {
+ String hash = storage.getPasswordHash(uuid);
+ if (hash != null) {
+ BCrypt.Result result = BCrypt.verifyer().verify(attempt.toCharArray(), hash);
+ if (result.verified) {
+ unlock(uuid); // unlock on successful login
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void lock(UUID uuid) {
+ lockedPlayers.add(uuid);
+ storage.setLastLockTime(uuid, System.currentTimeMillis());
+ }
+
+ public void unlock(UUID uuid) {
+ lockedPlayers.remove(uuid);
+ }
+
+ public boolean isLocked(UUID uuid) {
+ return lockedPlayers.contains(uuid);
+ }
+
+ public long getLastLockTime(UUID uuid) {
+ return storage.getLastLockTime(uuid);
+ }
+
+ public AuthStorage getStorage() {
+ return storage;
+ }
+}
diff --git a/src/main/java/com/adzel/addyauthreborn/auth/AuthStorage.java b/src/main/java/com/adzel/addyauthreborn/auth/AuthStorage.java
new file mode 100644
index 0000000..79154dd
--- /dev/null
+++ b/src/main/java/com/adzel/addyauthreborn/auth/AuthStorage.java
@@ -0,0 +1,120 @@
+package com.adzel.addyauthreborn.auth;
+
+import com.adzel.addyauthreborn.AddyAuthReborn;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.UUID;
+
+public class AuthStorage {
+
+ private final AddyAuthReborn plugin = AddyAuthReborn.getInstance();
+
+ public AuthStorage() {
+ try (Connection conn = plugin.getDataSource().getConnection()) {
+ PreparedStatement ps = conn.prepareStatement(
+ "CREATE TABLE IF NOT EXISTS addyauth_users (" +
+ "uuid VARCHAR(36) PRIMARY KEY," +
+ "password_hash TEXT NOT NULL," +
+ "last_locked_at BIGINT DEFAULT 0)"
+ );
+ ps.executeUpdate();
+ ps.close();
+ } catch (SQLException e) {
+ plugin.getLogger().severe("Failed to create users table: " + e.getMessage());
+ }
+ }
+
+ public boolean isRegistered(UUID uuid) {
+ try (Connection conn = plugin.getDataSource().getConnection()) {
+ PreparedStatement ps = conn.prepareStatement(
+ "SELECT uuid FROM addyauth_users WHERE uuid = ?"
+ );
+ ps.setString(1, uuid.toString());
+ ResultSet rs = ps.executeQuery();
+ boolean exists = rs.next();
+ rs.close();
+ ps.close();
+ return exists;
+ } catch (SQLException e) {
+ plugin.getLogger().warning("Failed to check registration: " + e.getMessage());
+ return false;
+ }
+ }
+
+ public boolean savePassword(UUID uuid, String hashedPassword) {
+ try (Connection conn = plugin.getDataSource().getConnection()) {
+ PreparedStatement ps = conn.prepareStatement(
+ "INSERT INTO addyauth_users (uuid, password_hash) VALUES (?, ?)"
+ );
+ ps.setString(1, uuid.toString());
+ ps.setString(2, hashedPassword);
+ ps.executeUpdate();
+ ps.close();
+ return true;
+ } catch (SQLException e) {
+ plugin.getLogger().warning("Failed to save password: " + e.getMessage());
+ return false;
+ }
+ }
+
+ public String getPasswordHash(UUID uuid) {
+ try (Connection conn = plugin.getDataSource().getConnection()) {
+ PreparedStatement ps = conn.prepareStatement(
+ "SELECT password_hash FROM addyauth_users WHERE uuid = ?"
+ );
+ ps.setString(1, uuid.toString());
+ ResultSet rs = ps.executeQuery();
+ if (rs.next()) {
+ String hash = rs.getString("password_hash");
+ rs.close();
+ ps.close();
+ return hash;
+ }
+ rs.close();
+ ps.close();
+ } catch (SQLException e) {
+ plugin.getLogger().warning("Failed to retrieve password hash: " + e.getMessage());
+ }
+ return null;
+ }
+
+ public boolean setLastLockTime(UUID uuid, long timestamp) {
+ try (Connection conn = plugin.getDataSource().getConnection()) {
+ PreparedStatement ps = conn.prepareStatement(
+ "UPDATE addyauth_users SET last_locked_at = ? WHERE uuid = ?"
+ );
+ ps.setLong(1, timestamp);
+ ps.setString(2, uuid.toString());
+ int updated = ps.executeUpdate();
+ ps.close();
+ return updated > 0;
+ } catch (SQLException e) {
+ plugin.getLogger().warning("Failed to update last lock time: " + e.getMessage());
+ return false;
+ }
+ }
+
+ public long getLastLockTime(UUID uuid) {
+ try (Connection conn = plugin.getDataSource().getConnection()) {
+ PreparedStatement ps = conn.prepareStatement(
+ "SELECT last_locked_at FROM addyauth_users WHERE uuid = ?"
+ );
+ ps.setString(1, uuid.toString());
+ ResultSet rs = ps.executeQuery();
+ if (rs.next()) {
+ long timestamp = rs.getLong("last_locked_at");
+ rs.close();
+ ps.close();
+ return timestamp;
+ }
+ rs.close();
+ ps.close();
+ } catch (SQLException e) {
+ plugin.getLogger().warning("Failed to fetch last lock time: " + e.getMessage());
+ }
+ return 0;
+ }
+}
diff --git a/src/main/java/com/adzel/addyauthreborn/commands/AddyAuthCommand.java b/src/main/java/com/adzel/addyauthreborn/commands/AddyAuthCommand.java
new file mode 100644
index 0000000..6e55bea
--- /dev/null
+++ b/src/main/java/com/adzel/addyauthreborn/commands/AddyAuthCommand.java
@@ -0,0 +1,46 @@
+package com.adzel.addyauthreborn.commands;
+
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+
+import com.adzel.addyauthreborn.AddyAuthReborn;
+import com.adzel.addyauthreborn.utils.MessageUtil;
+
+public class AddyAuthCommand implements CommandExecutor {
+
+ private final AddyAuthReborn plugin;
+
+ public AddyAuthCommand(AddyAuthReborn plugin) {
+ this.plugin = plugin;
+ }
+
+ @Override
+ public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
+
+ if (!(sender instanceof Player player)) {
+ sender.sendMessage("This command can only be run by a player.");
+ return true;
+ }
+
+ if (!player.hasPermission("addyauth.admin")) {
+ MessageUtil.send(player, "&cYou do not have permission to use this command.");
+ return true;
+ }
+
+ if (args.length == 0) {
+ MessageUtil.send(player, "&eUsage: /addyauth reload");
+ return true;
+ }
+
+ if (args[0].equalsIgnoreCase("reload")) {
+ plugin.reloadConfig();
+ MessageUtil.send(player, "&aAddyAuth Reborn config reloaded.");
+ return true;
+ }
+
+ MessageUtil.send(player, "&cUnknown subcommand.");
+ return true;
+ }
+}
diff --git a/src/main/java/com/adzel/addyauthreborn/commands/LockCommand.java b/src/main/java/com/adzel/addyauthreborn/commands/LockCommand.java
new file mode 100644
index 0000000..3ccf43b
--- /dev/null
+++ b/src/main/java/com/adzel/addyauthreborn/commands/LockCommand.java
@@ -0,0 +1,34 @@
+package com.adzel.addyauthreborn.commands;
+
+import com.adzel.addyauthreborn.AddyAuthReborn;
+import com.adzel.addyauthreborn.utils.MessageUtil;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+
+public class LockCommand implements CommandExecutor {
+
+ private final AddyAuthReborn plugin;
+
+ public LockCommand(AddyAuthReborn plugin) {
+ this.plugin = plugin;
+ }
+
+ @Override
+ public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
+ if (!(sender instanceof Player player)) {
+ sender.sendMessage("Only players can use this command.");
+ return true;
+ }
+
+ if (!player.hasPermission("addyauth.lock")) {
+ MessageUtil.send(player, "&cYou do not have permission to lock yourself.");
+ return true;
+ }
+
+ plugin.getAuthManager().lock(player.getUniqueId());
+ MessageUtil.send(player, "&eYou are now locked. Use &f/login &eto unlock.");
+ return true;
+ }
+}
diff --git a/src/main/java/com/adzel/addyauthreborn/commands/LoginCommand.java b/src/main/java/com/adzel/addyauthreborn/commands/LoginCommand.java
new file mode 100644
index 0000000..c349c4c
--- /dev/null
+++ b/src/main/java/com/adzel/addyauthreborn/commands/LoginCommand.java
@@ -0,0 +1,51 @@
+package com.adzel.addyauthreborn.commands;
+
+import com.adzel.addyauthreborn.AddyAuthReborn;
+import com.adzel.addyauthreborn.utils.MessageUtil;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+
+public class LoginCommand implements CommandExecutor {
+
+ private final AddyAuthReborn plugin;
+
+ public LoginCommand(AddyAuthReborn plugin) {
+ this.plugin = plugin;
+ }
+
+ @Override
+ public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
+ if (!(sender instanceof Player player)) {
+ sender.sendMessage("Only players can use this command.");
+ return true;
+ }
+
+ if (!player.hasPermission("addyauth.login")) {
+ MessageUtil.send(player, "&cYou do not have permission to login.");
+ return true;
+ }
+
+ if (args.length != 1) {
+ MessageUtil.send(player, "&cUsage: /login ");
+ return true;
+ }
+
+ if (!plugin.getAuthManager().isRegistered(player.getUniqueId())) {
+ MessageUtil.send(player, "&eYou are not registered. Use &f/register &e first.");
+ return true;
+ }
+
+ String password = args[0];
+ boolean valid = plugin.getAuthManager().verifyPassword(player.getUniqueId(), password);
+
+ if (!valid) {
+ MessageUtil.send(player, "&cIncorrect password.");
+ return true;
+ }
+
+ MessageUtil.send(player, "&aYou are now logged in and unlocked.");
+ return true;
+ }
+}
diff --git a/src/main/java/com/adzel/addyauthreborn/commands/RegisterCommand.java b/src/main/java/com/adzel/addyauthreborn/commands/RegisterCommand.java
new file mode 100644
index 0000000..0b287ae
--- /dev/null
+++ b/src/main/java/com/adzel/addyauthreborn/commands/RegisterCommand.java
@@ -0,0 +1,58 @@
+package com.adzel.addyauthreborn.commands;
+
+import com.adzel.addyauthreborn.AddyAuthReborn;
+import com.adzel.addyauthreborn.utils.MessageUtil;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+
+public class RegisterCommand implements CommandExecutor {
+
+ private final AddyAuthReborn plugin;
+
+ public RegisterCommand(AddyAuthReborn plugin) {
+ this.plugin = plugin;
+ }
+
+ @Override
+ public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
+ if (!(sender instanceof Player player)) {
+ sender.sendMessage("Only players can use this command.");
+ return true;
+ }
+
+ if (!player.hasPermission("addyauth.register")) {
+ MessageUtil.send(player, "&cYou do not have permission to register.");
+ return true;
+ }
+
+ if (args.length != 1) {
+ MessageUtil.send(player, "&cUsage: /register ");
+ return true;
+ }
+
+ String password = args[0];
+ int minLength = plugin.getMinPasswordLength();
+ int maxLength = plugin.getMaxPasswordLength();
+
+ if (password.length() < minLength || password.length() > maxLength) {
+ MessageUtil.send(player, "&cPassword must be between " + minLength + " and " + maxLength + " characters.");
+ return true;
+ }
+
+ if (plugin.getAuthManager().isRegistered(player.getUniqueId())) {
+ MessageUtil.send(player, "&eYou are already registered.");
+ return true;
+ }
+
+ boolean success = plugin.getAuthManager().registerPassword(player.getUniqueId(), password);
+ if (success) {
+ MessageUtil.send(player, "&aYour password has been registered successfully.");
+ } else {
+ MessageUtil.send(player, "&cSomething went wrong. Please try again.");
+ }
+
+ return true;
+ }
+}
diff --git a/src/main/java/com/adzel/addyauthreborn/listeners/AdminJoinListener.java b/src/main/java/com/adzel/addyauthreborn/listeners/AdminJoinListener.java
new file mode 100644
index 0000000..8bffbf5
--- /dev/null
+++ b/src/main/java/com/adzel/addyauthreborn/listeners/AdminJoinListener.java
@@ -0,0 +1,31 @@
+package com.adzel.addyauthreborn.listeners;
+
+import com.adzel.addyauthreborn.AddyAuthReborn;
+import org.bukkit.Bukkit;
+import org.bukkit.ChatColor;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.PlayerJoinEvent;
+
+public class AdminJoinListener implements Listener {
+
+ private final AddyAuthReborn plugin;
+
+ public AdminJoinListener(AddyAuthReborn plugin) {
+ this.plugin = plugin;
+ }
+
+ @EventHandler
+ public void onAdminJoin(PlayerJoinEvent event) {
+ if (!plugin.isUpdateAvailable()) return;
+
+ if (event.getPlayer().hasPermission("addyauth.admin")) {
+ Bukkit.getScheduler().runTaskLater(plugin, () -> {
+ event.getPlayer().sendMessage(ChatColor.YELLOW + "[AddyAuth Reborn] " +
+ ChatColor.RED + "A new version is available!");
+ event.getPlayer().sendMessage(ChatColor.GRAY + "Check: " +
+ ChatColor.BLUE + "https://git.adzeldevelops.site/Adzel/AddyAuth-Reborn/releases/latest");
+ }, 40L); // delay a bit so it doesn't clash with login spam
+ }
+ }
+}
diff --git a/src/main/java/com/adzel/addyauthreborn/listeners/ConnectionListener.java b/src/main/java/com/adzel/addyauthreborn/listeners/ConnectionListener.java
new file mode 100644
index 0000000..77ff5f5
--- /dev/null
+++ b/src/main/java/com/adzel/addyauthreborn/listeners/ConnectionListener.java
@@ -0,0 +1,32 @@
+package com.adzel.addyauthreborn.listeners;
+
+import com.adzel.addyauthreborn.AddyAuthReborn;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.PlayerJoinEvent;
+import org.bukkit.event.player.PlayerQuitEvent;
+
+import java.util.UUID;
+
+public class ConnectionListener implements Listener {
+
+ private final AddyAuthReborn plugin;
+
+ public ConnectionListener(AddyAuthReborn plugin) {
+ this.plugin = plugin;
+ }
+
+ @EventHandler
+ public void onJoin(PlayerJoinEvent event) {
+ UUID uuid = event.getPlayer().getUniqueId();
+ plugin.getOfflineLockManager().evaluateLogin(uuid);
+ plugin.getAFKMonitor().updateActivity(uuid); // start tracking afk immediately
+ }
+
+ @EventHandler
+ public void onQuit(PlayerQuitEvent event) {
+ UUID uuid = event.getPlayer().getUniqueId();
+ plugin.getOfflineLockManager().recordLogout(uuid);
+ plugin.getAFKMonitor().removePlayer(uuid); // clean up
+ }
+}
diff --git a/src/main/java/com/adzel/addyauthreborn/listeners/LockListeners.java b/src/main/java/com/adzel/addyauthreborn/listeners/LockListeners.java
new file mode 100644
index 0000000..b519b97
--- /dev/null
+++ b/src/main/java/com/adzel/addyauthreborn/listeners/LockListeners.java
@@ -0,0 +1,90 @@
+package com.adzel.addyauthreborn.listeners;
+
+import com.adzel.addyauthreborn.AddyAuthReborn;
+import com.adzel.addyauthreborn.utils.MessageUtil;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.event.player.*;
+
+public class LockListeners implements Listener {
+
+ private final AddyAuthReborn plugin;
+
+ public LockListeners(AddyAuthReborn plugin) {
+ this.plugin = plugin;
+ }
+
+ private boolean isLocked(Player player) {
+ return plugin.getAuthManager().isLocked(player.getUniqueId());
+ }
+
+ private void updateIfNotLocked(Player player) {
+ if (!isLocked(player)) {
+ plugin.getAFKMonitor().updateActivity(player.getUniqueId());
+ }
+ }
+
+ @EventHandler
+ public void onMove(PlayerMoveEvent event) {
+ Player player = event.getPlayer();
+ if (isLocked(player) && event.getFrom().distanceSquared(event.getTo()) > 0) {
+ event.setCancelled(true);
+ } else if (event.getFrom().distanceSquared(event.getTo()) > 1) {
+ updateIfNotLocked(player);
+ }
+ }
+
+ @EventHandler
+ public void onChat(AsyncPlayerChatEvent event) {
+ Player player = event.getPlayer();
+ if (isLocked(player)) {
+ event.setCancelled(true);
+ MessageUtil.send(player, "&cYou cannot chat while locked.");
+ } else {
+ updateIfNotLocked(player);
+ }
+ }
+
+ @EventHandler
+ public void onClick(InventoryClickEvent event) {
+ if (event.getWhoClicked() instanceof Player player) {
+ if (isLocked(player)) {
+ event.setCancelled(true);
+ } else {
+ updateIfNotLocked(player);
+ }
+ }
+ }
+
+ @EventHandler
+ public void onDrop(PlayerDropItemEvent event) {
+ Player player = event.getPlayer();
+ if (isLocked(player)) {
+ event.setCancelled(true);
+ } else {
+ updateIfNotLocked(player);
+ }
+ }
+
+ @EventHandler
+ public void onPickup(PlayerAttemptPickupItemEvent event) {
+ Player player = event.getPlayer();
+ if (isLocked(player)) {
+ event.setCancelled(true);
+ } else {
+ updateIfNotLocked(player);
+ }
+ }
+
+ @EventHandler
+ public void onInteract(PlayerInteractEvent event) {
+ Player player = event.getPlayer();
+ if (isLocked(player)) {
+ event.setCancelled(true);
+ } else {
+ updateIfNotLocked(player);
+ }
+ }
+}
diff --git a/src/main/java/com/adzel/addyauthreborn/lock/AFKMonitor.java b/src/main/java/com/adzel/addyauthreborn/lock/AFKMonitor.java
new file mode 100644
index 0000000..ae024f6
--- /dev/null
+++ b/src/main/java/com/adzel/addyauthreborn/lock/AFKMonitor.java
@@ -0,0 +1,73 @@
+package com.adzel.addyauthreborn.lock;
+
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+import org.bukkit.scheduler.BukkitRunnable;
+
+import com.adzel.addyauthreborn.AddyAuthReborn;
+import com.adzel.addyauthreborn.utils.MessageUtil;
+
+public class AFKMonitor {
+
+ private final AddyAuthReborn plugin;
+ private final Map lastActivityMap = new ConcurrentHashMap<>();
+ private final long timeoutMillis;
+ private final boolean enabled;
+ private BukkitRunnable monitorTask;
+
+ public AFKMonitor(AddyAuthReborn plugin) {
+ this.plugin = plugin;
+ this.enabled = plugin.getConfig().getBoolean("Time Based Settings.AFK Lock.Enabled", true);
+ this.timeoutMillis = plugin.getConfig().getLong("Time Based Settings.AFK Lock.Auto Lock", 30) * 60 * 1000L;
+
+ if (enabled) {
+ startMonitor();
+ }
+ }
+
+ public void updateActivity(UUID uuid) {
+ lastActivityMap.put(uuid, System.currentTimeMillis());
+ }
+
+ public void remove(UUID uuid) {
+ lastActivityMap.remove(uuid);
+ }
+
+ public void removePlayer(UUID uuid) {
+ lastActivityMap.remove(uuid);
+ }
+
+ private void startMonitor() {
+ monitorTask = new BukkitRunnable() {
+ @Override
+ public void run() {
+ long now = System.currentTimeMillis();
+
+ for (Player player : Bukkit.getOnlinePlayers()) {
+ UUID uuid = player.getUniqueId();
+
+ if (!plugin.getAuthManager().isRegistered(uuid)) continue;
+ if (plugin.getAuthManager().isLocked(uuid)) continue;
+
+ long lastActive = lastActivityMap.getOrDefault(uuid, now);
+ if ((now - lastActive) >= timeoutMillis) {
+ plugin.getAuthManager().lock(uuid); // ✅ Stores timestamp
+ MessageUtil.send(player, "&cYou have been locked due to AFK timeout.");
+ }
+ }
+ }
+ };
+ monitorTask.runTaskTimer(plugin, 20L * 60, 20L * 60); // every 60 seconds
+ }
+
+ public void shutdown() {
+ if (monitorTask != null) {
+ monitorTask.cancel();
+ }
+ lastActivityMap.clear();
+ }
+}
diff --git a/src/main/java/com/adzel/addyauthreborn/lock/OfflineLockManager.java b/src/main/java/com/adzel/addyauthreborn/lock/OfflineLockManager.java
new file mode 100644
index 0000000..f208e2c
--- /dev/null
+++ b/src/main/java/com/adzel/addyauthreborn/lock/OfflineLockManager.java
@@ -0,0 +1,40 @@
+package com.adzel.addyauthreborn.lock;
+
+import com.adzel.addyauthreborn.AddyAuthReborn;
+
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class OfflineLockManager {
+
+ private final AddyAuthReborn plugin;
+ private final Map logoutTimestamps = new ConcurrentHashMap<>();
+ private final long offlineThresholdMillis;
+
+ public OfflineLockManager(AddyAuthReborn plugin) {
+ this.plugin = plugin;
+ long minutes = plugin.getConfig().getLong("Time Based Settings.Offline Lock.Auto Lock", 120);
+ this.offlineThresholdMillis = minutes * 60_000;
+ }
+
+ public void recordLogout(UUID uuid) {
+ logoutTimestamps.put(uuid, System.currentTimeMillis());
+ }
+
+ public void evaluateLogin(UUID uuid) {
+ if (!plugin.getConfig().getBoolean("Time Based Settings.Offline Lock.Enabled", true)) return;
+
+ Long logoutTime = logoutTimestamps.get(uuid);
+ if (logoutTime == null) return;
+
+ long elapsed = System.currentTimeMillis() - logoutTime;
+ if (elapsed >= offlineThresholdMillis) {
+ plugin.getAuthManager().lock(uuid); // ✅ Stores timestamp
+ }
+ }
+
+ public void forget(UUID uuid) {
+ logoutTimestamps.remove(uuid);
+ }
+}
diff --git a/src/main/java/com/adzel/addyauthreborn/updates/UpdateChecker.java b/src/main/java/com/adzel/addyauthreborn/updates/UpdateChecker.java
new file mode 100644
index 0000000..6da5884
--- /dev/null
+++ b/src/main/java/com/adzel/addyauthreborn/updates/UpdateChecker.java
@@ -0,0 +1,67 @@
+package com.adzel.addyauthreborn.updates;
+
+import com.adzel.addyauthreborn.AddyAuthReborn;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.function.Consumer;
+
+public class UpdateChecker {
+
+ private final AddyAuthReborn plugin;
+ private final String apiUrl;
+
+ public UpdateChecker(AddyAuthReborn plugin, String apiUrl) {
+ this.plugin = plugin;
+ this.apiUrl = apiUrl;
+ }
+
+ public void getLatestVersion(Consumer callback) {
+ plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> {
+ try {
+ // Gitea's latest release endpoint returns a JSON object
+ URL url = new URL(apiUrl);
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setRequestProperty("Accept", "application/json");
+ connection.setConnectTimeout(5000);
+ connection.setReadTimeout(5000);
+
+ if (connection.getResponseCode() != 200) {
+ plugin.getLogger().warning("Update check failed: Invalid response from Gitea (" + connection.getResponseCode() + ")");
+ return;
+ }
+
+ BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
+ StringBuilder json = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ json.append(line);
+ }
+ reader.close();
+
+ String latest = parseTagName(json.toString());
+ if (latest != null) {
+ callback.accept(latest);
+ } else {
+ plugin.getLogger().warning("Update check failed: Could not parse latest tag.");
+ }
+
+ } catch (Exception e) {
+ plugin.getLogger().warning("Update check failed: " + e.getMessage());
+ }
+ });
+ }
+
+ private String parseTagName(String json) {
+ // crude but fast — this works with Gitea’s /releases/latest API
+ String key = "\"tag_name\":\"";
+ int index = json.indexOf(key);
+ if (index == -1) return null;
+
+ int start = index + key.length();
+ int end = json.indexOf("\"", start);
+ return end == -1 ? null : json.substring(start, end);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/adzel/addyauthreborn/utils/DatabaseUtil.java b/src/main/java/com/adzel/addyauthreborn/utils/DatabaseUtil.java
new file mode 100644
index 0000000..49b0654
--- /dev/null
+++ b/src/main/java/com/adzel/addyauthreborn/utils/DatabaseUtil.java
@@ -0,0 +1,78 @@
+package com.adzel.addyauthreborn.utils;
+
+import com.adzel.addyauthreborn.AddyAuthReborn;
+import com.zaxxer.hikari.HikariConfig;
+import com.zaxxer.hikari.HikariDataSource;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+public class DatabaseUtil {
+
+ private static HikariDataSource dataSource;
+
+ public static boolean initDatabase() {
+ AddyAuthReborn plugin = AddyAuthReborn.getInstance();
+
+ String type = plugin.getConfig().getString("Database.Type", "sqlite").toLowerCase();
+ HikariConfig config = new HikariConfig();
+
+ try {
+ switch (type) {
+ case "mysql":
+ case "mariadb":
+ config.setJdbcUrl("jdbc:mariadb://" +
+ plugin.getConfig().getString("Database.Host", "localhost") + ":" +
+ plugin.getConfig().getInt("Database.Port", 3306) + "/" +
+ plugin.getConfig().getString("Database.Name", "addyauth"));
+ config.setUsername(plugin.getConfig().getString("Database.Username", "root"));
+ config.setPassword(plugin.getConfig().getString("Database.Password", ""));
+ config.setDriverClassName("org.mariadb.jdbc.Driver");
+ break;
+
+ case "sqlite":
+ default:
+ config.setJdbcUrl("jdbc:sqlite:" + plugin.getDataFolder() + "/addyauth.db");
+ config.setDriverClassName("org.sqlite.JDBC");
+ break;
+ }
+
+ config.setMaximumPoolSize(5);
+ config.setPoolName("AddyAuthPool");
+ dataSource = new HikariDataSource(config);
+
+ try (Connection conn = dataSource.getConnection()) {
+ createTables(conn);
+ }
+
+ return true;
+
+ } catch (Exception e) {
+ plugin.getLogger().severe("Database initialization failed: " + e.getMessage());
+ return false;
+ }
+ }
+
+ private static void createTables(Connection conn) throws SQLException {
+ try (Statement stmt = conn.createStatement()) {
+ stmt.executeUpdate("""
+ CREATE TABLE IF NOT EXISTS addyauth_users (
+ uuid VARCHAR(36) PRIMARY KEY,
+ password_hash TEXT NOT NULL,
+ last_locked_at BIGINT DEFAULT 0
+ )
+ """);
+ }
+ }
+
+ public static HikariDataSource getDataSource() {
+ return dataSource;
+ }
+
+ public static void shutdown() {
+ if (dataSource != null && !dataSource.isClosed()) {
+ dataSource.close();
+ }
+ }
+}
diff --git a/src/main/java/com/adzel/addyauthreborn/utils/MessageUtil.java b/src/main/java/com/adzel/addyauthreborn/utils/MessageUtil.java
new file mode 100644
index 0000000..dadde88
--- /dev/null
+++ b/src/main/java/com/adzel/addyauthreborn/utils/MessageUtil.java
@@ -0,0 +1,25 @@
+package com.adzel.addyauthreborn.utils;
+
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.minimessage.MiniMessage;
+import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
+import org.bukkit.entity.Player;
+
+public class MessageUtil {
+
+ private static final MiniMessage mini = MiniMessage.miniMessage();
+ private static final LegacyComponentSerializer legacy = LegacyComponentSerializer.builder()
+ .character('&')
+ .hexColors()
+ .build();
+
+ public static void send(Player player, String message) {
+ // Convert &x style to MiniMessage
+ Component component = mini.deserialize(legacy.serialize(legacy.deserialize(message)));
+ player.sendMessage(component);
+ }
+
+ public static Component parse(String message) {
+ return mini.deserialize(legacy.serialize(legacy.deserialize(message)));
+ }
+}
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
new file mode 100644
index 0000000..9842291
--- /dev/null
+++ b/src/main/resources/plugin.yml
@@ -0,0 +1,53 @@
+name: AddyAuthReborn
+version: 0.1-pre
+main: com.adzel.addyauthreborn.AddyAuthReborn
+api-version: 1.20
+author: AdzelFirestar
+description: >
+ A secure authentication system for staff accounts, requiring a password before access to movement, interaction, and chat.
+
+commands:
+ register:
+ description: Register your staff password for the first time.
+ usage: /register
+ permission: addyauth.register
+
+ login:
+ description: Login using your staff password to unlock yourself.
+ usage: /login
+ permission: addyauth.login
+
+ lock:
+ description: Lock your own account until you login again.
+ usage: /lock
+ permission: addyauth.lock
+
+ addyauth:
+ description: Admin command for AddyAuth (reload, future diagnostics, etc.)
+ usage: /addyauth reload
+ permission: addyauth.admin
+
+permissions:
+ addyauth.*:
+ description: Gives access to all AddyAuth commands.
+ children:
+ addyauth.register: true
+ addyauth.login: true
+ addyauth.lock: true
+ addyauth.admin: true
+
+ addyauth.register:
+ description: Allows staff to register their password.
+ default: op
+
+ addyauth.login:
+ description: Allows staff to login to unlock themselves.
+ default: op
+
+ addyauth.lock:
+ description: Allows staff to lock their account.
+ default: op
+
+ addyauth.admin:
+ description: Grants access to admin commands and update notifications.
+ default: op
diff --git a/target/classes/plugin.yml b/target/classes/plugin.yml
new file mode 100644
index 0000000..9842291
--- /dev/null
+++ b/target/classes/plugin.yml
@@ -0,0 +1,53 @@
+name: AddyAuthReborn
+version: 0.1-pre
+main: com.adzel.addyauthreborn.AddyAuthReborn
+api-version: 1.20
+author: AdzelFirestar
+description: >
+ A secure authentication system for staff accounts, requiring a password before access to movement, interaction, and chat.
+
+commands:
+ register:
+ description: Register your staff password for the first time.
+ usage: /register
+ permission: addyauth.register
+
+ login:
+ description: Login using your staff password to unlock yourself.
+ usage: /login
+ permission: addyauth.login
+
+ lock:
+ description: Lock your own account until you login again.
+ usage: /lock
+ permission: addyauth.lock
+
+ addyauth:
+ description: Admin command for AddyAuth (reload, future diagnostics, etc.)
+ usage: /addyauth reload
+ permission: addyauth.admin
+
+permissions:
+ addyauth.*:
+ description: Gives access to all AddyAuth commands.
+ children:
+ addyauth.register: true
+ addyauth.login: true
+ addyauth.lock: true
+ addyauth.admin: true
+
+ addyauth.register:
+ description: Allows staff to register their password.
+ default: op
+
+ addyauth.login:
+ description: Allows staff to login to unlock themselves.
+ default: op
+
+ addyauth.lock:
+ description: Allows staff to lock their account.
+ default: op
+
+ addyauth.admin:
+ description: Grants access to admin commands and update notifications.
+ default: op
diff --git a/target/maven-archiver/pom.properties b/target/maven-archiver/pom.properties
new file mode 100644
index 0000000..15e8c9b
--- /dev/null
+++ b/target/maven-archiver/pom.properties
@@ -0,0 +1,3 @@
+artifactId=AddyAuthReborn
+groupId=com.adzel.addyauthreborn
+version=0.1-pre
diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
new file mode 100644
index 0000000..c95763a
--- /dev/null
+++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
@@ -0,0 +1,16 @@
+com\adzel\addyauthreborn\lock\AFKMonitor$1.class
+com\adzel\addyauthreborn\commands\LoginCommand.class
+com\adzel\addyauthreborn\listeners\ConnectionListener.class
+com\adzel\addyauthreborn\utils\DatabaseUtil.class
+com\adzel\addyauthreborn\auth\AuthStorage.class
+com\adzel\addyauthreborn\commands\LockCommand.class
+com\adzel\addyauthreborn\lock\AFKMonitor.class
+com\adzel\addyauthreborn\commands\AddyAuthCommand.class
+com\adzel\addyauthreborn\commands\RegisterCommand.class
+com\adzel\addyauthreborn\auth\AuthManager.class
+com\adzel\addyauthreborn\listeners\LockListeners.class
+com\adzel\addyauthreborn\AddyAuthReborn.class
+com\adzel\addyauthreborn\listeners\AdminJoinListener.class
+com\adzel\addyauthreborn\utils\MessageUtil.class
+com\adzel\addyauthreborn\updates\UpdateChecker.class
+com\adzel\addyauthreborn\lock\OfflineLockManager.class
diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
new file mode 100644
index 0000000..7ec1f85
--- /dev/null
+++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
@@ -0,0 +1,15 @@
+C:\Users\theon\OneDrive\Desktop\AddyAuth Reborn\src\main\java\com\adzel\addyauthreborn\AddyAuthReborn.java
+C:\Users\theon\OneDrive\Desktop\AddyAuth Reborn\src\main\java\com\adzel\addyauthreborn\listeners\AdminJoinListener.java
+C:\Users\theon\OneDrive\Desktop\AddyAuth Reborn\src\main\java\com\adzel\addyauthreborn\listeners\LockListeners.java
+C:\Users\theon\OneDrive\Desktop\AddyAuth Reborn\src\main\java\com\adzel\addyauthreborn\listeners\ConnectionListener.java
+C:\Users\theon\OneDrive\Desktop\AddyAuth Reborn\src\main\java\com\adzel\addyauthreborn\commands\LockCommand.java
+C:\Users\theon\OneDrive\Desktop\AddyAuth Reborn\src\main\java\com\adzel\addyauthreborn\lock\OfflineLockManager.java
+C:\Users\theon\OneDrive\Desktop\AddyAuth Reborn\src\main\java\com\adzel\addyauthreborn\utils\MessageUtil.java
+C:\Users\theon\OneDrive\Desktop\AddyAuth Reborn\src\main\java\com\adzel\addyauthreborn\commands\LoginCommand.java
+C:\Users\theon\OneDrive\Desktop\AddyAuth Reborn\src\main\java\com\adzel\addyauthreborn\updates\UpdateChecker.java
+C:\Users\theon\OneDrive\Desktop\AddyAuth Reborn\src\main\java\com\adzel\addyauthreborn\commands\AddyAuthCommand.java
+C:\Users\theon\OneDrive\Desktop\AddyAuth Reborn\src\main\java\com\adzel\addyauthreborn\utils\DatabaseUtil.java
+C:\Users\theon\OneDrive\Desktop\AddyAuth Reborn\src\main\java\com\adzel\addyauthreborn\auth\AuthStorage.java
+C:\Users\theon\OneDrive\Desktop\AddyAuth Reborn\src\main\java\com\adzel\addyauthreborn\auth\AuthManager.java
+C:\Users\theon\OneDrive\Desktop\AddyAuth Reborn\src\main\java\com\adzel\addyauthreborn\commands\RegisterCommand.java
+C:\Users\theon\OneDrive\Desktop\AddyAuth Reborn\src\main\java\com\adzel\addyauthreborn\lock\AFKMonitor.java
diff --git a/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst b/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst
new file mode 100644
index 0000000..e69de29
diff --git a/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst
new file mode 100644
index 0000000..e69de29