Skip to content

Commit ce8bcd2

Browse files
committed
implement bazaar add command with transaction locking and item return logic
1 parent 2aae1c9 commit ce8bcd2

7 files changed

Lines changed: 247 additions & 163 deletions

File tree

‎bukkit/src/main/java/net/leaderos/plugin/configuration/Language.java‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -624,8 +624,13 @@ public static class Bazaar extends OkaeriConfig {
624624

625625
/**
626626
* Add item lore
627+
* /bazaar add komutu ile eklemesini iste
627628
*/
628-
private List<String> addItemLore = Arrays.asList("", "&aClick to add item!");
629+
private List<String> addItemLore = Arrays.asList(
630+
"&7To add an item to bazaar storage,",
631+
"&7use &e/bazaar add &7while holding",
632+
"&7the item in your main hand."
633+
);
629634

630635
/**
631636
* Add item gui name
@@ -638,6 +643,11 @@ public static class Bazaar extends OkaeriConfig {
638643
* return item message
639644
*/
640645
private String returnItemMessage = "{prefix} &cYou have reached max storage amount &8(&4%max_amount%&8) &creturned &e%amount% &citem.";
646+
647+
/**
648+
* no item in hand message
649+
*/
650+
private String noItemInHandMessage = "{prefix} &cYou must have an item in your main hand to add to bazaar storage.";
641651
}
642652

643653
/**

‎bukkit/src/main/java/net/leaderos/plugin/configuration/lang/en.java‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -626,7 +626,11 @@ public static class Bazaar extends Language.Gui.Bazaar {
626626
/**
627627
* Add item lore
628628
*/
629-
private List<String> addItemLore = Arrays.asList("", "&aClick to add item!");
629+
private List<String> addItemLore = Arrays.asList(
630+
"&7To add an item to bazaar storage,",
631+
"&7use &e/bazaar add &7while holding",
632+
"&7the item in your main hand."
633+
);
630634

631635
/**
632636
* Add item gui name
@@ -639,6 +643,11 @@ public static class Bazaar extends Language.Gui.Bazaar {
639643
* return item message
640644
*/
641645
private String returnItemMessage = "{prefix} &cYou have reached max storage amount &8(&4%max_amount%&8) &creturned &e%amount% &citem.";
646+
647+
/**
648+
* no item in hand message
649+
*/
650+
private String noItemInHandMessage = "{prefix} &cYou must have an item in your main hand to add to bazaar storage.";
642651
}
643652

644653
/**

‎bukkit/src/main/java/net/leaderos/plugin/configuration/lang/tr.java‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -624,7 +624,11 @@ public static class Bazaar extends Language.Gui.Bazaar {
624624
/**
625625
* Add item lore
626626
*/
627-
private List<String> addItemLore = Arrays.asList("", "&aTıkla ve eşya ekle!");
627+
private List<String> addItemLore = Arrays.asList(
628+
"&7Pazar deposuna item eklemek için",
629+
"&7itemi elinde elinde tutarken",
630+
"&e/pazar ekle &7komutunu kullan."
631+
);
628632

629633
/**
630634
* Add item gui name
@@ -637,6 +641,11 @@ public static class Bazaar extends Language.Gui.Bazaar {
637641
* return item message
638642
*/
639643
private String returnItemMessage = "{prefix} &cDepo sınırına ulaştın, en fazla &8(&4%max_amount%&8) &ceşya ekleyebilirsin. &e%amount% &cadet eşya iade edildi.";
644+
645+
/**
646+
* no item in hand message
647+
*/
648+
private String noItemInHandMessage = "{prefix} &cPazar deposuna ekleyeceğin itemi elinde tutmalısın!";
640649
}
641650

642651
/**
Lines changed: 185 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,40 @@
11
package net.leaderos.plugin.modules.bazaar.commands;
22

3+
import com.cryptomorin.xseries.XMaterial;
34
import dev.triumphteam.cmd.bukkit.annotation.Permission;
45
import dev.triumphteam.cmd.core.BaseCommand;
56
import dev.triumphteam.cmd.core.annotation.Command;
67
import dev.triumphteam.cmd.core.annotation.Default;
8+
import dev.triumphteam.cmd.core.annotation.SubCommand;
79
import lombok.RequiredArgsConstructor;
810
import net.leaderos.plugin.Bukkit;
911
import net.leaderos.plugin.api.managers.ModuleManager;
1012
import net.leaderos.plugin.helpers.ChatUtil;
11-
import net.leaderos.plugin.modules.cache.model.User;
13+
import net.leaderos.plugin.helpers.GameUtil;
14+
import net.leaderos.plugin.helpers.ItemUtil;
15+
import net.leaderos.plugin.modules.bazaar.BazaarModule;
1216
import net.leaderos.plugin.modules.bazaar.gui.BazaarGui;
17+
import net.leaderos.plugin.modules.bazaar.helpers.BazaarHelper;
18+
import net.leaderos.plugin.modules.bazaar.model.PlayerBazaar;
19+
import net.leaderos.plugin.modules.cache.model.User;
20+
import net.leaderos.shared.helpers.RequestUtil;
21+
import net.leaderos.shared.model.Response;
22+
import net.leaderos.shared.model.request.impl.bazaar.AddBazaarItemRequest;
23+
import org.bukkit.Material;
1324
import org.bukkit.entity.Player;
25+
import org.bukkit.inventory.ItemStack;
26+
27+
import java.net.HttpURLConnection;
28+
import java.text.SimpleDateFormat;
29+
import java.util.Date;
30+
import java.util.List;
31+
import java.util.Set;
32+
import java.util.UUID;
33+
import java.util.concurrent.ConcurrentHashMap;
1434

1535
/**
16-
* bazaar commands
36+
* Bazaar commands
37+
*
1738
* @author poyrazinan
1839
* @since 1.0
1940
*/
@@ -22,16 +43,171 @@
2243
public class BazaarCommand extends BaseCommand {
2344

2445
/**
25-
* Default command of bazaar
46+
* Global in-flight lock — holds UUIDs of players currently inside an add-transaction.
47+
* A ConcurrentHashMap-backed Set gives thread-safe O(1) add/remove/contains.
48+
*
49+
* Dupe prevention strategy (layered):
50+
* 1. ADD_LOCK — per-player CAS lock, blocks concurrent invocations
51+
* (packet flooding, command spam, macro clients).
52+
* 2. RequestUtil — shared per-player cooldown prevents rapid successive calls.
53+
* 3. Immediate hand removal on the MAIN THREAD before going async. The item
54+
* only exists as an in-memory clone during the HTTP round-trip — nothing is
55+
* left in any inventory that could be duped by disconnecting or by replaying
56+
* inventory packets.
57+
* 4. Live storage-count check against the API so the cap cannot be bypassed by
58+
* racing two concurrent /bazaar add commands.
59+
* 5. On every failure path the item is returned to the player (or dropped at
60+
* their feet if the inventory is full), so nothing is ever silently lost.
61+
*/
62+
private static final Set<UUID> ADD_LOCK = ConcurrentHashMap.newKeySet();
63+
64+
// ─────────────────────────────────────────────────────────────────────────
65+
// Commands
66+
// ─────────────────────────────────────────────────────────────────────────
67+
68+
/**
69+
* Default command — opens the storage GUI.
70+
*
2671
* @param player executor
2772
*/
2873
@Default
2974
@Permission("leaderos.bazaar.open")
3075
public void defaultCommand(Player player) {
31-
if (ModuleManager.getModule("Bazaar").isEnabled())
32-
if (User.isPlayerAuthed(player))
33-
BazaarGui.showGui(player);
34-
else
35-
ChatUtil.sendMessage(player, Bukkit.getInstance().getLangFile().getMessages().getRegistrationRequired());
76+
if (!ModuleManager.getModule("Bazaar").isEnabled()) return;
77+
78+
if (User.isPlayerAuthed(player))
79+
BazaarGui.showGui(player);
80+
else
81+
ChatUtil.sendMessage(player, Bukkit.getInstance().getLangFile().getMessages().getRegistrationRequired());
82+
}
83+
84+
/**
85+
* Add subcommand — deposits the item in the player's main hand into bazaar storage.
86+
*
87+
* @param player executor
88+
*/
89+
@SubCommand(value = "add", alias = {"ekle"})
90+
@Permission("leaderos.bazaar.add")
91+
public void addCommand(Player player) {
92+
if (!ModuleManager.getModule("Bazaar").isEnabled()) return;
93+
94+
// Auth check
95+
if (!User.isPlayerAuthed(player)) {
96+
ChatUtil.sendMessage(player, Bukkit.getInstance().getLangFile().getMessages().getRegistrationRequired());
97+
return;
98+
}
99+
100+
// Per-player transaction lock (CAS)
101+
// Set.add() is atomic on ConcurrentHashMap; returns false if already present.
102+
if (!ADD_LOCK.add(player.getUniqueId())) {
103+
ChatUtil.sendMessage(player, Bukkit.getInstance().getLangFile().getMessages().getHaveRequestOngoing());
104+
return;
105+
}
106+
107+
// Shared request-rate cooldown
108+
if (!RequestUtil.canRequest(player.getUniqueId())) {
109+
ADD_LOCK.remove(player.getUniqueId());
110+
ChatUtil.sendMessage(player, Bukkit.getInstance().getLangFile().getMessages().getHaveRequestOngoing());
111+
return;
112+
}
113+
114+
// Validate main-hand item (on main thread)
115+
ItemStack mainHand = player.getInventory().getItemInMainHand();
116+
117+
if (mainHand == null || mainHand.getType() == Material.AIR || mainHand.getAmount() == 0) {
118+
ADD_LOCK.remove(player.getUniqueId());
119+
ChatUtil.sendMessage(player, Bukkit.getInstance().getLangFile().getGui().getBazaarGui().getNoItemInHandMessage());
120+
return;
121+
}
122+
123+
// Resolve user & storage cap
124+
User user = User.getUser(player.getName());
125+
if (user == null) {
126+
ADD_LOCK.remove(player.getUniqueId());
127+
ChatUtil.sendMessage(player, Bukkit.getInstance().getLangFile().getMessages().getRegistrationRequired());
128+
return;
129+
}
130+
131+
int maxStorage = GameUtil.getAmountFromPerm(
132+
player,
133+
"bazaar.maxstorage.",
134+
Bukkit.getInstance().getModulesFile().getBazaar().getDefaultStorageSize()
135+
);
136+
137+
// Clone and immediately remove from hand
138+
// Critical dupe-prevention step: after this line the item no longer exists
139+
// in any inventory. Packet replays / inventory-open exploits cannot copy it.
140+
ItemStack itemToStore = mainHand.clone();
141+
player.getInventory().setItemInMainHand(null);
142+
player.updateInventory();
143+
144+
// Acquire shared cooldown only AFTER removal to minimise the lock window.
145+
RequestUtil.addRequest(player.getUniqueId());
146+
147+
// Async HTTP call
148+
Bukkit.getFoliaLib().getScheduler().runAsync((asyncTask) -> {
149+
try {
150+
int serverId = BazaarModule.getServerId();
151+
152+
// Live storage-count check (prevents race-condition cap bypass)
153+
List<PlayerBazaar> currentItems = PlayerBazaar.getBazaarStorage(user.getId());
154+
if (currentItems.size() >= maxStorage) {
155+
BazaarHelper.returnItemToPlayer(player, itemToStore);
156+
String msg = Bukkit.getInstance().getLangFile().getGui().getBazaarGui()
157+
.getReturnItemMessage()
158+
.replace("%max_amount%", String.valueOf(maxStorage))
159+
.replace("%amount%", "1");
160+
ChatUtil.sendMessage(player, msg);
161+
return;
162+
}
163+
164+
// Build API payload
165+
XMaterial material = XMaterial.matchXMaterial(itemToStore);
166+
String name = ItemUtil.getName(itemToStore);
167+
String lore = (itemToStore.hasItemMeta() && itemToStore.getItemMeta().hasLore())
168+
? String.join("\n", itemToStore.getItemMeta().getLore())
169+
: null;
170+
int amount = itemToStore.getAmount();
171+
int maxDurability = itemToStore.getType().getMaxDurability();
172+
int durability = ItemUtil.getDurability(itemToStore, maxDurability);
173+
String base64 = ItemUtil.toBase64(itemToStore);
174+
String creationDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
175+
String modelId = ItemUtil.getModelId(itemToStore);
176+
String enchants = ItemUtil.getEnchantments(itemToStore);
177+
178+
Response response = new AddBazaarItemRequest(
179+
user.getId(), name, lore, amount, maxDurability,
180+
durability, base64, 0.0, creationDate, modelId,
181+
enchants, serverId, material.name()
182+
).getResponse();
183+
184+
// Handle API result
185+
if (response.getResponseCode() == HttpURLConnection.HTTP_OK
186+
&& response.getResponseMessage().getBoolean("status")) {
187+
188+
// Success — notify player
189+
String msg = Bukkit.getInstance().getLangFile().getGui().getBazaarGui()
190+
.getAddItemMessage()
191+
.replace("%item_name%", name);
192+
ChatUtil.sendMessage(player, msg);
193+
194+
} else {
195+
// API rejected, return item
196+
BazaarHelper.returnItemToPlayer(player, itemToStore);
197+
ChatUtil.sendMessage(player, Bukkit.getInstance().getLangFile().getMessages().getPlayerNotAvailable());
198+
}
199+
200+
} catch (Exception e) {
201+
e.printStackTrace();
202+
// Safety net: always return the item on unexpected exceptions
203+
BazaarHelper.returnItemToPlayer(player, itemToStore);
204+
ChatUtil.sendMessage(player, Bukkit.getInstance().getLangFile().getMessages().getPlayerNotAvailable());
205+
206+
} finally {
207+
// Release both locks unconditionally
208+
RequestUtil.invalidate(player.getUniqueId());
209+
ADD_LOCK.remove(player.getUniqueId());
210+
}
211+
});
36212
}
37-
}
213+
}

0 commit comments

Comments
 (0)