fix(marketplace): avoid inventory desync on failed offer insert

Expose whether a marketplace offer was persisted before mutating inventory state, refuse sells whose database insert failed, and synchronize the sold timestamp into the online seller's in-memory offer when present. This keeps failed or racing marketplace operations from desynchronizing credits/items.
This commit is contained in:
simoleo89
2026-06-09 22:02:53 +02:00
parent 8161e3d7e5
commit 19cde45d3e
3 changed files with 29 additions and 2 deletions
@@ -279,8 +279,9 @@ public class MarketPlace {
return; return;
} }
int soldTimestamp = Emulator.getIntUnixTimestamp();
try (PreparedStatement updateOffer = connection.prepareStatement("UPDATE marketplace_items SET state = 2, sold_timestamp = ? WHERE id = ? AND state = 1")) { try (PreparedStatement updateOffer = connection.prepareStatement("UPDATE marketplace_items SET state = 2, sold_timestamp = ? WHERE id = ? AND state = 1")) {
updateOffer.setInt(1, Emulator.getIntUnixTimestamp()); updateOffer.setInt(1, soldTimestamp);
updateOffer.setInt(2, offerId); updateOffer.setInt(2, offerId);
int updated = updateOffer.executeUpdate(); int updated = updateOffer.executeUpdate();
if (updated == 0) { if (updated == 0) {
@@ -307,7 +308,11 @@ public class MarketPlace {
client.sendResponse(new MarketplaceBuyErrorComposer(MarketplaceBuyErrorComposer.REFRESH, 0, offerId, price)); client.sendResponse(new MarketplaceBuyErrorComposer(MarketplaceBuyErrorComposer.REFRESH, 0, offerId, price));
if (habbo != null) { if (habbo != null) {
habbo.getInventory().getOffer(offerId).setState(MarketPlaceState.SOLD); MarketPlaceOffer offer = habbo.getInventory().getOffer(offerId);
if (offer != null) {
offer.setState(MarketPlaceState.SOLD);
offer.setSoldTimestamp(soldTimestamp);
}
} }
} }
} }
@@ -369,6 +374,11 @@ public class MarketPlace {
event.item.setFromGift(false); event.item.setFromGift(false);
MarketPlaceOffer offer = new MarketPlaceOffer(event.item, event.price, client.getHabbo()); MarketPlaceOffer offer = new MarketPlaceOffer(event.item, event.price, client.getHabbo());
if (!offer.isPersisted()) {
LOGGER.warn("Marketplace offer insert failed for user {} item {}", client.getHabbo().getHabboInfo().getId(), event.item.getId());
return false;
}
client.getHabbo().getInventory().addMarketplaceOffer(offer); client.getHabbo().getInventory().addMarketplaceOffer(offer);
client.getHabbo().getInventory().getItemsComponent().removeHabboItem(event.item); client.getHabbo().getInventory().getItemsComponent().removeHabboItem(event.item);
item.setUserId(-1); item.setUserId(-1);
@@ -98,6 +98,10 @@ public class MarketPlaceOffer implements Runnable {
return this.offerId; return this.offerId;
} }
public boolean isPersisted() {
return this.offerId > 0;
}
public void setOfferId(int offerId) { public void setOfferId(int offerId) {
this.offerId = offerId; this.offerId = offerId;
} }
@@ -0,0 +1,13 @@
package com.eu.habbo.habbohotel.catalog.marketplace;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
class MarketPlaceOfferContractTest {
@Test
void exposesPersistenceState() {
assertDoesNotThrow(() -> MarketPlaceOffer.class.getDeclaredMethod("isPersisted"));
}
}