11package net .leaderos .plugin .modules .bazaar .commands ;
22
3+ import com .cryptomorin .xseries .XMaterial ;
34import dev .triumphteam .cmd .bukkit .annotation .Permission ;
45import dev .triumphteam .cmd .core .BaseCommand ;
56import dev .triumphteam .cmd .core .annotation .Command ;
67import dev .triumphteam .cmd .core .annotation .Default ;
8+ import dev .triumphteam .cmd .core .annotation .SubCommand ;
79import lombok .RequiredArgsConstructor ;
810import net .leaderos .plugin .Bukkit ;
911import net .leaderos .plugin .api .managers .ModuleManager ;
1012import 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 ;
1216import 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 ;
1324import 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 */
2243public 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