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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [2.5.9] - 2026-07-07

### Added
- **Manual category default reseed command** — added `/shopadmin reseed [mode]` to restore missing bundled category defaults on demand without overwriting existing files. Omitting `mode` reseeds all bundled modes; providing a mode (e.g. `prison`, `smp`) reseeds only that mode.

### Changed
- **Bootstrap architecture cleanup** — startup and shutdown orchestration now lives in the `bootstrap` package (`EzShopsBootstrap`), while `EzShopsPlugin` is reduced to a thin lifecycle entry point (`onEnable`/`onDisable` delegation).
- **Runtime component access moved to registry** — component/debug/reseed accessors are now provided by `EzShopsRegistry`, removing lifecycle/state getters from `EzShopsPlugin`.

### Fixed
- **Default category file persistence across restarts** — bundled category defaults are now seeded per mode directory (`shop/{mode}/categories/*.yml`) and are no longer recreated automatically for existing mode directories when server owners intentionally delete them.
- **Mode support for default seeding** — bundled shop resource discovery now works across available packaged modes (including future bundled modes), rather than relying on prison-only default category handling.

## [2.5.8] - 2026-07-06

### Fixed
Expand Down
10 changes: 10 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ Opens the admin moderation GUI for inspecting and removing any active player sho
/shopadmin
/shopadmin browse
/shopadmin market
/shopadmin reseed [mode]
```
**Permission:** `ezshops.shop.admin`
**Aliases:** None
Expand All @@ -242,6 +243,15 @@ Switches the browse GUI to the **Team Market** view, showing all active team mar
**Permission:** `ezshops.shop.admin`
**Example:** `/shopadmin market`

##### `/shopadmin reseed [mode]`
Restores missing bundled category defaults without overwriting existing category files.

- No argument: reseeds all bundled modes.
- With `mode`: reseeds only that mode (for example `prison` or `smp`).

**Permission:** `ezshops.shop.admin`
**Examples:** `/shopadmin reseed`, `/shopadmin reseed smp`

---

### Sign Shop Setup
Expand Down
2 changes: 1 addition & 1 deletion docs/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ EzShops uses a hierarchical permission system. Permissions are organized into ca

**Details:**
- `ezshops.reload` - Allows reloading all shop configurations, menus, categories, and pricing without server restart
- `ezshops.shop.admin` - Opens the admin browse GUI to inspect and remove any player shop or team market listing
- `ezshops.shop.admin` - Opens the admin browse GUI to inspect and remove any player shop or team market listing, and allows `/shopadmin reseed [mode]` to restore missing bundled category defaults
- `ezshops.shop.admin.minionhead` - Bypasses normal restrictions on minion heads (usually crate-only items)

### Sign Shop Management
Expand Down
1 change: 1 addition & 0 deletions listings/modrinth-hangar.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ Check our latest project: [PvPIndex]([https://pvpindex.com/](https://modrinth.co
- `/sellhand`, `/sellinventory` – Quick-sell items or entire inventories.
- `/price <material>` – Check live buy/sell prices.
- `/shop reload` – Reload all configs instantly.
- `/shopadmin reseed [mode]` – Restore missing bundled category defaults (all modes or one mode) without overwriting existing files.
- `/stock buy <item> <amount>` – Buy stock market items at the current price.
- `/stock sell <item> <amount>` – Sell your stock market items.
- `/stock overview` – View all stock market items and their current prices.
Expand Down
1 change: 1 addition & 0 deletions listings/spigot.bbcode
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
[*][B]/sellhand & /sellinventory[/B]: Cash out specific stacks or an entire inventory in one click, with summary receipts.
[*][B]/price[/B]: Let players and staff quote live buy/sell totals using tab-complete across every configured material.
[*][B]/shop reload[/B]: Instantly reload all shop configuration files and update menus (permission [icode]ezshops.reload[/icode]).
[*][B]/shopadmin reseed [mode][/B]: Restore missing bundled category defaults for all modes or one specific mode without overwriting existing category files.
[/LIST]
[B][URL='https://ez-plugins.github.io/EzShops/commands']Full command reference →[/URL] · [URL='https://ez-plugins.github.io/EzShops/permissions']Permissions reference →[/URL][/B]

Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>com.skyblockexp</groupId>
<artifactId>ezshops</artifactId>
<version>2.5.8</version>
<version>2.5.9</version>
<name>EzShops Plugin</name>
<description>Standalone plugin providing the Skyblock shop command and sign shops.</description>
<packaging>jar</packaging>
Expand Down
225 changes: 8 additions & 217 deletions src/main/java/com/skyblockexp/ezshops/EzShopsPlugin.java
Original file line number Diff line number Diff line change
@@ -1,236 +1,27 @@
package com.skyblockexp.ezshops;

import com.skyblockexp.ezshops.api.EzShopsAPI;
import com.skyblockexp.ezshops.bootstrap.CoreShopComponent;
import com.skyblockexp.ezshops.bootstrap.MetricsComponent;
import com.skyblockexp.ezshops.bootstrap.PluginComponent;
import com.skyblockexp.ezshops.bootstrap.SignShopComponent;
import com.skyblockexp.ezshops.bootstrap.StockComponent;
import com.skyblockexp.ezshops.bootstrap.PlayerShopComponent;
import com.skyblockexp.ezshops.bootstrap.TeamShopComponent;
import com.skyblockexp.ezshops.boost.SellPriceBoostEffect;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
import net.milkbowl.vault.economy.Economy;
import org.bukkit.plugin.RegisteredServiceProvider;
import java.util.logging.Level;
import com.skyblockexp.ezshops.bootstrap.EzShopsBootstrap;
import org.bukkit.plugin.java.JavaPlugin;

/**
* Standalone plugin responsible for the /shop command and sign shops.
* Thin plugin entry point. Startup/shutdown orchestration lives in bootstrap.
*/
public class EzShopsPlugin extends JavaPlugin {
private static final List<String> DEFAULT_SHOP_RESOURCES = List.of(
"shop.yml",
"stock-gui.yml",
"shop/prison/menu.yml",
"shop/prison/categories/building.yml",
"shop/prison/categories/daily_specials.yml",
"shop/prison/categories/decorations.yml",
"shop/prison/categories/enchantments.yml",
"shop/prison/categories/farming.yml",
"shop/prison/categories/fishing.yml",
"shop/prison/categories/food.yml",
"shop/prison/categories/mining.yml",
"shop/prison/categories/mob_drops.yml",
"shop/prison/categories/redstone.yml",
"shop/prison/categories/spawners.yml",
"shop/prison/categories/valuables.yml",
"shop/prison/categories/wood.yml",
"shop/prison/rotations/daily-specials.yml",

// Add any bundled locale files here so they are copied alongside the defaults.
"messages/messages_en.yml",
"messages/messages_es.yml",
"messages/messages_nl.yml",
"messages/messages_zh.yml");

private Economy economy;
private List<PluginComponent> components;
private CoreShopComponent coreComponent;
private StockComponent stockComponent;
private TeamShopComponent teamShopComponent;
private PlayerShopComponent playerShopComponent;
private boolean debugMode;
private EzShopsBootstrap bootstrap;

@Override
public void onEnable() {
if (!setupEconomy()) {
getLogger().severe("Vault economy provider not found; disabling EzShops.");
getServer().getPluginManager().disablePlugin(this);
return;
}

saveDefaultResources();
saveDefaultConfig();

debugMode = getConfig().getBoolean("debug", false);

coreComponent = new CoreShopComponent(economy);
teamShopComponent = new TeamShopComponent(economy);
playerShopComponent = new PlayerShopComponent(economy, getConfig());
stockComponent = new StockComponent();
components = new ArrayList<>();
components.add(teamShopComponent); // MUST be first so services exist when CoreShopComponent.enable() runs
components.add(coreComponent);
components.add(stockComponent);
components.add(playerShopComponent);
components.add(new SignShopComponent(coreComponent));
components.add(new MetricsComponent());
components.add(new com.skyblockexp.ezshops.bootstrap.ShopAdminComponent(playerShopComponent, teamShopComponent));

try {
for (PluginComponent component : components) {
component.enable(this);
}
} catch (RuntimeException ex) {
getLogger().log(Level.SEVERE, "Failed to enable EzShops component", ex);
throw ex;
}

// Initialize the EzShops API after all components are enabled
try {
EzShopsAPI.initialize(this);
getLogger().info("EzShops API initialized successfully.");
} catch (Exception ex) {
getLogger().log(Level.WARNING, "Failed to initialize EzShops API", ex);
}

// Register EzBoost integration if EzBoost is present and integration is enabled
boolean ezboostIntegration = getConfig().getBoolean("ezboost-integration", true);
if (ezboostIntegration && getServer().getPluginManager().getPlugin("EzBoost") != null) {
try {
// Get the EzBoost plugin instance to access its class loader
org.bukkit.plugin.Plugin ezBoostPlugin = getServer().getPluginManager().getPlugin("EzBoost");

ClassLoader ezBoostClassLoader = ezBoostPlugin.getClass().getClassLoader();

// Check if EzBoost classes are available using the plugin's class loader
Class<?> ezBoostAPIClass = Class.forName("com.skyblockexp.ezboost.api.EzBoostAPI", true, ezBoostClassLoader);
Class<?> customBoostEffectClass = Class.forName("com.skyblockexp.ezboost.boost.CustomBoostEffect", true, ezBoostClassLoader);

Object boostEffect = SellPriceBoostEffect.create();
if (boostEffect != null) {
java.lang.reflect.Method registerMethod = ezBoostAPIClass.getMethod("registerCustomBoostEffect", customBoostEffectClass);
registerMethod.invoke(null, boostEffect);
getLogger().info("EzBoost sell price boost integration enabled.");
}
} catch (ClassNotFoundException e) {
getLogger().info("EzBoost classes not found, integration disabled: " + e.getMessage());
} catch (Exception ex) {
getLogger().log(Level.WARNING, "Failed to register EzBoost sell price boost effect", ex);
}
} else if (!ezboostIntegration) {
getLogger().info("EzBoost integration is disabled in config.");
if (bootstrap == null) {
bootstrap = new EzShopsBootstrap(this);
}

getLogger().info("EzShops plugin enabled.");
bootstrap.start();
}

@Override
public void onDisable() {
// Shutdown the API first
try {
EzShopsAPI.shutdown();
} catch (Exception ex) {
getLogger().log(Level.WARNING, "Error shutting down EzShops API", ex);
}

if (components != null) {
ListIterator<PluginComponent> iterator = components.listIterator(components.size());
while (iterator.hasPrevious()) {
PluginComponent component = iterator.previous();
try {
component.disable();
} catch (RuntimeException ex) {
getLogger().log(Level.SEVERE,
"Failed to disable EzShops component " + component.getClass().getSimpleName(), ex);
}
}
components = null;
}
economy = null;
getLogger().info("EzShops plugin disabled.");
}

public CoreShopComponent getCoreShopComponent() {
return this.coreComponent;
}

/** Alias used internally by TeamShopComponent during enable. */
public CoreShopComponent getCoreComponent() {
return this.coreComponent;
}

public TeamShopComponent getTeamShopComponent() {
return this.teamShopComponent;
}

public StockComponent getStockComponent() {
return this.stockComponent;
}

public PlayerShopComponent getPlayerShopComponent() {
return this.playerShopComponent;
}

/** Reloads feature components after toggling via admin GUI. */
public void reloadFeatures() {
playerShopComponent.reload();
stockComponent.reload();
}

public boolean isDebugMode() {
return this.debugMode;
}

private boolean setupEconomy() {
// First check if an Economy provider is already registered via ServicesManager.
RegisteredServiceProvider<Economy> registration =
getServer().getServicesManager().getRegistration(Economy.class);
if (registration != null) {
economy = registration.getProvider();
return economy != null;
}

// Fallback: ensure the Vault plugin is present (even if no provider registered yet).
if (getServer().getPluginManager().getPlugin("Vault") == null) {
return false;
}

return false;
}

private void saveDefaultResources() {
File dataFolder = getDataFolder();
if (!dataFolder.exists() && !dataFolder.mkdirs()) {
getLogger().warning("Unable to create plugin data folder for default EzShops configuration.");
return;
}

for (String resourcePath : DEFAULT_SHOP_RESOURCES) {
saveResourceIfAbsent(resourcePath);
}
}

private void saveResourceIfAbsent(String resourcePath) {
File destination = new File(getDataFolder(), resourcePath.replace('/', File.separatorChar));
if (destination.exists()) {
return;
}

File parent = destination.getParentFile();
if (parent != null && !parent.exists() && !parent.mkdirs()) {
getLogger().warning("Unable to create directory for default resource: " + parent.getAbsolutePath());
return;
}

try {
saveResource(resourcePath, false);
} catch (IllegalArgumentException ex) {
getLogger().warning("Missing packaged resource '" + resourcePath + "': " + ex.getMessage());
if (bootstrap != null) {
bootstrap.stop();
}
}
}
3 changes: 2 additions & 1 deletion src/main/java/com/skyblockexp/ezshops/api/EzShopsAPI.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.skyblockexp.ezshops.api;

import com.skyblockexp.ezshops.EzShopsPlugin;
import com.skyblockexp.ezshops.bootstrap.EzShopsRegistry;
import com.skyblockexp.ezshops.bootstrap.StockComponent;
import com.skyblockexp.ezshops.stock.StockMarketManager;
import com.skyblockexp.ezshops.shop.api.ShopPriceService;
Expand Down Expand Up @@ -58,7 +59,7 @@ private EzShopsAPI(EzShopsPlugin plugin) {
private void initializeAPIs() {
// Initialize StockAPI if the stock component is enabled
try {
StockComponent stockComponent = plugin.getStockComponent();
StockComponent stockComponent = EzShopsRegistry.current().getStockComponent();
if (stockComponent != null) {
StockMarketManager stockManager = stockComponent.getStockMarketManager();
if (stockManager != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ public void enable(EzShopsPlugin plugin) {

islandLevelProvider = createIslandLevelProvider(plugin);
ignoreIslandRequirements = islandLevelProvider == null;
if (ignoreIslandRequirements && plugin.isDebugMode()) {
if (ignoreIslandRequirements && EzShopsRegistry.current().isDebugMode()) {
plugin.getLogger().info(
"Island level provider not detected; island requirements will be ignored.");
}
Expand All @@ -150,18 +150,18 @@ public void enable(EzShopsPlugin plugin) {
shopMenu = new ShopMenu(plugin, pricingManager, transactionService, islandLevelProvider,
ignoreIslandRequirements, ShopMenu.DisplayMode.FLAT_LIST, guiMessages,
transactionMessages.restrictions());
if (plugin.isDebugMode()) {
if (EzShopsRegistry.current().isDebugMode()) {
plugin.getLogger().info("Shop categories are disabled; displaying all items in a single list.");
}
} else {
shopMenu = null;
if (plugin.isDebugMode()) {
if (EzShopsRegistry.current().isDebugMode()) {
plugin.getLogger().info("Shop categories are disabled; the /shop menu will be unavailable.");
}
}

shopCommand = new ShopCommand(plugin, pricingManager, transactionService, shopMenu, commandMessages.shop(),
transactionMessages.errors(), transactionMessages.restrictions(), plugin.isDebugMode());
transactionMessages.errors(), transactionMessages.restrictions(), EzShopsRegistry.current().isDebugMode());
sellHandCommand = new SellHandCommand(transactionService, pricingManager, commandMessages.sellHand());
sellInventoryCommand = new SellInventoryCommand(transactionService, commandMessages.sellInventory());
priceCommand = new PriceCommand(pricingManager, transactionService, commandMessages.price());
Expand Down
Loading
Loading