Skip to content
Merged
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
32 changes: 32 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -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
8 changes: 1 addition & 7 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -122,16 +122,10 @@
<version>7.0.6-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>me.blackvein.quests</groupId>
<artifactId>quests-main</artifactId>
<version>4.0.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>me.clip</groupId>
<artifactId>placeholderapi</artifactId>
<version>2.10.9</version>
<version>2.12.2</version>
<scope>provided</scope>
</dependency>
</dependencies>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<RealEstateListing> 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 <id> 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 <listingId>");
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 <listingId>");
}

private boolean isInteger(String s) {
try {
Integer.parseInt(s);
return true;
} catch (NumberFormatException e) {
return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -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 <listingId>`
- 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 <id>` 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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package net.supremesurvival.supremecore.realestate;

public record RealEstateListing(
int id,
String townName,
String worldName,
int centerX,
int centerZ,
double price
) {
}
Loading