From 17d6921a4a7ad3bdf71f20f937b02163501276a1 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 29 May 2020 13:49:38 -0400 Subject: [PATCH] ge plugin: submit partially completed trades --- .../http/api/ge/GrandExchangeTrade.java | 3 + .../service/ge/GrandExchangeController.java | 20 +- .../grandexchange/GrandExchangePlugin.java | 90 ++++++--- .../GrandExchangePluginTest.java | 176 ++++++++++++++++++ 4 files changed, 258 insertions(+), 31 deletions(-) diff --git a/http-api/src/main/java/net/runelite/http/api/ge/GrandExchangeTrade.java b/http-api/src/main/java/net/runelite/http/api/ge/GrandExchangeTrade.java index 17b6effb0f..85dccbc793 100644 --- a/http-api/src/main/java/net/runelite/http/api/ge/GrandExchangeTrade.java +++ b/http-api/src/main/java/net/runelite/http/api/ge/GrandExchangeTrade.java @@ -30,7 +30,10 @@ import lombok.Data; public class GrandExchangeTrade { private boolean buy; + private boolean cancel; private int itemId; private int quantity; + private int total; private int price; + private int offer; } diff --git a/http-service/src/main/java/net/runelite/http/service/ge/GrandExchangeController.java b/http-service/src/main/java/net/runelite/http/service/ge/GrandExchangeController.java index ba291385cf..63555cbe7b 100644 --- a/http-service/src/main/java/net/runelite/http/service/ge/GrandExchangeController.java +++ b/http-service/src/main/java/net/runelite/http/service/ge/GrandExchangeController.java @@ -29,6 +29,7 @@ import java.util.Collection; import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import net.runelite.http.api.RuneLiteAPI; import net.runelite.http.api.ge.GrandExchangeTrade; import net.runelite.http.service.account.AuthFilter; import net.runelite.http.service.account.beans.SessionEntry; @@ -58,14 +59,23 @@ public class GrandExchangeController @PostMapping public void submit(HttpServletRequest request, HttpServletResponse response, @RequestBody GrandExchangeTrade grandExchangeTrade) throws IOException { - SessionEntry session = authFilter.handle(request, response); - - if (session == null) + SessionEntry session = null; + if (request.getHeader(RuneLiteAPI.RUNELITE_AUTH) != null) { - return; + session = authFilter.handle(request, response); + if (session == null) + { + // error is set here on the response, so we shouldn't continue + return; + } } + Integer userId = session == null ? null : session.getUser(); - grandExchangeService.add(session.getUser(), grandExchangeTrade); + // We don't keep track of pending trades in the web UI, so only add cancelled or completed trades + if (userId != null && (grandExchangeTrade.isCancel() || grandExchangeTrade.getQuantity() == grandExchangeTrade.getTotal())) + { + grandExchangeService.add(userId, grandExchangeTrade); + } } @GetMapping diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangePlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangePlugin.java index e43cf40475..46dbb93c64 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangePlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangePlugin.java @@ -371,39 +371,88 @@ public class GrandExchangePlugin extends Plugin BufferedImage itemImage = itemManager.getImage(offer.getItemId(), offer.getTotalQuantity(), shouldStack); SwingUtilities.invokeLater(() -> panel.getOffersPanel().updateOffer(offerItem, itemImage, offer, slot)); - submitTrades(slot, offer); + submitTrade(slot, offer); updateConfig(slot, offer); } - private void submitTrades(int slot, GrandExchangeOffer offer) + @VisibleForTesting + void submitTrade(int slot, GrandExchangeOffer offer) { - if (offer.getState() != GrandExchangeOfferState.BOUGHT && offer.getState() != GrandExchangeOfferState.SOLD && - offer.getState() != GrandExchangeOfferState.CANCELLED_BUY && offer.getState() != GrandExchangeOfferState.CANCELLED_SELL) - { - return; - } + GrandExchangeOfferState state = offer.getState(); - // Cancelled offers may have been cancelled before buying/selling any items - if (offer.getQuantitySold() == 0) + if (state != GrandExchangeOfferState.CANCELLED_BUY && state != GrandExchangeOfferState.CANCELLED_SELL && state != GrandExchangeOfferState.BUYING && state != GrandExchangeOfferState.SELLING) { return; } SavedOffer savedOffer = getOffer(slot); - if (!shouldUpdate(savedOffer, offer)) + if (savedOffer == null && (state == GrandExchangeOfferState.BUYING || state == GrandExchangeOfferState.SELLING) && offer.getQuantitySold() == 0) + { + // new offer + GrandExchangeTrade grandExchangeTrade = new GrandExchangeTrade(); + grandExchangeTrade.setBuy(state == GrandExchangeOfferState.BUYING); + grandExchangeTrade.setItemId(offer.getItemId()); + grandExchangeTrade.setQuantity(0); + grandExchangeTrade.setTotal(offer.getTotalQuantity()); + grandExchangeTrade.setPrice(0); + grandExchangeTrade.setOffer(offer.getPrice()); + + log.debug("Submitting new trade: {}", grandExchangeTrade); + grandExchangeClient.submit(grandExchangeTrade); + return; + } + + if (savedOffer == null || savedOffer.getItemId() != offer.getItemId() || savedOffer.getPrice() != offer.getPrice() || savedOffer.getTotalQuantity() != offer.getTotalQuantity()) + { + // desync + return; + } + + if (savedOffer.getState() == offer.getState() && savedOffer.getQuantitySold() == offer.getQuantitySold()) + { + // no change + return; + } + + if (state == GrandExchangeOfferState.CANCELLED_BUY || state == GrandExchangeOfferState.CANCELLED_SELL) + { + GrandExchangeTrade grandExchangeTrade = new GrandExchangeTrade(); + grandExchangeTrade.setBuy(state == GrandExchangeOfferState.CANCELLED_BUY); + grandExchangeTrade.setCancel(true); + grandExchangeTrade.setItemId(offer.getItemId()); + grandExchangeTrade.setQuantity(offer.getQuantitySold()); + grandExchangeTrade.setTotal(offer.getTotalQuantity()); + grandExchangeTrade.setPrice(offer.getQuantitySold() > 0 ? offer.getSpent() / offer.getQuantitySold() : 0); + grandExchangeTrade.setOffer(offer.getPrice()); + + log.debug("Submitting cancelled: {}", grandExchangeTrade); + grandExchangeClient.submit(grandExchangeTrade); + return; + } + + final int qty = offer.getQuantitySold() - savedOffer.getQuantitySold(); + if (qty <= 0) { return; } - // getPrice() is the price of the offer, not necessarily what the item bought at - int priceEach = offer.getSpent() / offer.getQuantitySold(); + // offer.getPrice() is the price of the offer, not necessarily what the item bought at, so we compute it + // based on how much was spent & the qty + final int dspent = offer.getSpent() - savedOffer.getSpent(); + final int price = dspent / qty; + if (price <= 0) + { + return; + } GrandExchangeTrade grandExchangeTrade = new GrandExchangeTrade(); - grandExchangeTrade.setBuy(offer.getState() == GrandExchangeOfferState.BOUGHT || offer.getState() == GrandExchangeOfferState.CANCELLED_BUY); + grandExchangeTrade.setBuy(state == GrandExchangeOfferState.BUYING); grandExchangeTrade.setItemId(offer.getItemId()); - grandExchangeTrade.setQuantity(offer.getQuantitySold()); - grandExchangeTrade.setPrice(priceEach); + grandExchangeTrade.setQuantity(qty); + grandExchangeTrade.setTotal(offer.getTotalQuantity()); + grandExchangeTrade.setPrice(price); + grandExchangeTrade.setOffer(offer.getPrice()); log.debug("Submitting trade: {}", grandExchangeTrade); grandExchangeClient.submit(grandExchangeTrade); @@ -430,17 +479,6 @@ public class GrandExchangePlugin extends Plugin } } - private boolean shouldUpdate(SavedOffer savedOffer, GrandExchangeOffer grandExchangeOffer) - { - if (savedOffer == null) - { - return false; - } - - // Only update offer if state has changed - return savedOffer.getState() != grandExchangeOffer.getState(); - } - @Subscribe public void onChatMessage(ChatMessage event) { diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/grandexchange/GrandExchangePluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/grandexchange/GrandExchangePluginTest.java index dc16d25dfc..d95df70528 100644 --- a/runelite-client/src/test/java/net/runelite/client/plugins/grandexchange/GrandExchangePluginTest.java +++ b/runelite-client/src/test/java/net/runelite/client/plugins/grandexchange/GrandExchangePluginTest.java @@ -24,14 +24,100 @@ */ package net.runelite.client.plugins.grandexchange; +import com.google.inject.Guice; +import com.google.inject.testing.fieldbinder.Bind; +import com.google.inject.testing.fieldbinder.BoundFieldModule; import java.util.Arrays; import java.util.List; +import java.util.concurrent.ScheduledExecutorService; +import javax.inject.Inject; +import net.runelite.api.Client; +import net.runelite.api.GrandExchangeOffer; +import net.runelite.api.GrandExchangeOfferState; +import net.runelite.api.ItemID; +import net.runelite.client.Notifier; +import net.runelite.client.account.SessionManager; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.config.RuneLiteConfig; +import net.runelite.client.game.ItemManager; +import net.runelite.client.input.KeyManager; +import net.runelite.client.input.MouseManager; import static net.runelite.client.plugins.grandexchange.GrandExchangePlugin.findFuzzyIndices; +import static net.runelite.http.api.RuneLiteAPI.GSON; +import net.runelite.http.api.ge.GrandExchangeClient; +import net.runelite.http.api.ge.GrandExchangeTrade; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import static org.mockito.ArgumentMatchers.any; +import org.mockito.Mock; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import org.mockito.junit.MockitoJUnitRunner; +@RunWith(MockitoJUnitRunner.class) public class GrandExchangePluginTest { + @Inject + private GrandExchangePlugin grandExchangePlugin; + + @Mock + @Bind + private GrandExchangeConfig grandExchangeConfig; + + @Mock + @Bind + private Notifier notifier; + + @Mock + @Bind + private SessionManager sessionManager; + + @Mock + @Bind + private ConfigManager configManager; + + @Mock + @Bind + private ItemManager itemManager; + + @Mock + @Bind + private KeyManager keyManager; + + @Mock + @Bind + private MouseManager mouseManager; + + @Mock + @Bind + private ScheduledExecutorService scheduledExecutorService; + + @Mock + @Bind + private GrandExchangeClient grandExchangeClient; + + @Mock + @Bind + private Client client; + + @Mock + @Bind + private RuneLiteConfig runeLiteConfig; + + @Before + public void setUp() + { + Guice.createInjector(BoundFieldModule.of(this)).injectMembers(this); + when(client.getUsername()).thenReturn("adam"); + } + @Test public void testFindFuzzyIndices() { @@ -39,4 +125,94 @@ public class GrandExchangePluginTest // robe bottom assertEquals(Arrays.asList(11, 12, 15), fuzzyIndices); } + + @Test + public void testSubmitTrade() + { + SavedOffer savedOffer = new SavedOffer(); + savedOffer.setItemId(ItemID.ABYSSAL_WHIP); + savedOffer.setQuantitySold(1); + savedOffer.setTotalQuantity(10); + savedOffer.setPrice(1000); + savedOffer.setSpent(25); + savedOffer.setState(GrandExchangeOfferState.BUYING); + when(configManager.getConfiguration("geoffer.adam", "0")).thenReturn(GSON.toJson(savedOffer)); + + // buy 2 @ 10/ea + GrandExchangeOffer grandExchangeOffer = mock(GrandExchangeOffer.class); + when(grandExchangeOffer.getQuantitySold()).thenReturn(1 + 2); + when(grandExchangeOffer.getItemId()).thenReturn(ItemID.ABYSSAL_WHIP); + when(grandExchangeOffer.getTotalQuantity()).thenReturn(10); + when(grandExchangeOffer.getPrice()).thenReturn(1000); + when(grandExchangeOffer.getSpent()).thenReturn(25 + 10 * 2); + when(grandExchangeOffer.getState()).thenReturn(GrandExchangeOfferState.BUYING); + grandExchangePlugin.submitTrade(0, grandExchangeOffer); + + ArgumentCaptor captor = ArgumentCaptor.forClass(GrandExchangeTrade.class); + verify(grandExchangeClient).submit(captor.capture()); + + GrandExchangeTrade trade = captor.getValue(); + assertTrue(trade.isBuy()); + assertEquals(ItemID.ABYSSAL_WHIP, trade.getItemId()); + assertEquals(2, trade.getQuantity()); + assertEquals(10, trade.getTotal()); + assertEquals(10, trade.getPrice()); + } + + @Test + public void testDuplicateTrade() + { + SavedOffer savedOffer = new SavedOffer(); + savedOffer.setItemId(ItemID.ABYSSAL_WHIP); + savedOffer.setQuantitySold(1); + savedOffer.setTotalQuantity(10); + savedOffer.setPrice(1000); + savedOffer.setSpent(25); + savedOffer.setState(GrandExchangeOfferState.BUYING); + when(configManager.getConfiguration("geoffer.adam", "0")).thenReturn(GSON.toJson(savedOffer)); + + GrandExchangeOffer grandExchangeOffer = mock(GrandExchangeOffer.class); + when(grandExchangeOffer.getQuantitySold()).thenReturn(1); + when(grandExchangeOffer.getItemId()).thenReturn(ItemID.ABYSSAL_WHIP); + when(grandExchangeOffer.getTotalQuantity()).thenReturn(10); + when(grandExchangeOffer.getPrice()).thenReturn(1000); + lenient().when(grandExchangeOffer.getSpent()).thenReturn(25); + when(grandExchangeOffer.getState()).thenReturn(GrandExchangeOfferState.BUYING); + grandExchangePlugin.submitTrade(0, grandExchangeOffer); + + verify(grandExchangeClient, never()).submit(any(GrandExchangeTrade.class)); + } + + @Test + public void testCancelTrade() + { + SavedOffer savedOffer = new SavedOffer(); + savedOffer.setItemId(ItemID.ABYSSAL_WHIP); + savedOffer.setQuantitySold(1); + savedOffer.setTotalQuantity(10); + savedOffer.setPrice(1000); + savedOffer.setSpent(25); + savedOffer.setState(GrandExchangeOfferState.BUYING); + when(configManager.getConfiguration("geoffer.adam", "0")).thenReturn(GSON.toJson(savedOffer)); + + GrandExchangeOffer grandExchangeOffer = mock(GrandExchangeOffer.class); + when(grandExchangeOffer.getQuantitySold()).thenReturn(1); + when(grandExchangeOffer.getItemId()).thenReturn(ItemID.ABYSSAL_WHIP); + when(grandExchangeOffer.getTotalQuantity()).thenReturn(10); + when(grandExchangeOffer.getPrice()).thenReturn(1000); + when(grandExchangeOffer.getSpent()).thenReturn(25); + when(grandExchangeOffer.getState()).thenReturn(GrandExchangeOfferState.CANCELLED_BUY); + grandExchangePlugin.submitTrade(0, grandExchangeOffer); + + ArgumentCaptor captor = ArgumentCaptor.forClass(GrandExchangeTrade.class); + verify(grandExchangeClient).submit(captor.capture()); + + GrandExchangeTrade trade = captor.getValue(); + assertTrue(trade.isBuy()); + assertTrue(trade.isCancel()); + assertEquals(ItemID.ABYSSAL_WHIP, trade.getItemId()); + assertEquals(1, trade.getQuantity()); + assertEquals(10, trade.getTotal()); + assertEquals(25, trade.getPrice()); + } } \ No newline at end of file