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
10 changes: 9 additions & 1 deletion doc/features/Features.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
# Handling Professions
## General Configuration

The queue supports instant collection for recipes that do not have a crafting delay:

```yaml
instant_collect: false
```

Set `instant_collect` to `true` to collect recipes with a `craftingTime` less than or equal to zero immediately from the recipe GUI.

## Browsing
### Usage
You open the browse gui with the command `/craft browse`.
Expand Down Expand Up @@ -808,4 +816,4 @@ recipes:

### Additionals
- Setting the permission `fusion.browse` to false in your permission handler disabled the browse command for the player. If you dont set it at all, its natively active. You can also look for further permissions under the [Permissions](Permissions) page.
- Make sure to visit [Customizing Sections](Customizations) for further knowledge about the ItemBuilder, Cost-Section and Condition-Section of professions and even recipes!
- Make sure to visit [Customizing Sections](Customizations) for further knowledge about the ItemBuilder, Cost-Section and Condition-Section of professions and even recipes!
15 changes: 15 additions & 0 deletions doc/persistence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Player-state persistence

Recipe limits, profession state, and auto-crafting settings are read from SQL when requested. Changes are written immediately. Experience and recipe-limit increments use SQL arithmetic rather than replacing a value loaded earlier in the session. Normal logout no longer writes a player-state snapshot.

Crafting queues retain live objects for Bukkit scheduling and GUI references. Enqueue inserts the row immediately and retains its generated ID. The active item is checkpointed every second; waiting items do not require a write every second. Failed checkpoints remain pending for retry while the queue is loaded. Logout completes its final checkpoints synchronously before unloading, and saving an online player does not cancel their queue task.

Reload preserves the persisted crafting duration, progress, paid experience cost, and insertion order. Completed items remain completed. Offline time is distributed sequentially from the first unfinished item's checkpoint, then the resulting progress is saved so another rejoin cannot apply the same interval again. Disabling offline progress leaves saved progress unchanged.

Collecting a queued craft deletes its completed row and consumes its crafting limit in one database transaction. A failed transaction leaves the row available. Rewards are delivered only after that transaction succeeds. Cancellation similarly requires a successful deletion before refunding. Bukkit inventory changes and commands cannot be part of the SQL transaction: a process crash after commit but before reward delivery remains a delivery-loss window.

Calculated recipe eligibility is no longer shared through the global GUI cache. Rendering and craft attempts evaluate current requirements. Static recipe configuration and live queue objects still remain in memory; this is not a mechanism for synchronizing active queues between multiple servers.

The direct SQL calls run on the server thread where Bukkit state is involved. Indexed lookups and active-item checkpoints limit unnecessary work, but database latency now directly affects those operations. MySQL/MariaDB load testing is needed before rolling out to a busy remote-database installation. Existing corrupted values are not reconstructed by this change.

Regression coverage uses temporary SQLite databases and includes repeated saves, persisted durations/costs/timestamps, queue order and category matching, completed reloads, permanent limits and cooldown expiry, stale-object increments, atomic claims and rollback, profession writes, and offline progression enabled/disabled. It does not substitute for a live Minecraft or MySQL/MariaDB integration test.
9 changes: 9 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@
</repositories>

<dependencies>
<dependency>
<groupId>com.github.MilkBowl</groupId>
<artifactId>VaultAPI</artifactId>
<version>1.7.1</version>
<scope>test</scope>
<exclusions>
<exclusion><groupId>*</groupId><artifactId>*</artifactId></exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>studio.magemonkey</groupId>
<artifactId>codex</artifactId>
Expand Down
45 changes: 18 additions & 27 deletions src/main/java/studio/magemonkey/fusion/Fusion.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,12 @@ public void reloadConfig() {
hookManager = new HookManager();

Cfg.init();
Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
ProfessionsCfg.init();
EditorRegistry.reload();
SQLManager.init();
BrowseConfig.load();
ShowRecipesCfg.load();
DivinityService.init();
});
ProfessionsCfg.init();
EditorRegistry.reload();
SQLManager.init();
BrowseConfig.load();
ShowRecipesCfg.load();
DivinityService.init();
}

@Override
Expand Down Expand Up @@ -118,12 +116,10 @@ public void onLoad() {
public void onEnable() {
super.onEnable();
this.reloadConfig();
Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
Fusion.getInstance().getLogger().info("Attempting to migrate data into SQL [ExperienceManager].");
ExperienceManager.migrateIntoSQL();
Fusion.getInstance().getLogger().info("Attempting to migrate data into SQL [PConfigManager].");
PConfigManager.migrateIntoSQL();
});
Fusion.getInstance().getLogger().info("Attempting to migrate data into SQL [ExperienceManager].");
ExperienceManager.migrateIntoSQL();
Fusion.getInstance().getLogger().info("Attempting to migrate data into SQL [PConfigManager].");
PConfigManager.migrateIntoSQL();
LevelFunction.generate(200);
this.getCommand("craft").setExecutor(new Commands());
this.getCommand("fusion-editor").setExecutor(new FusionEditorCommand());
Expand Down Expand Up @@ -178,7 +174,9 @@ public double getPlayerCooldown(Player player) {
private void notifyForQueue(Player player) {
int finishedQueueAmount = PlayerLoader.getPlayer(player.getUniqueId()).getFinishedSize();
if (finishedQueueAmount > 0) {
Cfg.notifyForQueue(player, finishedQueueAmount);
Cfg.notifyForQueue(player,
finishedQueueAmount,
PlayerLoader.getPlayer(player.getUniqueId()).getFinishedOutputAmount());
}
}

Expand All @@ -190,17 +188,10 @@ private void runQueueTask() {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
PlayerLoader.getPlayerBlocking(player, 5000); // Wait up to 5s for any pending saves to finish
Bukkit.getScheduler().runTask(this, () -> {
if(!Cfg.autoJoinProfessions.isEmpty()) {
Cfg.autoJoinProfessions(player);
}
if (Cfg.craftingQueue) {
notifyForQueue(player);
}
});
});
// Player/queue creation touches Bukkit state and must run on the server thread.
PlayerLoader.getPlayer(player);
if (!Cfg.autoJoinProfessions.isEmpty()) Cfg.autoJoinProfessions(player);
if (Cfg.craftingQueue) notifyForQueue(player);
}

@EventHandler
Expand All @@ -211,4 +202,4 @@ public void onPlayerQuit(PlayerQuitEvent event) {
public static void registerListener(Listener listener) {
Bukkit.getPluginManager().registerEvents(listener, Fusion.getInstance());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public class FusionEvent extends Event implements Cancellable {
* The fusion player instance of the player
*/
private final FusionPlayer fusionPlayer;
private boolean cancelled;

/**
* Constructor for the FusionEvent
Expand All @@ -49,11 +50,12 @@ public FusionEvent(String professionName, CraftingTable craftingTable, Player pl

@Override
public boolean isCancelled() {
return false;
return cancelled;
}

@Override
public void setCancelled(boolean b) {
cancelled = b;
}

@NotNull
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import studio.magemonkey.fusion.cfg.ProfessionsCfg;
import studio.magemonkey.fusion.data.queue.CraftingQueue;
import studio.magemonkey.fusion.data.queue.QueueItem;
import studio.magemonkey.fusion.util.RecipeAmounts;

@Getter
public class QueueItemAddedEvent extends FusionEvent {
Expand All @@ -17,6 +18,10 @@ public class QueueItemAddedEvent extends FusionEvent {
* The queue item
*/
private final QueueItem queueItem;
/**
* The number of recipe executions represented by this event.
*/
private final int recipeAmount;

/**
* Constructor for the QueueItemAddedEvent
Expand All @@ -27,8 +32,21 @@ public class QueueItemAddedEvent extends FusionEvent {
* @param queueItem The queue item
*/
public QueueItemAddedEvent(String professionName, Player player, CraftingQueue queue, QueueItem queueItem) {
this(professionName, player, queue, queueItem, 1);
}

public QueueItemAddedEvent(String professionName,
Player player,
CraftingQueue queue,
QueueItem queueItem,
int recipeAmount) {
super(professionName, ProfessionsCfg.getTable(professionName), player);
this.queue = queue;
this.queueItem = queueItem;
this.recipeAmount = Math.max(0, recipeAmount);
}

public int getOutputAmount() {
return RecipeAmounts.outputAmount(queueItem.getRecipe()) * recipeAmount;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import studio.magemonkey.fusion.data.queue.CraftingQueue;
import studio.magemonkey.fusion.data.queue.QueueItem;
import studio.magemonkey.fusion.data.recipes.RecipeItem;
import studio.magemonkey.fusion.util.RecipeAmounts;

import java.util.List;

Expand All @@ -26,6 +27,10 @@ public class QueueItemFinishedEvent extends FusionEvent {
*/
@Setter
private List<RecipeItem> resultItems;
/**
* The number of recipe executions represented by this event.
*/
private final int recipeAmount;

/**
* Constructor for the QueueItemFinishedEvent
Expand All @@ -41,9 +46,23 @@ public QueueItemFinishedEvent(String professionName,
CraftingQueue queue,
QueueItem queueItem,
List<RecipeItem> resultItems) {
this(professionName, player, queue, queueItem, resultItems, 1);
}

public QueueItemFinishedEvent(String professionName,
Player player,
CraftingQueue queue,
QueueItem queueItem,
List<RecipeItem> resultItems,
int recipeAmount) {
super(professionName, ProfessionsCfg.getTable(professionName), player);
this.queue = queue;
this.queueItem = queueItem;
this.resultItems = resultItems;
this.recipeAmount = Math.max(0, recipeAmount);
}

public int getOutputAmount() {
return RecipeAmounts.outputAmount(queueItem.getRecipe(), resultItems) * recipeAmount;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import studio.magemonkey.fusion.cfg.ProfessionsCfg;
import studio.magemonkey.fusion.data.queue.CraftingQueue;
import studio.magemonkey.fusion.data.queue.QueueItem;
import studio.magemonkey.fusion.util.RecipeAmounts;

import java.util.List;

Expand All @@ -29,6 +30,10 @@ public class QueueItemRemovedEvent extends FusionEvent {
* Whether the item was refunded
*/
private final boolean refunded;
/**
* The number of completed recipe executions represented by this event.
*/
private final int recipeAmount;
/**
* The refunded items in case `refunded` is `true`
*/
Expand All @@ -53,11 +58,27 @@ public QueueItemRemovedEvent(String professionName,
boolean finished,
boolean refunded,
List<ItemStack> refundedItems) {
this(professionName, player, queue, queueItem, finished, refunded, refundedItems, finished ? 1 : 0);
}

public QueueItemRemovedEvent(String professionName,
Player player,
CraftingQueue queue,
QueueItem queueItem,
boolean finished,
boolean refunded,
List<ItemStack> refundedItems,
int recipeAmount) {
super(professionName, ProfessionsCfg.getTable(professionName), player);
this.queue = queue;
this.queueItem = queueItem;
this.finished = finished;
this.refunded = refunded;
this.refundedItems = refundedItems;
this.recipeAmount = finished ? Math.max(0, recipeAmount) : 0;
}

public int getOutputAmount() {
return RecipeAmounts.outputAmount(queueItem.getRecipe()) * recipeAmount;
}
}
Loading