diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..d1a1fdc
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,32 @@
+name: Build SupremeCore
+
+on:
+ push:
+ branches:
+ - main
+ - feat/**
+ pull_request:
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up Java 17
+ uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: '17'
+ cache: maven
+
+ - name: Build with Maven
+ run: mvn -B clean package
+
+ - name: Upload build artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: SupremeCore-jars
+ path: target/*.jar
diff --git a/pom.xml b/pom.xml
index acbcb5c..0a1667e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -122,16 +122,10 @@
7.0.6-SNAPSHOT
provided
-
- me.blackvein.quests
- quests-main
- 4.0.2
- provided
-
me.clip
placeholderapi
- 2.10.9
+ 2.12.2
provided
diff --git a/src/main/java/net/supremesurvival/supremecore/SupremeCore.java b/src/main/java/net/supremesurvival/supremecore/SupremeCore.java
index a3dd486..e0f6fb3 100644
--- a/src/main/java/net/supremesurvival/supremecore/SupremeCore.java
+++ b/src/main/java/net/supremesurvival/supremecore/SupremeCore.java
@@ -15,6 +15,7 @@
import net.supremesurvival.supremecore.tomes.TomesCommand;
import net.supremesurvival.supremecore.mobUtils.HorseInfo;
import net.supremesurvival.supremecore.sanguine.Vampire;
+import net.supremesurvival.supremecore.realestate.RealEstateCommand;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.event.Listener;
@@ -47,6 +48,7 @@ public void onEnable() {
this.getCommand("HorseInfo").setExecutor(new HorseInfo());
this.getCommand("Landmarks").setExecutor(new LandmarkCommand());
this.getCommand("Vampire").setExecutor(vampire);
+ this.getCommand("RealEstate").setExecutor(new RealEstateCommand());
Morality.enable();
this.getServer().getPluginManager().registerEvents(new Morality(), this);
this.getServer().getPluginManager().registerEvents(vampire, this);
diff --git a/src/main/java/net/supremesurvival/supremecore/landmarks/LandmarkManager.java b/src/main/java/net/supremesurvival/supremecore/landmarks/LandmarkManager.java
index 2dff922..23fd094 100644
--- a/src/main/java/net/supremesurvival/supremecore/landmarks/LandmarkManager.java
+++ b/src/main/java/net/supremesurvival/supremecore/landmarks/LandmarkManager.java
@@ -4,7 +4,7 @@
import com.sk89q.worldguard.WorldGuard;
import com.sk89q.worldguard.protection.regions.ProtectedRegion;
import com.sk89q.worldguard.protection.regions.RegionContainer;
-import com.sk89q.worldguard.protection.regions.RegionManager;
+import com.sk89q.worldguard.protection.managers.RegionManager;
import net.supremesurvival.supremecore.commonUtils.Logger;
import net.supremesurvival.supremecore.commonUtils.fileHandler.ConfigUtility;
import net.supremesurvival.supremecore.commonUtils.fileHandler.FileHandler;
diff --git a/src/main/java/net/supremesurvival/supremecore/realestate/RealEstateCommand.java b/src/main/java/net/supremesurvival/supremecore/realestate/RealEstateCommand.java
new file mode 100644
index 0000000..af4bd6b
--- /dev/null
+++ b/src/main/java/net/supremesurvival/supremecore/realestate/RealEstateCommand.java
@@ -0,0 +1,118 @@
+package net.supremesurvival.supremecore.realestate;
+
+import org.bukkit.ChatColor;
+import org.bukkit.Location;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+import java.util.Locale;
+
+public class RealEstateCommand implements CommandExecutor {
+ private static final int PAGE_SIZE = 8;
+
+ private final RealEstateManager manager = new RealEstateManager();
+
+ @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 (args.length == 0) {
+ sendHelp(player);
+ return true;
+ }
+
+ if (args[0].equalsIgnoreCase("list")) {
+ String townFilter = null;
+ int page = 1;
+
+ if (args.length >= 2) {
+ if (isInteger(args[1])) {
+ page = Math.max(1, Integer.parseInt(args[1]));
+ } else {
+ townFilter = args[1];
+ }
+ }
+
+ if (args.length >= 3 && isInteger(args[2])) {
+ page = Math.max(1, Integer.parseInt(args[2]));
+ }
+
+ List listings = manager.getListings(townFilter);
+ if (listings.isEmpty()) {
+ player.sendMessage(ChatColor.GRAY + "No for-sale plots found" + (townFilter != null ? " for town filter '" + townFilter + "'" : "") + ".");
+ return true;
+ }
+
+ int totalPages = (int) Math.ceil((double) listings.size() / PAGE_SIZE);
+ page = Math.min(page, Math.max(totalPages, 1));
+ int start = (page - 1) * PAGE_SIZE;
+ int end = Math.min(start + PAGE_SIZE, listings.size());
+
+ player.sendMessage(ChatColor.GOLD + "=== Real Estate Listings (" + page + "/" + totalPages + ") ===");
+ for (int i = start; i < end; i++) {
+ RealEstateListing l = listings.get(i);
+ player.sendMessage(
+ ChatColor.YELLOW + "#" + l.id() + ChatColor.DARK_GRAY + " • "
+ + ChatColor.AQUA + l.townName()
+ + ChatColor.DARK_GRAY + " • "
+ + ChatColor.GRAY + l.worldName()
+ + ChatColor.DARK_GRAY + " @ "
+ + ChatColor.WHITE + l.centerX() + ", " + l.centerZ()
+ + ChatColor.DARK_GRAY + " • "
+ + ChatColor.GREEN + "$" + String.format(Locale.US, "%.2f", l.price())
+ );
+ }
+
+ player.sendMessage(ChatColor.GRAY + "Use /realestate view to teleport for viewing.");
+ return true;
+ }
+
+ if (args[0].equalsIgnoreCase("view")) {
+ if (args.length < 2 || !isInteger(args[1])) {
+ player.sendMessage(ChatColor.RED + "Usage: /realestate view ");
+ return true;
+ }
+
+ int id = Integer.parseInt(args[1]);
+ RealEstateListing listing = manager.getListingById(id);
+ if (listing == null) {
+ player.sendMessage(ChatColor.RED + "Listing #" + id + " not found. Use /realestate list first.");
+ return true;
+ }
+
+ Location target = manager.resolveTeleportLocation(listing);
+ if (target == null) {
+ player.sendMessage(ChatColor.RED + "Could not resolve a safe viewing location for that listing.");
+ return true;
+ }
+
+ player.teleport(target);
+ player.sendMessage(ChatColor.GOLD + "Viewing listing #" + id + ChatColor.GRAY + " in " + ChatColor.AQUA + listing.townName());
+ return true;
+ }
+
+ sendHelp(player);
+ return true;
+ }
+
+ private void sendHelp(Player player) {
+ player.sendMessage(ChatColor.YELLOW + "/realestate list [town] [page]");
+ player.sendMessage(ChatColor.YELLOW + "/realestate view ");
+ }
+
+ private boolean isInteger(String s) {
+ try {
+ Integer.parseInt(s);
+ return true;
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/net/supremesurvival/supremecore/realestate/RealEstateDesign.md b/src/main/java/net/supremesurvival/supremecore/realestate/RealEstateDesign.md
new file mode 100644
index 0000000..fad83bf
--- /dev/null
+++ b/src/main/java/net/supremesurvival/supremecore/realestate/RealEstateDesign.md
@@ -0,0 +1,47 @@
+# Towny Real Estate Hook (Design)
+
+## Goal
+Expose Towny plots for sale via commands and provide safe teleport for plot viewing.
+
+## Command UX
+
+- `/realestate list [town] [page]`
+ - Lists for-sale plots from Towny.
+ - Output includes listingId, town, world, plot coords, and price.
+
+- `/realestate view `
+ - Teleports player to a safe viewing location for a selected listing.
+
+## Data Source
+
+Use Towny API as source of truth:
+
+- Town list from `TownyUniverse`
+- Plot sale state from `TownBlock` (`isForSale` / sale metadata)
+- Price from TownBlock sale value
+
+## Behavior
+
+- Cache listing snapshots for 10–30 seconds to reduce heavy scans.
+- Stable listing ids per refresh window so `view ` works predictably.
+- Restrict teleports by world denylist + cooldown + optional warmup.
+- Never bypass server protections (combat tag, region policy, etc.).
+
+## Config Additions (planned)
+
+```yaml
+realestate:
+ enabled: true
+ list-page-size: 8
+ cache-seconds: 20
+ view-cooldown-seconds: 15
+ view-warmup-seconds: 0
+ allowed-worlds: []
+```
+
+## Follow-up Tasks
+
+1. Add `RealEstateManager` to gather and cache Towny listings.
+2. Wire command responses to real listing data.
+3. Add teleport safety resolver (surface-safe location).
+4. Add permissions and polish formatting.
diff --git a/src/main/java/net/supremesurvival/supremecore/realestate/RealEstateListing.java b/src/main/java/net/supremesurvival/supremecore/realestate/RealEstateListing.java
new file mode 100644
index 0000000..3d893e0
--- /dev/null
+++ b/src/main/java/net/supremesurvival/supremecore/realestate/RealEstateListing.java
@@ -0,0 +1,11 @@
+package net.supremesurvival.supremecore.realestate;
+
+public record RealEstateListing(
+ int id,
+ String townName,
+ String worldName,
+ int centerX,
+ int centerZ,
+ double price
+) {
+}
diff --git a/src/main/java/net/supremesurvival/supremecore/realestate/RealEstateManager.java b/src/main/java/net/supremesurvival/supremecore/realestate/RealEstateManager.java
new file mode 100644
index 0000000..566243c
--- /dev/null
+++ b/src/main/java/net/supremesurvival/supremecore/realestate/RealEstateManager.java
@@ -0,0 +1,163 @@
+package net.supremesurvival.supremecore.realestate;
+
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.World;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Reflection-based Towny listing adapter so we can iterate quickly across Towny API versions.
+ */
+public class RealEstateManager {
+
+ private static final long CACHE_MS = 20_000L;
+
+ private List cachedListings = new ArrayList<>();
+ private long lastRefresh = 0L;
+
+ public List getListings(String townFilter) {
+ if (System.currentTimeMillis() - lastRefresh > CACHE_MS || cachedListings.isEmpty()) {
+ cachedListings = fetchTownyListings();
+ lastRefresh = System.currentTimeMillis();
+ }
+
+ if (townFilter == null || townFilter.isBlank()) {
+ return cachedListings;
+ }
+
+ String needle = townFilter.toLowerCase(Locale.ROOT);
+ List out = new ArrayList<>();
+ for (RealEstateListing listing : cachedListings) {
+ if (listing.townName().toLowerCase(Locale.ROOT).contains(needle)) {
+ out.add(listing);
+ }
+ }
+ return out;
+ }
+
+ public RealEstateListing getListingById(int id) {
+ for (RealEstateListing listing : getListings(null)) {
+ if (listing.id() == id) return listing;
+ }
+ return null;
+ }
+
+ public Location resolveTeleportLocation(RealEstateListing listing) {
+ World world = Bukkit.getWorld(listing.worldName());
+ if (world == null) return null;
+
+ int x = listing.centerX();
+ int z = listing.centerZ();
+ int y = world.getHighestBlockYAt(x, z) + 1;
+ return new Location(world, x + 0.5, y, z + 0.5);
+ }
+
+ @SuppressWarnings("unchecked")
+ private List fetchTownyListings() {
+ try {
+ Class> universeClass = Class.forName("com.palmergames.bukkit.towny.TownyUniverse");
+ Object universe = invokeStatic(universeClass, "getInstance");
+ List