X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~yarrgweb/git?p=jarrg-ian.git;a=blobdiff_plain;f=src%2Fnet%2Fchiark%2Fyarrg%2FMarketUploader.java;h=980d5de1b527f0b2968e54196ce8292e98d060d6;hp=53b434971f92c0874fb301ada62f3aaaca8fde45;hb=9a0ebbeb9b39aa6ed93c419a306f60267a1d0cc9;hpb=c3697ced3acabd60d04562f8a5965a3ceb4c3f22 diff --git a/src/net/chiark/yarrg/MarketUploader.java b/src/net/chiark/yarrg/MarketUploader.java index 53b4349..980d5de 100644 --- a/src/net/chiark/yarrg/MarketUploader.java +++ b/src/net/chiark/yarrg/MarketUploader.java @@ -21,1149 +21,1483 @@ import java.util.regex.*; import java.util.prefs.Preferences; import java.beans.*; -/** -* MarketUploader is a class that handles the uploading of market -* data from Yohoho! Puzzle Pirates via the Java Accessibility -* API. -* -* MarketUploader initializes after the main YPP window has -* initialized. It provides a simple window with a "Capture -* Market Data" button displayed. Upon clicking this button, a -* progress dialog is displayed, and the data is processed and -* submitted to the YARRG and PCTB servers. If any errors occur, -* an error dialog is shown, and processing returns, the button -* becoming re-enabled. -*/ -public class MarketUploader implements TopLevelWindowListener, GUIInitializedListener { - private JFrame frame = null; - private Window window = null; - private JButton findMarket = null; - private JLabel resultSummary = null; - private JLabel arbitrageResult = null; - private int unknownPCTBcommods = 0; - private long startTime = 0; - - private final static String PCTB_LIVE_HOST_URL = "http://pctb.crabdance.com/"; - private final static String PCTB_TEST_HOST_URL = "http://pctb.ilk.org/"; - private String PCTB_HOST_URL; - - // Yarrg protocol parameters - private final static String YARRG_CLIENTNAME = "jpctb greenend"; - private final static String YARRG_CLIENTVERSION = +/* + * MarketUploader is a class that handles the uploading of market + * data from Yohoho! Puzzle Pirates via the Java Accessibility + * API. + * + * MarketUploader initializes after the main YPP window has + * initialized. It provides a simple window with a "Capture + * Market Data" button displayed. Upon clicking this button, a + * progress dialog is displayed, and the data is processed and + * submitted to the YARRG and PCTB servers. If any errors occur, + * an error dialog is shown, and processing returns, the button + * becoming re-enabled. + */ +public class MarketUploader +implements Runnable, TopLevelWindowListener, GUIInitializedListener { + // UI object references which are set during startup + private JFrame frame = null; + private Window window = null; + + // Genuinely global variables + private PrintStream dtxt = null; + public int uploadcounter = 0; + + // UI objects which are enabled/disabled, cleared/set, created/destroyed, + // etc., for each upload + private JButton findMarket = null; + private JLabel resultSummary = null; + private JLabel arbitrageResult = null; + private ProgressMonitor progmon = null; + + // PCTB protocol parameters + private final static String PCTB_LIVE_HOST_URL = "http://pctb.crabdance.com/"; + private final static String PCTB_TEST_HOST_URL = "http://pctb.ilk.org/"; + private String PCTB_HOST_URL; + + // YARRG protocol parameters + private final static String YARRG_CLIENTNAME = "jpctb greenend"; + private final static String YARRG_CLIENTVERSION = net.chiark.yarrg.Version.version; - private final static String YARRG_CLIENTFIXES = "bug-094"; - private final static String YARRG_LIVE_URL = "http://upload.yarrg.chiark.net/commod-update-receiver"; - private final static String YARRG_TEST_URL = "http://upload.yarrg.chiark.net/test/commod-update-receiver"; - private String YARRG_URL; - - private boolean uploadToYarrg; - private boolean uploadToPCTB; - private boolean showArbitrage; - - private String islandName = null; - private String oceanName = null; - private java.util.concurrent.CountDownLatch latch = null; - - private AccessibleContext sidePanel; - private HashMap commodMap; - public PrintStream dtxt = null; - - private PropertyChangeListener changeListener = new PropertyChangeListener() { - public void propertyChange(PropertyChangeEvent e) { - if(e.getNewValue() != null && - e.getPropertyName().equals(AccessibleContext.ACCESSIBLE_CHILD_PROPERTY)) { - Accessible islandInfo = descendNodes(window,new int[] {0,1,0,0,2,2,0,0,0,0,1,2});; - String text = islandInfo.getAccessibleContext().getAccessibleText().getAtIndex(AccessibleText.SENTENCE,0); - int index = text.indexOf(":"); - String name = text.substring(0,index); - islandName = name; - // if (dtxt!=null) dtxt.println(islandName); - sidePanel.removePropertyChangeListener(this); - latch.countDown(); - } - } - }; - - private int parseQty(String str) { - if (str.equals(">1000")) { - return 1001; - } else { - return Integer.parseInt(str); - } - } - - private void progresslog(String s) { - if (dtxt == null) return; - long now = new Date().getTime(); - dtxt.println("progress "+(now - startTime)+"ms "+s); - } - - private void debug_write_stringdata(String what, String data) throws FileNotFoundException,IOException { - if (dtxt==null) return; - PrintStream strm = new PrintStream(new File("jarrg-debug-"+what)); - strm.print(data); - strm.close(); - } - - private void debug_write_bytes(String what, byte[] data) throws FileNotFoundException,IOException { - if (dtxt==null) return; - FileOutputStream strm = new FileOutputStream(new File("jarrg-debug-"+what)); - strm.write(data); - strm.close(); - } + private final static String YARRG_CLIENTFIXES = "bug-094"; + private final static String YARRG_LIVE_URL = + "http://upload.yarrg.chiark.net/commod-update-receiver"; + private final static String YARRG_TEST_URL = + "http://upload.yarrg.chiark.net/test/commod-update-receiver"; + private String YARRG_URL; + + // Preferences + private boolean uploadToYarrg; + private boolean uploadToPCTB; + private boolean showArbitrage; + + // Values cleared/set for each upload, or used during upload processing + private long startTime = 0; + + private String islandName = null; + private String oceanName = null; + private java.util.concurrent.CountDownLatch latch = null; + + private AccessibleContext sidePanel; + + // PCTB-specific variables + private int unknownPCTBcommods = 0; + private HashMap commodMap; + + + /***************************************** + * UPLOAD-TARGET-INDEPENDENT CODE * + *****************************************/ + + + /* + * UTILITY METHODS AND SUBCLASSES + * + * Useable on any thread. + * + */ + private int parseQty(String str) { + if (str.equals(">1000")) { + return 1001; + } else { + return Integer.parseInt(str); + } + } + + private void debuglog(String s) { + if (dtxt == null) return; + long now = new Date().getTime(); + dtxt.println("progress "+(now - startTime)+"ms " + +Thread.currentThread().getName()+": "+s); + } + + private void debug_write_stringdata(String what, String data) + throws FileNotFoundException,IOException { + if (dtxt==null) return; + PrintStream strm = new PrintStream(new File("jarrg-debug-"+what)); + strm.print(data); + strm.close(); + } + + private void debug_write_bytes(String what, byte[] data) + throws FileNotFoundException,IOException { + if (dtxt==null) return; + FileOutputStream strm = new FileOutputStream(new File("jarrg-debug-"+what)); + strm.write(data); + strm.close(); + } - private void progressNote(ProgressMonitor pm, String s) { - String arb = null; - if (arbitrageResult != null) - arb = arbitrageResult.getText(); - if (arb != null && arb.length() != 0) - s = "" + arb + "
" + s; - pm.setNote(s); - } - /** - * An abstract market offer, entailing a commodity being bought or sold by - * a shoppe, for a certain price in a certain quantity. Not instantiable. - * - * @see Buy - * @see Sell - */ - abstract class Offer { - public int commodity, price, quantity, shoppe; - /** - * Create an offer from record, determining the shoppe Id from - * stallMap and the commodity Id from commodMap. - * priceIndex should be the index of the price in the record - * (the quantity will be priceIndex + 1). - * - * @param record the record with data to create the offer from - * @param stallMap a map containing the ids of the various stalls - * @param commodMap a map containing the ids of the various commodities - * @param priceIndex the index of the price in the record - */ - public Offer(ArrayList record, LinkedHashMap stallMap, HashMap commodMap, - int priceIndex) { - Integer commodId = commodMap.get(record.get(0)); - if(commodId == null) { - throw new IllegalArgumentException(); - } - commodity = commodId.intValue(); - price = Integer.parseInt(record.get(priceIndex)); - String qty = record.get(priceIndex+1); - quantity = parseQty(qty); - shoppe = stallMap.get(record.get(1)).intValue(); - } - - /** - * Returns a human-readable version of this offer, useful for debugging - * - * @return human-readable offer - */ - public String toString() { - return "[C:" + commodity + ",$" + price + ",Q:" + quantity + ",S:" + shoppe + "]"; - } - } - - /** - * An offer from a shoppe or stall to buy a certain quantity of a commodity - * for a certain price. If placed in an ordered Set, sorts by commodity index ascending, - * then by buy price descending, and finally by stall id ascending. - */ - class Buy extends Offer implements Comparable { - /** - * Creates a new Buy offer from the given record - * using the other parameters to determine stall id and commodity id of the offer. - * - * @param record the record with data to create the offer from - * @param stallMap a map containing the ids of the various stalls - * @param commodMap a map containing the ids of the various commodities - */ - public Buy(ArrayList record, LinkedHashMap stallMap, HashMap commodMap) { - super(record,stallMap,commodMap,2); - } + /* + * ENTRY POINT AND STARTUP + * + * Main thread and/or event thread + */ + + public static void main(String[] args) { + // This is not normally called, it seems. + new MarketUploader(); + } + + public MarketUploader() { + Preferences prefs = Preferences.userNodeForPackage(getClass()); + + if (prefs.getBoolean("writeDebugFiles", false)) { + try { + dtxt = new PrintStream(new File("jarrg-debug-log.txt")); + } catch (java.io.FileNotFoundException e) { + System.err.println("JARRG: Error opening debug log: "+e); + } + } + + if (prefs.getBoolean("useLiveServers", true)) { + YARRG_URL = YARRG_LIVE_URL; + PCTB_HOST_URL = PCTB_LIVE_HOST_URL; + } else { + YARRG_URL = YARRG_TEST_URL; + PCTB_HOST_URL = PCTB_TEST_HOST_URL; + } - /** - * Sorts by commodity index ascending, then price descending, then stall id ascending. - */ - public int compareTo(Buy buy) { - // organize by: commodity index, price, stall index - if(commodity == buy.commodity) { - // organize by price, then by stall index - if(price == buy.price) { - // organize by stall index - return shoppe>buy.shoppe ? 1 : -1; - } else if(price > buy.price) { - return -1; - } else { - return 1; - } - } else if(commodity > buy.commodity) { - return 1; - } else { - return -1; - } - } - } + uploadToYarrg=prefs.getBoolean("uploadToYarrg", true); + uploadToPCTB=prefs.getBoolean("uploadToPCTB", true); + showArbitrage=prefs.getBoolean("showArbitrage", true); + + debuglog("main on dispatch thread: "+EventQueue.isDispatchThread()); + EventQueue.invokeLater(this); + } + + /* + * We arrange to wait for the GUI to be initialised, then look at + * every top-level window to see if the Puzzle Pirates window turns up. + */ + public void run() { + debuglog("MarketUploader run()..."); + if (EventQueueMonitor.isGUIInitialized()) { + debuglog("MarketUploader GUI already ready"); + guiInitialized(); + } else { + debuglog("MarketUploader waiting for GUI"); + EventQueueMonitor.addGUIInitializedListener(this); + } + } + + public void guiInitialized() { + Window ws[]= EventQueueMonitor.getTopLevelWindows(); + EventQueueMonitor.addTopLevelWindowListener(this); + for (int i=0; i { - /** - * Creates a new Sell offer from the given record - * using the other parameters to determine stall id and commodity id of the offer. - * - * @param record the record with data to create the offer from - * @param stallMap a map containing the ids of the various stalls - * @param commodMap a map containing the ids of the various commodities - */ - public Sell(ArrayList record, LinkedHashMap stallMap, HashMap commodMap) { - super(record,stallMap,commodMap,4); - } - - /** - * Sorts by commodity index ascending, then price ascending, then stall id ascending. - */ - public int compareTo(Sell sell) { - // organize by: commodity index, price, stall index - if(commodity == sell.commodity) { - // organize by price, then by stall index - if(price == sell.price) { - // organize by stall index - return shoppe>sell.shoppe ? 1 : -1; - } else if(price > sell.price) { - return 1; - } else { - return -1; - } - } else if(commodity > sell.commodity) { - return 1; - } else { - return -1; - } - } - } + public void topLevelWindowCreated(Window w) { + if (frame!=null) + // already got it + return; + String name = w.getAccessibleContext().getAccessibleName(); + debuglog("MarketUploader checking toplevel "+name); + if (!name.equals("Puzzle Pirates")) + // Only if we're running alongside a Window named "Puzzle Pirates" + return; + debuglog("MarketUploader found toplevel, creating gui"); + window = w; + createGUI(); + frame.setVisible(true); + } - /** - * Entry point. Read our preferences. - */ - public MarketUploader() { - Preferences prefs = Preferences.userNodeForPackage(getClass()); - - if (prefs.getBoolean("writeDebugFiles", false)) { - try { - dtxt = new PrintStream(new File("jarrg-debug-log.txt")); - } catch (java.io.FileNotFoundException e) { - System.err.println("JARRG: Error opening debug log: "+e); - } - } - - if (prefs.getBoolean("useLiveServers", true)) { - YARRG_URL = YARRG_LIVE_URL; - PCTB_HOST_URL = PCTB_LIVE_HOST_URL; - } else { - YARRG_URL = YARRG_TEST_URL; - PCTB_HOST_URL = PCTB_TEST_HOST_URL; - } + private void createGUI() { + // Actually set up our GUI + on_ui_thread(); + frame = new JFrame("Jarrg Uploader"); + frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + GridLayout layout = new GridLayout(2,1); + frame.getContentPane().setLayout(layout); + //frame.setPreferredSize(new Dimension(200, 60)); - uploadToYarrg=prefs.getBoolean("uploadToYarrg", true); - uploadToPCTB=prefs.getBoolean("uploadToPCTB", true); - showArbitrage=prefs.getBoolean("showArbitrage", true); - - EventQueueMonitor.addTopLevelWindowListener(this); - if (EventQueueMonitor.isGUIInitialized()) { - createGUI(); - } else { - EventQueueMonitor.addGUIInitializedListener(this); - } + findMarket = new JButton("Upload Market Data"); + findMarket.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + // This is called when the user clicks "upload" + on_ui_thread(); + + uploadcounter++; + findMarket.setEnabled(false); + resultSummary.setText(""); + arbitrageResult.setText(""); + new Thread("MarketUploader-uploader-"+uploadcounter) { + public void run() { + startTime = new Date().getTime(); + unknownPCTBcommods = 0; + try { + runUpload(uploadcounter); + } catch(Exception e) { + error(e.toString()); + e.printStackTrace(); + } + try { + new UIX() { public void body() { + if(sidePanel != null) { + sidePanel.removePropertyChangeListener(changeListener); + } + if (progmon != null) { + progmon.close(); + progmon = null; + } + findMarket.setEnabled(true); + }}.exec("tidying"); + } catch (Exception e) { + System.err.println("exception tidying on UI thread:"); + e.printStackTrace(); + } + } + }.start(); } - - /** - * Set up the GUI, with its window and one-button - * interface. Only initialize if we're running alongside - * a Window named "Puzzle Pirates" though. - */ - private void createGUI() { - if (frame != null && window != null) { - if (window.getAccessibleContext().getAccessibleName().equals("Puzzle Pirates")) frame.setVisible(true); - return; - } - frame = new JFrame("Jarrg Uploader"); - frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); - GridLayout layout = new GridLayout(2,1); - frame.getContentPane().setLayout(layout); - //frame.setPreferredSize(new Dimension(200, 60)); - - findMarket = new JButton("Upload Market Data"); - findMarket.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) { - findMarket.setEnabled(false); - new Thread() { - public void run() { - startTime = new Date().getTime(); - resultSummary.setText(""); - arbitrageResult.setText(""); - unknownPCTBcommods = 0; - try { - runUpload(); - } catch(Exception e) { - error(e.toString()); - e.printStackTrace(); - resultSummary.setText("failed"); - } finally { - if(sidePanel != null) { - // remove it if it's still attached - sidePanel.removePropertyChangeListener(changeListener); - } - } - findMarket.setEnabled(true); - } - }.start(); - } - }); - frame.add(findMarket); - - resultSummary = new JLabel("ready"); - frame.add(resultSummary); + }); + frame.add(findMarket); + + resultSummary = new JLabel("ready"); + frame.add(resultSummary); - arbitrageResult = new JLabel(""); + arbitrageResult = new JLabel(""); - if (showArbitrage) { - layout.setRows(layout.getRows() + 1); - frame.add(arbitrageResult); - } + if (showArbitrage) { + layout.setRows(layout.getRows() + 1); + frame.add(arbitrageResult); + } - frame.pack(); - } + frame.pack(); + } + + /* + * THREAD HANDLING + * + * Special measures are needed because: + * - it is not permitted to use any Swing UI classes or objects + * other than on the Swing event thread + * - we want to run our upload asynchronously + * - we want to do some computation asynchronously (eg, the + * arbitrage and upload data prep) + * + * So what we do is this: + * 1. When the user asks to upload, we spawn a new thread + * to do the upload ("MarketUploader-uploader-*", see + * the call to "new Thread" inside createGUI. + * 2. Whenever that thread needs to touch a UI object it + * uses EventQueue.invokeLater or .invokeAndWait to + * perform the relevant action. We wrap these calls up + * in three utility classes: + * UIA - runs code on UI thread, asynchronously + * UIX - runs code on UI thread, waits for it to finish + * UIXR - as UIX but also returns a value + * These hide the details of the EventQueue class and also do + * some debugging and argument shuffling; the calling syntax is + * still painful, unfortunately, and there is a weird constraint + * on variables used inside the inner body. For a simple + * example, see the handling of "summary" and "summary_final" + * for the call to UIX at the bottom of runUpload. + * 3. Try to put everything back when that thread exits. + * + * Additionally: + * a. There is another thread spawed early to get a timestamp from + * YARRG, if we are uploading there. + * b. Finding the island name can involve callbacks which run in + * the UI event thread. Basically we do the work there, and use + * a CountDownLatch to cause the uploader thread to wait as + * appropriate. + */ + + private void on_ui_thread() { assert(EventQueue.isDispatchThread()); } + private void on_our_thread() { assert(!EventQueue.isDispatchThread()); } + + private abstract class UIA implements Runnable { + private String what; + public abstract void body(); + public void run() { + debuglog("UIA 2 "+what+" begin"); + body(); + debuglog("UIA 3 "+what+" done"); + } + public void exec(String what_in) { + what = what_in; + debuglog("UIA 1 "+what+" request"); + EventQueue.invokeLater(this); + } + }; + private abstract class UIXR implements Runnable { + public abstract ReturnType bodyr(); + public ReturnType return_value; + private String what; + public void run() { + debuglog("UIX 2 "+what+" begin"); + return_value = bodyr(); + debuglog("UIX 3 "+what+" done"); + } + public ReturnType exec(String what_in) throws Exception { + what = what_in; + if (EventQueue.isDispatchThread()) { + debuglog("UIX 1 "+what+" (event thread) entry"); + this.run(); + debuglog("UIX 4 "+what+" (event thread) exit"); + } else { + debuglog("UIX 1 "+what+" (other thread) entry"); + EventQueue.invokeAndWait(this); + debuglog("UIX 4 "+what+" (other thread) exit"); + } + return return_value; + } + }; + private abstract class UIX extends UIXR implements Runnable { + public abstract void body(); + public Object bodyr() { body(); return null; } + }; + + /* + * ERROR REPORTING AND GENERAL UTILITIES + * + * Synchronous modal dialogues + * error and error_html may be called from any thread + */ + + public void error(final String msg) { + try { + new UIX() { public void body() { + resultSummary.setText("failed"); + JOptionPane.showMessageDialog(frame,msg,"Error", + JOptionPane.ERROR_MESSAGE); + }}.exec("error()"); + } catch (Exception e) { + System.err.println("exception reporting to UI thread:"); + e.printStackTrace(); + } + } - /** - * Finds the island name from the /who tab, sets global islandName variable - */ - private void getIsland() { - - // If the league tracker is there, we can skip the faff - // and ask for its tooltip, since we're on a boat - - Accessible leagueTracker = descendNodes(window,new int[] {0,1,0,0,2,1,1,1}); - try { - islandName = ((JLabel)leagueTracker).getToolTipText(); - } catch (NullPointerException e) { - // evidently we're actually on an island - - islandName = null; - AccessibleContext chatArea = descendNodes(window,new int[] {0,1,0,0,0,2,0,0,2}).getAccessibleContext(); - // attach the property change listener to the outer sunshine panel if the "ahoy" tab - // is not active, otherwise attach it to the scroll panel in the "ahoy" tab. - if(!"com.threerings.piracy.client.AttentionListPanel". - equals(descendNodes(window,new int[] {0,1,0,0,2,2,0}).getClass().getCanonicalName())) { - sidePanel = descendNodes(window,new int[] {0,1,0,0,2,2}).getAccessibleContext(); - } else { - sidePanel = descendNodes(window,new int[] {0,1,0,0,2,2,0,0,0}).getAccessibleContext(); - } - sidePanel.addPropertyChangeListener(changeListener); - latch = new java.util.concurrent.CountDownLatch(1); - // make the Players Online ("/who") panel appear - AccessibleEditableText chat = chatArea.getAccessibleEditableText(); - chat.setTextContents("/w"); - int c = chatArea.getAccessibleAction().getAccessibleActionCount(); - for(int i=0;i(.*)", + Pattern.DOTALL | Pattern.CASE_INSENSITIVE); + Matcher m = body.matcher(html); + if (m.find()) { + html = m.group(1); + Pattern fixup = Pattern.compile("<(\\w+) */>");; + m = fixup.matcher(html); + html = m.replaceAll("<$1>"); + m = Pattern.compile("[\\r\\n]+").matcher(html); + html = m.replaceAll(" "); + } + String whole_msg = "

Error

"+msg + +"

PCTB Server said:

"+html+"
"; + debuglog("###" + whole_msg + "###"); + + error(whole_msg); + } + + public void progressNote(final String s_in) throws Exception { + new UIA() { public void body() { + String arb = null; + arb = arbitrageResult.getText(); + String s = s_in; + if (arb != null && arb.length() != 0) + s = "" + arb + "
" + s; + progmon.setNote(s); + }}.exec("progressNote "+s_in); + } + public void setProgress(final int nv) throws Exception { + new UIA() { public void body() { + progmon.setProgress(nv); + }}.exec("setProgress "+nv); + } + public boolean checkCancelled() throws Exception { + return new UIXR() { public Boolean bodyr() { + boolean can = progmon.isCanceled(); + if (can) resultSummary.setText("cancelled"); + return new Boolean(can); + }}.exec("checkCancelled").booleanValue(); + } + + + /* + * ACTUAL DATA COLLECTION AND UPLOAD + */ + + private void runUpload(int counter) throws Exception { + // Runs the data collection process, and upload the results. + // In most cases of error, we call error() (which synchronously + // reports the error) and then simply return. + + on_our_thread(); + + boolean doneyarrg = false, donepctb = false; + YarrgTimestampFetcher yarrgts_thread = null; + + debuglog("starting"); + + if (uploadToYarrg) { + debuglog("(async) yarrg timestamp..."); + yarrgts_thread = new YarrgTimestampFetcher(counter); + yarrgts_thread.start(); + } - /** - * Find the ocean name from the window title, and set global oceanName variable - */ - private void getOcean() { - oceanName = null; - AccessibleContext topwindow = window.getAccessibleContext(); - oceanName = topwindow.getAccessibleName().replaceAll(".*on the (\\w+) ocean", "$1"); - } + final AccessibleTable accesstable = + new UIXR() { public AccessibleTable bodyr() { + progmon = new ProgressMonitor + (frame,"Processing Market Data","Getting table data",0,100); + progmon.setMillisToDecideToPopup(0); + progmon.setMillisToPopup(0); + + AccessibleTable at = findMarketTable(); + if(at == null) { + error("Market table not found!"+ + " Please open the Buy/Sell Commodities interface."); + return null; + } + if(at.getAccessibleRowCount() == 0) { + error("No data found, please wait for the table to have data first!"); + return null; + } + if(!isDisplayAll()) { + error("Please select \"All\" from the Display: popup menu."); + return null; + } + + debuglog("(async) getisland..."); + getIsland(); + debuglog("getocean..."); + getOcean(); + debuglog("getocean done"); + + return at; + }}.exec("accesstable"); + if (accesstable == null) return; + + if (latch != null) { + latch.await(2, java.util.concurrent.TimeUnit.SECONDS); + } + debuglog("(async) getisland done"); + + String yarrgts = null; + if (yarrgts_thread != null) { + debuglog("(async) yarrg timestamp join..."); + yarrgts_thread.join(); + debuglog("(async) yarrg timestamp joined."); + yarrgts = yarrgts_thread.ts; + } + if (islandName == null) { + error("Could not find island name in YPP user interface."); + return; + } - /** - * Shows a dialog with the error msg. - * - * @param msg a String describing the error that occured. - */ - private void error(String msg) { - JOptionPane.showMessageDialog(frame,msg,"Error",JOptionPane.ERROR_MESSAGE); - } - - private void error_html(String msg, String html) { - Pattern body = Pattern.compile("(.*)", Pattern.DOTALL | Pattern.CASE_INSENSITIVE); - Matcher m = body.matcher(html); - if (m.find()) { - html = m.group(1); - Pattern fixup = Pattern.compile("<(\\w+) */>");; - m = fixup.matcher(html); - html = m.replaceAll("<$1>"); - m = Pattern.compile("[\\r\\n]+").matcher(html); - html = m.replaceAll(" "); - } - String whole_msg = "

Error

"+msg+"

PCTB Server said:

"+html+"
"; - if (dtxt!=null) dtxt.println("###" + whole_msg + "###"); - - JOptionPane.showMessageDialog(frame,whole_msg,"Error",JOptionPane.ERROR_MESSAGE); + debuglog("table check..."); + + final ArrayList> data = + new UIXR>> + () { public ArrayList> bodyr() { + String headings_expected[] = new String[] + { "Commodity", "Trading outlet", "Buy price", + "Will buy", "Sell price", "Will sell" }; + + ArrayList> headers = + getData(accesstable.getAccessibleColumnHeader()); + if (headers.size() != 1) { + error("Table headings not one row! " + headers.toString()); + return null; + } + if (headers.get(0).size() < 6 || + headers.get(0).size() > 7) { + error("Table headings not six or seven columns! " + headers.toString()); + return null; + } + for (int col=0; col> headers = getData(accesstable.getAccessibleColumnHeader()); - if (headers.size() != 1) { - error("Table headings not one row! " + headers.toString()); - return; - } - if (headers.get(0).size() < 6 || - headers.get(0).size() > 7) { - error("Table headings not six or seven columns! " + headers.toString()); - return; - } - for (int col=0; col> data = getData(accesstable); - - if (showArbitrage) { - progresslog("arbitrage..."); - calculateArbitrage(data); - progresslog("arbitrage done."); - } - - if (uploadToYarrg && yarrgts != null) { - progresslog("yarrg prepare..."); - progressNote(pm, "Yarrg: Preparing data"); - pm.setProgress(10); - - StringBuilder yarrgsb = new StringBuilder(); - String yarrgdata; // string containing what we'll feed to yarrg - - for (ArrayList row : data) { - if (row.size() > 6) { - row.remove(6); - } - for (String rowitem : row) { - yarrgsb.append(rowitem != null ? rowitem : ""); - yarrgsb.append("\t"); - } - yarrgsb.setLength(yarrgsb.length()-1); // chop - yarrgsb.append("\n"); - } - - yarrgdata = yarrgsb.toString(); - - progressNote(pm, "Yarrg: Uploading"); - progresslog("yarrg upload..."); - - doneyarrg = runYarrg(yarrgts, oceanName, islandName, yarrgdata); - progresslog("yarrg done."); - } - - if (uploadToPCTB) { - progresslog("pctb prepare..."); - progressNote(pm, "PCTB: Getting stall names"); - pm.setProgress(20); - if(pm.isCanceled()) { - return; - } - TreeSet buys = new TreeSet(); - TreeSet sells = new TreeSet(); - LinkedHashMap stallMap = getStallMap(data); - pm.setProgress(40); - progressNote(pm, "PCTB: Sorting offers"); - if(pm.isCanceled()) { - return; - } - // get commod map + } + + debuglog("table read..."); + + return getData(accesstable); + }}.exec("data"); + if (data == null) return; + + if (showArbitrage) { + debuglog("arbitrage..."); + calculateArbitrage(data); + debuglog("arbitrage done."); + } + + if (uploadToYarrg && yarrgts != null) { + debuglog("yarrg prepare..."); + progressNote("Yarrg: Preparing data"); + setProgress(10); + + StringBuilder yarrgsb = new StringBuilder(); + String yarrgdata; // string containing what we'll feed to yarrg - progresslog("pctb commodmap..."); - HashMap commodMap = getCommodMap(); - if(commodMap == null) { - return; - } - progresslog("pctb commodmap done."); - int[] offerCount = getBuySellMaps(data,buys,sells,stallMap,commodMap); - // if (dtxt!=null) dtxt.println(sells); - // if (dtxt!=null) dtxt.println("\n\n\n"+buys); - - ByteArrayOutputStream outStream = new ByteArrayOutputStream(); - pm.setProgress(60); - progressNote(pm, "PCTB: Sending data"); - if(pm.isCanceled()) { - return; - } - GZIPOutputStream out = new GZIPOutputStream(outStream); - DataOutputStream dos = new DataOutputStream(out); - dos.writeBytes("005y\n"); - dos.writeBytes(stallMap.size()+"\n"); - dos.writeBytes(getAbbrevStallList(stallMap)); - writeBuySellOffers(buys,sells,offerCount,out); - out.finish(); - progresslog("pctb send..."); - - byte[] ba = outStream.toByteArray(); - debug_write_bytes("pctb-marketdata.gz", ba); - - InputStream in = sendInitialData(new ByteArrayInputStream(ba)); - progresslog("pctb sent."); - if (in == null) return; - pm.setProgress(80); - if(pm.isCanceled()) { - return; - } - progressNote(pm, "PCTB: Waiting ..."); - progresslog("pctb finish..."); - donepctb = finishUpload(in); - progresslog("pctb done."); - } - pm.setProgress(100); - - if ((uploadToPCTB && !donepctb) || - (uploadToYarrg && !doneyarrg)) { - resultSummary.setText("trouble"); - } else if (unknownPCTBcommods != 0) { - resultSummary.setText("PCTB lacks "+unknownPCTBcommods+" commod"); - } else if (donepctb || doneyarrg) { - resultSummary.setText("Done " + islandName); - } else { - resultSummary.setText("uploaded nowhere!"); - } - progresslog("done."); + for (ArrayList row : data) { + if (row.size() > 6) { + row.remove(6); } - - /** - * Get the offer data out of the table and cache it in an ArrayList. - * - * @param table the AccessibleTable containing the market data - * @return an array of record arrays, each representing a row of the table - */ - private ArrayList> getData(AccessibleTable table) { - ArrayList> data = new ArrayList>(); - for (int i = 0; i < table.getAccessibleRowCount(); i++) { - ArrayList row = new ArrayList(); - for (int j = 0; j < table.getAccessibleColumnCount(); j++) { - row.add(table.getAccessibleAt(i, j).getAccessibleContext().getAccessibleName()); - } - data.add(row); - } - return data; + for (String rowitem : row) { + yarrgsb.append(rowitem != null ? rowitem : ""); + yarrgsb.append("\t"); } + yarrgsb.setLength(yarrgsb.length()-1); // chop + yarrgsb.append("\n"); + } + + yarrgdata = yarrgsb.toString(); + + progressNote("Yarrg: Uploading"); + debuglog("yarrg upload..."); + + doneyarrg = runYarrg(yarrgts, oceanName, islandName, yarrgdata); + debuglog("yarrg done."); + } + + if (uploadToPCTB) { + debuglog("pctb prepare..."); + progressNote("PCTB: Getting stall names"); + setProgress(20); + if(checkCancelled()) { + return; + } + TreeSet buys = new TreeSet(); + TreeSet sells = new TreeSet(); + LinkedHashMap stallMap = getStallMap(data); + setProgress(40); + progressNote("PCTB: Sorting offers"); + if(checkCancelled()) { + return; + } + // get commod map + + debuglog("pctb commodmap..."); + HashMap commodMap = getCommodMap(); + if(commodMap == null) { + return; + } + debuglog("pctb commodmap done."); + int[] offerCount = getBuySellMaps(data,buys,sells,stallMap,commodMap); + // debuglog(sells); + // debuglog("\n\n\n"+buys); + + ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + setProgress(60); + progressNote("PCTB: Sending data"); + if(checkCancelled()) { + return; + } + GZIPOutputStream out = new GZIPOutputStream(outStream); + DataOutputStream dos = new DataOutputStream(out); + dos.writeBytes("005y\n"); + dos.writeBytes(stallMap.size()+"\n"); + dos.writeBytes(getAbbrevStallList(stallMap)); + writeBuySellOffers(buys,sells,offerCount,out); + out.finish(); + debuglog("pctb send..."); + + byte[] ba = outStream.toByteArray(); + debug_write_bytes("pctb-marketdata.gz", ba); + + InputStream in = sendInitialData(new ByteArrayInputStream(ba)); + debuglog("pctb sent."); + if (in == null) return; + setProgress(80); + if(checkCancelled()) { + return; + } + progressNote("PCTB: Waiting ..."); + debuglog("pctb finish..."); + donepctb = finishUpload(in); + debuglog("pctb done."); + } + setProgress(99); + + String summary; + if ((uploadToPCTB && !donepctb) || + (uploadToYarrg && !doneyarrg)) { + summary= "trouble"; + } else if (unknownPCTBcommods != 0) { + summary= "PCTB lacks "+unknownPCTBcommods+" commod(s)"; + } else if (donepctb || doneyarrg) { + summary= "Done " + islandName; + } else { + summary= "uploaded nowhere!"; + } + final String summary_final = summary; + new UIX() { public void body() { + resultSummary.setText(summary_final); + }}.exec("resultSummary.setText"); + + debuglog("done."); + } + + /* + * UPLOAD HELPER FUNCTIONS FOR EXTRACTING SPECIFIC UI DATA + */ - /** - * @return the table containing market data if it exists, otherwise null - */ - public AccessibleTable findMarketTable() { - Accessible node1 = window; - Accessible node = descendNodes(node1,new int[] {0,1,0,0,0,0,1,0,0,1,0,0}); // commod market - // commod market: {0,1,0,0,0,0,1,0,0,1,0} {0,1,0,0,0,0,1,0,1,0,0,1,0,0}) - // if (dtxt!=null) dtxt.println(node); - if (!(node instanceof JTable)) { - node = descendNodes(node1,new int[] {0,1,0,0,0,0,1,0,1,0,0,1,0,0}); // commod market - } - if (!(node instanceof JTable)) return null; - AccessibleTable table = node.getAccessibleContext().getAccessibleTable(); - // if (dtxt!=null) dtxt.println(table); - return table; - } + private ArrayList> getData(AccessibleTable table) { + // Gets the offer data out of the table and returns it as an ArrayList + + on_ui_thread(); + ArrayList> data = new ArrayList>(); + for (int i = 0; i < table.getAccessibleRowCount(); i++) { + ArrayList row = new ArrayList(); + for (int j = 0; j < table.getAccessibleColumnCount(); j++) { + row.add(table.getAccessibleAt(i, j) + .getAccessibleContext().getAccessibleName()); + } + data.add(row); + } + return data; + } - /** - * Utility method to descend through several levels of Accessible children - * at once. - * - * @param parent the node on which to start the descent - * @param path an array of ints, each int being the index of the next - * accessible child to descend. - * @return the Accessible reached by following the descent path, - * or null if the desired path was invalid. - */ - private Accessible descendNodes(Accessible parent, int[] path) { - for(int i=0;iAccessible "node". - * - * @param parent the node with children - * @param childNum the index of the child of parent to return - * @return the childNum child of parent or null - * if the child is not found. - */ - private Accessible descend(Accessible parent, int childNum) { - if (parent == null) return null; - int children = parent.getAccessibleContext().getAccessibleChildrenCount(); - if (childNum >= children) { - if (dtxt!=null) dtxt.println("DESCEND "+childNum+" > "+children+" NOT FOUND"); - return null; - } - Accessible child = parent.getAccessibleContext().getAccessibleChild(childNum); - if (dtxt!=null) dtxt.println("DESCEND "+childNum+" "+child.getClass().getName()+" OK"); - return child; - } + private boolean isDisplayAll() { + // Returns true iff the "Display:" menu on the commodities + // interface in YPP is set to "All" + on_ui_thread(); + + Accessible button = descendNodes(window,new int[] {0,1,0,0,0,0,1,0,0,0,1}); + if(!(button instanceof JButton)) { + button = descendNodes(window,new int[] {0,1,0,0,0,0,1,0,1,0,0,0,1}); + } + String display = button.getAccessibleContext().getAccessibleName(); + if(!display.equals("All")) { + return false; + } + return true; + } - public static void main(String[] args) { - new MarketUploader(); - } - - /** - * Set the global window variable after the YPP window is created, - * remove the top level window listener, and start the GUI - */ - public void topLevelWindowCreated(Window w) { - window = w; - EventQueueMonitor.removeTopLevelWindowListener(this); - createGUI(); + /* + * FUNCTIONS AND CALLBACKS FOR FINDING ISLAND AND OCEAN + */ + + private void getOcean() { + // Finds the ocean name from the window title. + // Stores it in the global oceanName + on_ui_thread(); + + oceanName = null; + AccessibleContext topwindow = window.getAccessibleContext(); + oceanName = topwindow.getAccessibleName() + .replaceAll(".*on the (\\w+) ocean", "$1"); + } + + private void getIsland() { + // Tries to find the island name. Either: + // (a) sets the islandName global + // or + // (b) sets latch to a new CountDownLatch, and arranges that + // at some point later, islandName will be set and the latch + // decremented to zero + on_ui_thread(); + + // If the league tracker is there, we can skip the faff + // and ask for its tooltip, since we're on a boat + + Accessible leagueTrackerContainer = + descendNodes(window,new int[] {0,1,0,0,2,1}); + Accessible leagueTrackerItself = + descendByClass(leagueTrackerContainer, + "com.threerings.yohoho.sea.client.LeagueTracker"); + Accessible leagueTracker = descend(leagueTrackerItself, 1); + try { + islandName = ((JLabel)leagueTracker).getToolTipText(); + latch = null; + } catch (NullPointerException e) { + // evidently we're actually on an island + + islandName = null; + AccessibleContext chatArea = + descendNodes(window,new int[] {0,1,0,0,0,2,0,0,2}) + .getAccessibleContext(); + // attach the property change listener to the outer sunshine + // panel if the "ahoy" tab is not active, otherwise attach it to + // the scroll panel in the "ahoy" tab. + if(!"com.threerings.piracy.client.AttentionListPanel". + equals(descendNodes(window,new int[] {0,1,0,0,2,2,0}) + .getClass().getCanonicalName())) { + sidePanel = descendNodes(window,new int[] {0,1,0,0,2,2}) + .getAccessibleContext(); + } else { + sidePanel = descendNodes(window,new int[] {0,1,0,0,2,2,0,0,0}) + .getAccessibleContext(); + } + sidePanel.addPropertyChangeListener(changeListener); + latch = new java.util.concurrent.CountDownLatch(1); + // make the Players Online ("/who") panel appear + AccessibleEditableText chat = chatArea.getAccessibleEditableText(); + chat.setTextContents("/w"); + int c = chatArea.getAccessibleAction().getAccessibleActionCount(); + for(int i=0;i= children) { + debuglog("DESCEND "+childNum+" > "+children+" NOT FOUND"); + return null; + } + Accessible child = parent.getAccessibleContext() + .getAccessibleChild(childNum); + debuglog("DESCEND "+childNum+" "+child.getClass().getName()+" OK"); + return child; + } + + private Accessible descendByClass(Accessible parent, String classname) { + // Descends one level to the first child which has the specified class. + on_ui_thread(); + + if (parent == null) return null; + AccessibleContext ac = parent.getAccessibleContext(); + int children = ac.getAccessibleChildrenCount(); + for (int i=0; irecord, determining the shoppe Id from + * stallMap and the commodity Id from commodMap. + * priceIndex should be the index of the price in the record + * (the quantity will be priceIndex + 1). + * + * @param record the record with data to create the offer from + * @param stallMap a map containing the ids of the various stalls + * @param commodMap a map containing the ids of the various commodities + * @param priceIndex the index of the price in the record + */ + public Offer(ArrayList record, + LinkedHashMap stallMap, + HashMap commodMap, + int priceIndex) { + Integer commodId = commodMap.get(record.get(0)); + if(commodId == null) { + throw new IllegalArgumentException(); + } + commodity = commodId.intValue(); + price = Integer.parseInt(record.get(priceIndex)); + String qty = record.get(priceIndex+1); + quantity = parseQty(qty); + shoppe = stallMap.get(record.get(1)).intValue(); + } + + /** + * Returns a human-readable version of this offer, useful for debugging + * + * @return human-readable offer + */ + public String toString() { + return "[C:" + commodity + ",$" + price + ",Q:" + + quantity + ",S:" + shoppe + "]"; + } + } - /** - * Returns true if the "Display:" menu on the commodities interface in YPP is set to "All" - * - * @return true if all commodities are displayed, otherwise false - */ - private boolean isDisplayAll() { - Accessible button = descendNodes(window,new int[] {0,1,0,0,0,0,1,0,0,0,1}); - if(!(button instanceof JButton)) { - button = descendNodes(window,new int[] {0,1,0,0,0,0,1,0,1,0,0,0,1}); - } - String display = button.getAccessibleContext().getAccessibleName(); - if(!display.equals("All")) { - return false; - } - return true; + /** + * An offer from a shoppe or stall to buy a certain quantity of a + * commodity for a certain price. If placed in an ordered Set, + * sorts by commodity index ascending, then by buy price + * descending, and finally by stall id ascending. + */ + class Buy extends Offer implements Comparable { + /** + * Creates a new Buy offer from the given + * record using the other parameters to determine + * stall id and commodity id of the offer. + * + * @param record the record with data to create the offer from + * @param stallMap a map containing the ids of the various stalls + * @param commodMap a map containing the ids of the various commodities + */ + public Buy(ArrayList record, + LinkedHashMap stallMap, + HashMap commodMap) { + super(record,stallMap,commodMap,2); + } + + /** + * Sorts by commodity index ascending, then price descending, + * then stall id ascending. + */ + public int compareTo(Buy buy) { + // organize by: commodity index, price, stall index + if(commodity == buy.commodity) { + // organize by price, then by stall index + if(price == buy.price) { + // organize by stall index + return shoppe>buy.shoppe ? 1 : -1; + } else if(price > buy.price) { + return -1; + } else { + return 1; } + } else if(commodity > buy.commodity) { + return 1; + } else { + return -1; + } + } + } - public void topLevelWindowDestroyed(Window w) {} - - public void guiInitialized() { - createGUI(); + /** + * An offer from a shoppe or stall to sell a certain quantity of + * a commodity for a certain price. If placed in an ordered Set, + * sorts by commodity index ascending, then by sell price + * ascending, and finally by stall id ascending. + */ + class Sell extends Offer implements Comparable { + /** + * Creates a new Sell offer from the given + * record using the other parameters to determine + * stall id and commodity id of the offer. + * + * @param record the record with data to create the offer from + * @param stallMap a map containing the ids of the various stalls + * @param commodMap a map containing the ids of the various commodities + */ + public Sell(ArrayList record, + LinkedHashMap stallMap, + HashMap commodMap) { + super(record,stallMap,commodMap,4); + } + + /** + * Sorts by commodity index ascending, then price ascending, then + * stall id ascending. + */ + public int compareTo(Sell sell) { + // organize by: commodity index, price, stall index + if(commodity == sell.commodity) { + // organize by price, then by stall index + if(price == sell.price) { + // organize by stall index + return shoppe>sell.shoppe ? 1 : -1; + } else if(price > sell.price) { + return 1; + } else { + return -1; } + } else if(commodity > sell.commodity) { + return 1; + } else { + return -1; + } + } + } + + /** + * Gets the list of commodities and their associated commodity ids. + * + * @return a map where the key is the commodity and the value is + * the commodity id. + */ + private HashMap getCommodMap() { + on_our_thread(); + if(commodMap != null) { + return commodMap; + } + HashMap map = new HashMap(); + String xml; + try { + URL host = new URL(PCTB_HOST_URL + "commodmap.php"); + BufferedReader br = + new BufferedReader(new InputStreamReader(host.openStream())); + StringBuilder sb = new StringBuilder(); + String str; + while((str = br.readLine()) != null) { + sb.append(str); + } + if (dtxt != null) + debug_write_stringdata("pctb-commodmap.xmlish", sb.toString()); + int first = sb.indexOf("
") + 5;
+      int last = sb.indexOf("");
+      xml = sb.substring(first,last);
+      Reader reader = new CharArrayReader(xml.toCharArray());
+      Document d = DocumentBuilderFactory.newInstance()
+	.newDocumentBuilder().parse(new InputSource(reader));
+      NodeList maps = d.getElementsByTagName("CommodMap");
+      for(int i=0;i getCommodMap() {
-		if(commodMap != null) {
-			return commodMap;
-		}
-		HashMap map = new HashMap();
-		String xml;
-		try {
-			URL host = new URL(PCTB_HOST_URL + "commodmap.php");
-			BufferedReader br = new BufferedReader(new InputStreamReader(host.openStream()));
-			StringBuilder sb = new StringBuilder();
-			String str;
-			while((str = br.readLine()) != null) {
-				sb.append(str);
-			}
-			if (dtxt != null) debug_write_stringdata("pctb-commodmap.xmlish", sb.toString());
-			int first = sb.indexOf("
") + 5;
-			int last = sb.indexOf("");
-			xml = sb.substring(first,last);
-			Reader reader = new CharArrayReader(xml.toCharArray());
-			Document d = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(reader));
-			NodeList maps = d.getElementsByTagName("CommodMap");
-			for(int i=0;iLinkedHashMap
+   *	where the key is the stall name and the value is the generated
+   *	stall id (position in the list).  

The reason this method + * returns a LinkedHashMap instead of a simple HashMap is the + * need for iterating over the stall names in insertion order for + * output to the server. + * + * @param offers the list of records from the commodity buy/sell interface + * @return an iterable ordered map of the stall names and + * generated stall ids + */ + private LinkedHashMap getStallMap + (ArrayList> offers) { + int count = 0; + LinkedHashMap map = new LinkedHashMap(); + for(ArrayList offer : offers) { + String shop = offer.get(1); + if(!map.containsKey(shop)) { + count++; + map.put(shop,count); + } + } + return map; + } - /** - * Given the list of offers, this method will find all the unique stall names - * and return them in a LinkedHashMap where the key is the stall name - * and the value is the generated stall id (position in the list). - *

- * The reason this method returns a LinkedHashMap instead of a simple HashMap is the need - * for iterating over the stall names in insertion order for output to the server. - * - * @param offers the list of records from the commodity buy/sell interface - * @return an iterable ordered map of the stall names and generated stall ids - */ - private LinkedHashMap getStallMap(ArrayList> offers) { - int count = 0; - LinkedHashMap map = new LinkedHashMap(); - for(ArrayList offer : offers) { - String shop = offer.get(1); - if(!map.containsKey(shop)) { - count++; - map.put(shop,count); - } - } - return map; + /** + * Gets a sorted list of Buys and Sells from the list of + * records. buys and sells should be + * pre-initialized and passed into the method to receive the + * data. Returns a 2-length int array with the number of buys + * and sells found. + * + * @param offers the data found from the market table in-game + * @param buys an empty initialized TreeSet<Offer> to + * hold the Buy offers. + * @param sells an empty initialized TreeSet<Offer> to + * hold the Sell offers. + * @param stalls the map of stalls to their ids + * @param commodMap the map of commodities to their ids + * @return a 2-length int[] array containing the number of buys + * and sells, respectively + */ + private int[] getBuySellMaps(ArrayList> offers, + TreeSet buys, + TreeSet sells, + LinkedHashMap stalls, + HashMap commodMap) { + int[] buySellCount = new int[2]; + for(ArrayList offer : offers) { + try { + if(offer.get(2) != null) { + buys.add(new Buy(offer,stalls,commodMap)); + buySellCount[0]++; } - - /** - * Gets a sorted list of Buys and Sells from the list of records. buys and sells - * should be pre-initialized and passed into the method to receive the data. - * Returns a 2-length int array with the number of buys and sells found. - * - * @param offers the data found from the market table in-game - * @param buys an empty initialized TreeSet<Offer> to - * hold the Buy offers. - * @param sells an empty initialized TreeSet<Offer> to - * hold the Sell offers. - * @param stalls the map of stalls to their ids - * @param commodMap the map of commodities to their ids - * @return a 2-length int[] array containing the number of buys and sells, respectively - */ - private int[] getBuySellMaps(ArrayList> offers, TreeSet buys, - TreeSet sells, LinkedHashMap stalls, HashMap commodMap) { - int[] buySellCount = new int[2]; - for(ArrayList offer : offers) { - try { - if(offer.get(2) != null) { - buys.add(new Buy(offer,stalls,commodMap)); - buySellCount[0]++; - } - if(offer.get(4) != null) { - sells.add(new Sell(offer,stalls,commodMap)); - buySellCount[1]++; - } - } catch(IllegalArgumentException e) { - unknownPCTBcommods++; - if (dtxt!=null) dtxt.println("Error: Unsupported Commodity \"" + offer.get(0) + "\""); - } - } - if (buySellCount[0]==0 && buySellCount[1]==0) { - error("No (valid) offers for PCTB?!"); - throw new IllegalArgumentException(); - } - return buySellCount; + if(offer.get(4) != null) { + sells.add(new Sell(offer,stalls,commodMap)); + buySellCount[1]++; } + } catch(IllegalArgumentException e) { + unknownPCTBcommods++; + debuglog("Error: Unsupported Commodity \"" + offer.get(0) + "\""); + } + } + if (buySellCount[0]==0 && buySellCount[1]==0) { + error("No (valid) offers for PCTB?!"); + throw new IllegalArgumentException(); + } + return buySellCount; + } - /** - * Prepares the list of stalls for writing to the output stream. - * The String returned by this method is ready to be written - * directly to the stream. - *

- * All shoppe names are left as they are. Stall names are abbreviated just before the - * apostrophe in the possessive, with an "^" and a letter matching the stall's type - * appended. Example: "Burninator's Ironworking Stall" would become "Burninator^I". - * - * @param stallMap the map of stalls and stall ids in an iterable order - * @return a String containing the list of stalls in format ready - * to be written to the output stream. - */ - private String getAbbrevStallList(LinkedHashMap stallMap) { - // set up some mapping - HashMap types = new HashMap(); - types.put("Apothecary Stall", "A"); - types.put("Distilling Stall", "D"); - types.put("Furnishing Stall", "F"); - types.put("Ironworking Stall", "I"); - types.put("Shipbuilding Stall", "S"); - types.put("Tailoring Stall", "T"); - types.put("Weaving Stall", "W"); + /** + * Prepares the list of stalls for writing to the output stream. + * The String returned by this method is ready to be + * written directly to the stream.

All shoppe names are left + * as they are. Stall names are abbreviated just before the + * apostrophe in the possessive, with an "^" and a letter + * matching the stall's type appended. Example: "Burninator's + * Ironworking Stall" would become "Burninator^I". + * + * @param stallMap the map of stalls and stall ids in an iterable order + * @return a String containing the list of stalls in + * format ready to be written to the output stream. + */ + private String getAbbrevStallList(LinkedHashMap stallMap) { + // set up some mapping + HashMap types = new HashMap(); + types.put("Apothecary Stall", "A"); + types.put("Distilling Stall", "D"); + types.put("Furnishing Stall", "F"); + types.put("Ironworking Stall", "I"); + types.put("Shipbuilding Stall", "S"); + types.put("Tailoring Stall", "T"); + types.put("Weaving Stall", "W"); - StringBuilder sb = new StringBuilder(); - for(String name : stallMap.keySet()) { - int index = name.indexOf("'s"); - String finalName = name; - String type = null; - if (index > 0) { - finalName = name.substring(0,index); - if(index + 2 < name.length()) { - String end = name.substring(index+2,name.length()).trim(); - type = types.get(end); - } - } - if(type==null) { - sb.append(name+"\n"); - } else { - sb.append(finalName+"^"+type+"\n"); - } - } - return sb.toString(); + StringBuilder sb = new StringBuilder(); + for(String name : stallMap.keySet()) { + int index = name.indexOf("'s"); + String finalName = name; + String type = null; + if (index > 0) { + finalName = name.substring(0,index); + if(index + 2 < name.length()) { + String end = name.substring(index+2,name.length()).trim(); + type = types.get(end); } + } + if(type==null) { + sb.append(name+"\n"); + } else { + sb.append(finalName+"^"+type+"\n"); + } + } + return sb.toString(); + } - /** - * Writes a list of offers in correct format to the output stream. - *

- * The format is thus: (all numbers are 2-byte integers in little-endian format) - * (number of offers of this type, aka buy/sell) - * (commodity ID) (number of offers for this commodity) [shopID price qty][shopID price qty]... - * - * @param out the output stream to write the data to - * @param offers the offers to write - */ - private void writeOffers(OutputStream out, TreeSet offers) throws IOException { - ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - if(offers.size() == 0) { - // nothing to write, and "0" has already been written - return; - } - int commodity = offers.first().commodity; - int count = 0; - for(Offer offer : offers) { - if(commodity != offer.commodity) { - // write out buffer - writeBufferedOffers(out,buffer.toByteArray(),commodity,count); - buffer.reset(); - commodity = offer.commodity; - count = 0; - } - writeLEShort(offer.shoppe,buffer); // stall index - writeLEShort(offer.price,buffer); // buy price - writeLEShort(offer.quantity,buffer); // buy qty - count++; - } - writeBufferedOffers(out,buffer.toByteArray(),commodity,count); - } + /** + * Writes a list of offers in correct format to the output stream. + *

+ * The format is thus: (all numbers are 2-byte integers in + * little-endian format) (number of offers of this type, aka + * buy/sell) (commodity ID) (number of offers for this commodity) + * [shopID price qty][shopID price qty]... + * + * @param out the output stream to write the data to + * @param offers the offers to write + */ + private void writeOffers(OutputStream out, TreeSet offers) + throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + if(offers.size() == 0) { + // nothing to write, and "0" has already been written + return; + } + int commodity = offers.first().commodity; + int count = 0; + for(Offer offer : offers) { + if(commodity != offer.commodity) { + // write out buffer + writeBufferedOffers(out,buffer.toByteArray(),commodity,count); + buffer.reset(); + commodity = offer.commodity; + count = 0; + } + writeLEShort(offer.shoppe,buffer); // stall index + writeLEShort(offer.price,buffer); // buy price + writeLEShort(offer.quantity,buffer); // buy qty + count++; + } + writeBufferedOffers(out,buffer.toByteArray(),commodity,count); + } - /** - * Writes the buffered data to the output strea for one commodity. - * - * @param out the stream to write to - * @param buffer the buffered data to write - * @param commodity the commmodity id to write before the buffered data - * @param count the number of offers for this commodity to write before the data - */ - private void writeBufferedOffers(OutputStream out, byte[] buffer, int commodity, int count) throws IOException { - writeLEShort(commodity,out); // commod index - writeLEShort(count,out); // offer count - out.write(buffer); // the buffered offers - } + /** + * Writes the buffered data to the output strea for one commodity. + * + * @param out the stream to write to + * @param buffer the buffered data to write + * @param commodity the commmodity id to write before the buffered data + * @param count the number of offers for this commodity to write + * before the data + */ + private void writeBufferedOffers(OutputStream out, byte[] buffer, + int commodity, int count) + throws IOException { + writeLEShort(commodity,out); // commod index + writeLEShort(count,out); // offer count + out.write(buffer); // the buffered offers + } - /** - * Writes the buy and sell offers to the outputstream by calling other methods. - * - * @param buys list of Buy offers to write - * @param sells list of Sell offers to write - * @param offerCount 2-length int array containing the number of buys and sells to write out - * @param out the stream to write to - */ - private void writeBuySellOffers(TreeSet buys, - TreeSet sells, int[] offerCount, OutputStream out) throws IOException { - // # buy offers - writeLEShort(offerCount[0],out); - writeOffers(out,buys); - // # sell offers - writeLEShort(offerCount[1],out); - writeOffers(out,sells); - } + /** + * Writes the buy and sell offers to the outputstream by calling + * other methods. + * + * @param buys list of Buy offers to write + * @param sells list of Sell offers to write + * @param offerCount 2-length int array containing the number of + * buys and sells to write out + * @param out the stream to write to + */ + private void writeBuySellOffers(TreeSet buys, + TreeSet sells, + int[] offerCount, OutputStream out) + throws IOException { + // # buy offers + writeLEShort(offerCount[0],out); + writeOffers(out,buys); + // # sell offers + writeLEShort(offerCount[1],out); + writeOffers(out,sells); + } - private String readstreamstring(InputStream in) throws IOException { - StringBuilder sb = new StringBuilder(); - BufferedReader br = new BufferedReader(new InputStreamReader(in)); - String str; - while((str = br.readLine()) != null) { - sb.append(str+"\n"); - } - return sb.toString(); - } - - /** - * Sends the data to the server via multipart-formdata POST, - * with the gzipped data as a file upload. - * - * @param file an InputStream open to the gzipped data we want to send - */ - private InputStream sendInitialData(InputStream file) throws IOException { - ClientHttpRequest http = new ClientHttpRequest(PCTB_HOST_URL + "upload.php"); - http.setParameter("marketdata","marketdata.gz",file,"application/gzip"); - if (!http.post()) { - String err = readstreamstring(http.resultstream()); - error("Error sending initial data:\n"+err); - return null; - } - return http.resultstream(); - } + private String readstreamstring(InputStream in) throws IOException { + StringBuilder sb = new StringBuilder(); + BufferedReader br = new BufferedReader(new InputStreamReader(in)); + String str; + while((str = br.readLine()) != null) { + sb.append(str+"\n"); + } + return sb.toString(); + } + + /** + * Sends the data to the server via multipart-formdata POST, + * with the gzipped data as a file upload. + * + * @param file an InputStream open to the gzipped data we want to send + */ + private InputStream sendInitialData(InputStream file) throws IOException { + on_our_thread(); + ClientHttpRequest http = + new ClientHttpRequest(PCTB_HOST_URL + "upload.php"); + http.setParameter("marketdata","marketdata.gz",file,"application/gzip"); + if (!http.post()) { + String err = readstreamstring(http.resultstream()); + error("Error sending initial data:\n"+err); + return null; + } + return http.resultstream(); + } - /** - * Utility method to write a 2-byte int in little-endian form to an output stream. - * - * @param num an integer to write - * @param out stream to write to - */ - private void writeLEShort(int num, OutputStream out) throws IOException { - out.write(num & 0xFF); - out.write((num >>> 8) & 0xFF); - } + /** + * Utility method to write a 2-byte int in little-endian form to + * an output stream. + * + * @param num an integer to write + * @param out stream to write to + */ + private void writeLEShort(int num, OutputStream out) throws IOException { + out.write(num & 0xFF); + out.write((num >>> 8) & 0xFF); + } - /** - * Reads the response from the server, and selects the correct parameters - * which are sent in a GET request to the server asking it to confirm - * the upload and accept the data into the database. Notably, the island id - * and ocean id are determined, while other parameter such as the filename - * are determined from the hidden form fields. - * - * @param in stream of data from the server to read - */ - private boolean finishUpload(InputStream in) throws IOException { - String html = readstreamstring(in); - debug_write_stringdata("pctb-initial.html", html); - Matcher m; - - Pattern params = Pattern.compile("(?s).+?.+?"); - m = params.matcher(html); - if(!m.find()) { - error_html("The PCTB server returned unusual data. Maybe you're using an old version of the uploader?", - html); - return false; - } - String forceReload = m.group(1); - String filename = m.group(2); - - Pattern oceanNumPat = Pattern.compile(""); - m = oceanNumPat.matcher(html); - if (!m.find()) { - error_html("Unable to find the ocean in the server's list of oceans!", html); - return false; - } - String oceanNum = m.group(1); - - Pattern oceanIslandNum = Pattern.compile("islands\\[" + oceanNum + "\\]\\[\\d+\\]=new Option\\(\"" + islandName + "\",(\\d+)"); - m = oceanIslandNum.matcher(html); - if(!m.find()) { - error_html("This does not seem to be a valid island! Unable to upload.", html); - return false; - } - String islandNum = m.group(1); - - URL get = new URL(PCTB_HOST_URL + "upload.php?action=setisland&ocean=" + oceanNum + "&island=" - + islandNum + "&forcereload=" + forceReload + "&filename=" + filename); - String complete = readstreamstring(get.openStream()); - debug_write_stringdata("pctb-final.html", complete); - Pattern done = Pattern.compile("Your data has been integrated into the database. Thank you!"); - m = done.matcher(complete); - if(m.find()) { - return true; - } else { - error_html("Something was wrong with the final upload parameters!", complete); - return false; - } - } - - private InputStream post_for_yarrg(ClientHttpRequest http) throws IOException { - if (!http.post()) { - String err = readstreamstring(http.resultstream()); - error("

Error reported by YARRG server

\n" + err); - return null; - } - return http.resultstream(); + /** + * Reads the response from the server, and selects the correct parameters + * which are sent in a GET request to the server asking it to confirm + * the upload and accept the data into the database. Notably, the island id + * and ocean id are determined, while other parameter such as the filename + * are determined from the hidden form fields. + * + * @param in stream of data from the server to read + */ + private boolean finishUpload(InputStream in) throws IOException { + on_our_thread(); + + String html = readstreamstring(in); + debug_write_stringdata("pctb-initial.html", html); + Matcher m; + + Pattern params = Pattern.compile + ("(?s)"+ + ".+?"+ + ".+?"); + m = params.matcher(html); + if(!m.find()) { + error_html("The PCTB server returned unusual data."+ + " Maybe you're using an old version of the uploader?", + html); + return false; } - - private String getYarrgTimestamp() throws IOException { - ClientHttpRequest http = new ClientHttpRequest (YARRG_URL); - http.setParameter("clientname", YARRG_CLIENTNAME); - http.setParameter("clientversion", YARRG_CLIENTVERSION); - http.setParameter("clientfixes", YARRG_CLIENTFIXES); - http.setParameter("requesttimestamp", "y"); - InputStream in = post_for_yarrg(http); - if (in == null) return null; - BufferedReader br = new BufferedReader(new InputStreamReader(in)); - String tsresult = br.readLine(); - return tsresult.substring(3, tsresult.length()-1); + String forceReload = m.group(1); + String filename = m.group(2); + + Pattern oceanNumPat = + Pattern.compile(""); + m = oceanNumPat.matcher(html); + if (!m.find()) { + error_html("Unable to find the ocean in the server's list of oceans!", + html); + return false; } - - private boolean runYarrg(String timestamp, String ocean, String island, String yarrgdata) throws IOException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - BufferedOutputStream bufos = new BufferedOutputStream(new GZIPOutputStream(bos)); - bufos.write(yarrgdata.getBytes() ); - bufos.close(); - byte[] compressed = bos.toByteArray(); - debug_write_bytes("yarrg-deduped.tsv.gz", compressed); - ByteArrayInputStream file = new ByteArrayInputStream(compressed); - - ClientHttpRequest http = new ClientHttpRequest (YARRG_URL); - http.setParameter("clientname", YARRG_CLIENTNAME); - http.setParameter("clientversion", YARRG_CLIENTVERSION); - http.setParameter("clientfixes", YARRG_CLIENTFIXES); - http.setParameter("timestamp", timestamp); - http.setParameter("ocean", ocean); - http.setParameter("island", island); - http.setParameter("data", "deduped.tsv.gz", file, "application/octet-stream"); - InputStream in = post_for_yarrg(http); - if (in == null) return false; - String output = readstreamstring(in); - if (!output.startsWith("OK")) { - error("

Unexpected output from YARRG server

\n" + output); - return false; - } - debug_write_stringdata("yarrg-result.txt", output); - return true; + String oceanNum = m.group(1); + + Pattern oceanIslandNum = + Pattern.compile("islands\\[" + oceanNum + + "\\]\\[\\d+\\]=new Option\\(\"" + islandName + + "\",(\\d+)"); + m = oceanIslandNum.matcher(html); + if(!m.find()) { + error_html("This does not seem to be a valid island! Unable to upload.", + html); + return false; + } + String islandNum = m.group(1); + + URL get = new URL(PCTB_HOST_URL + + "upload.php?action=setisland&ocean=" + oceanNum + + "&island=" + islandNum + + "&forcereload=" + forceReload + + "&filename=" + filename); + String complete = readstreamstring(get.openStream()); + debug_write_stringdata("pctb-final.html", complete); + Pattern done = Pattern.compile + ("Your data has been integrated into the database. Thank you!"); + m = done.matcher(complete); + if(m.find()) { + return true; + } else { + error_html("Something was wrong with the final upload parameters!", + complete); + return false; } + } - private int calculateArbitrageCommodity(ArrayList> arb_bs) { - // if (dtxt!=null) dtxt.println("ARBITRAGE?"); - int profit = 0; - SortedSet buys = arb_bs.get(0); - SortedSet sells = arb_bs.get(1); - while (true) { - int[] buy, sell; - try { - // NB "sell" means they sell, ie we buy - sell = sells.last(); - buy = buys.first(); - } catch (NoSuchElementException e) { - break; - } - int unitprofit = buy[0] - sell[0]; - int count = buy[1] < sell[1] ? buy[1] : sell[1]; - // if (dtxt!=null) dtxt.println(" sell @"+sell[0]+" x"+sell[1]+" buy @"+buy[0]+" x"+buy[1] - // +" => x"+count+" @"+unitprofit); + /***************************************** + * YARRG-SPECIFIC HELPER FUNCTIONS ETC. * + *****************************************/ - if (unitprofit <= 0) - break; - - profit += count * unitprofit; - buy[1] -= count; - sell[1] -= count; - if (buy[1]==0) buys.remove(buy); - if (sell[1]==0) sells.remove(sell); - } - // if (dtxt!=null) dtxt.println(" PROFIT "+profit); - return profit; + private InputStream post_for_yarrg(ClientHttpRequest http) + throws IOException { + on_our_thread(); + if (!http.post()) { + String err = readstreamstring(http.resultstream()); + error("

Error reported by YARRG server

\n" + err); + return null; } + return http.resultstream(); + } - private class arbitrageOfferComparator implements Comparator { - public int compare(Object o1, Object o2) { - int p1 = ((int[])o1)[0]; - int p2 = ((int[])o2)[0]; - return p2 - p1; - } + private class YarrgTimestampFetcher extends Thread { + public YarrgTimestampFetcher(int counter) { + super("MarketUploader-YarrgTimestampFetcher-"+uploadcounter); } - - private @SuppressWarnings("unchecked") - void calculateArbitrage(ArrayList> data) { - int arbitrage = 0; - ArrayList> arb_bs = null; - String lastcommod = null; - Comparator compar = new arbitrageOfferComparator(); - - for (ArrayList row : data) { - String thiscommod = row.get(0); - // if (dtxt!=null) dtxt.println("ROW "+row.toString()); - if (lastcommod == null || !thiscommod.equals(lastcommod)) { - if (lastcommod != null) - arbitrage += calculateArbitrageCommodity(arb_bs); - // if (dtxt!=null) dtxt.println("ROW rdy"); - arb_bs = new ArrayList>(2); - arb_bs.add(0, new TreeSet(compar)); - arb_bs.add(1, new TreeSet(compar)); - // if (dtxt!=null) dtxt.println("ROW init"); - lastcommod = thiscommod; - } - for (int bs = 0; bs < 2; bs++) { - String pricestr = row.get(bs*2 + 2); - if (pricestr == null) - continue; - int[] entry = new int[2]; - // if (dtxt!=null) dtxt.println("ROW BS "+bs); - entry[0] = parseQty(pricestr); - entry[1] = parseQty(row.get(bs*2 + 3)); - arb_bs.get(bs).add(entry); - } - } - arbitrage += calculateArbitrageCommodity(arb_bs); - if (arbitrage != 0) { - arbitrageResult.setText("arbitrage: "+arbitrage+" poe"); - } else { - arbitrageResult.setText("no arbitrage"); - } + public String ts = null; + public void run() { + try { + ts = getYarrgTimestamp(); + debuglog("(async) yarrg timestamp ready."); + } catch(Exception e) { + error("Error getting YARRG timestamp: "+e); + } + } + }; + + private String getYarrgTimestamp() throws IOException { + ClientHttpRequest http = new ClientHttpRequest (YARRG_URL); + http.setParameter("clientname", YARRG_CLIENTNAME); + http.setParameter("clientversion", YARRG_CLIENTVERSION); + http.setParameter("clientfixes", YARRG_CLIENTFIXES); + http.setParameter("requesttimestamp", "y"); + InputStream in = post_for_yarrg(http); + if (in == null) return null; + BufferedReader br = new BufferedReader(new InputStreamReader(in)); + String tsresult = br.readLine(); + return tsresult.substring(3, tsresult.length()-1); + } + + private boolean runYarrg(String timestamp, String ocean, String island, + String yarrgdata) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + BufferedOutputStream bufos = + new BufferedOutputStream(new GZIPOutputStream(bos)); + bufos.write(yarrgdata.getBytes() ); + bufos.close(); + byte[] compressed = bos.toByteArray(); + debug_write_bytes("yarrg-deduped.tsv.gz", compressed); + ByteArrayInputStream file = new ByteArrayInputStream(compressed); + + ClientHttpRequest http = new ClientHttpRequest (YARRG_URL); + http.setParameter("clientname", YARRG_CLIENTNAME); + http.setParameter("clientversion", YARRG_CLIENTVERSION); + http.setParameter("clientfixes", YARRG_CLIENTFIXES); + http.setParameter("timestamp", timestamp); + http.setParameter("ocean", ocean); + http.setParameter("island", island); + http.setParameter("data", "deduped.tsv.gz", file, + "application/octet-stream"); + InputStream in = post_for_yarrg(http); + if (in == null) return false; + String output = readstreamstring(in); + if (!output.startsWith("OK")) { + error("

Unexpected output from YARRG server

\n" + output); + return false; + } + debug_write_stringdata("yarrg-result.txt", output); + return true; + } + + private int calculateArbitrageCommodity(ArrayList> arb_bs) { + // debuglog("ARBITRAGE?"); + int profit = 0; + SortedSet buys = arb_bs.get(0); + SortedSet sells = arb_bs.get(1); + while (true) { + int[] buy, sell; + try { + // NB "sell" means they sell, ie we buy + sell = sells.last(); + buy = buys.first(); + } catch (NoSuchElementException e) { + break; + } + + int unitprofit = buy[0] - sell[0]; + int count = buy[1] < sell[1] ? buy[1] : sell[1]; + // debuglog(" sell @"+sell[0]+" x"+sell[1] + // +" buy @"+buy[0]+" x"+buy[1] + // +" => x"+count+" @"+unitprofit); + + if (unitprofit <= 0) + break; + + profit += count * unitprofit; + buy[1] -= count; + sell[1] -= count; + if (buy[1]==0) buys.remove(buy); + if (sell[1]==0) sells.remove(sell); + } + // debuglog(" PROFIT "+profit); + return profit; + } + + /***************************************** + * ARBITRAGE * + *****************************************/ + + private class arbitrageOfferComparator implements Comparator { + public int compare(Object o1, Object o2) { + int p1 = ((int[])o1)[0]; + int p2 = ((int[])o2)[0]; + return p2 - p1; + } + } + + private @SuppressWarnings("unchecked") + void calculateArbitrage(ArrayList> data) + throws InterruptedException { + int arbitrage = 0; + ArrayList> arb_bs = null; + String lastcommod = null; + Comparator compar = new arbitrageOfferComparator(); + + for (ArrayList row : data) { + String thiscommod = row.get(0); + // debuglog("ROW "+row.toString()); + if (lastcommod == null || !thiscommod.equals(lastcommod)) { + if (lastcommod != null) + arbitrage += calculateArbitrageCommodity(arb_bs); + // debuglog("ROW rdy"); + arb_bs = new ArrayList>(2); + arb_bs.add(0, new TreeSet(compar)); + arb_bs.add(1, new TreeSet(compar)); + // debuglog("ROW init"); + lastcommod = thiscommod; + } + for (int bs = 0; bs < 2; bs++) { + String pricestr = row.get(bs*2 + 2); + if (pricestr == null) + continue; + int[] entry = new int[2]; + // debuglog("ROW BS "+bs); + entry[0] = parseQty(pricestr); + entry[1] = parseQty(row.get(bs*2 + 3)); + arb_bs.get(bs).add(entry); + } + } + arbitrage += calculateArbitrageCommodity(arb_bs); + String arb; + if (arbitrage != 0) { + arb = "arbitrage: "+arbitrage+" poe"; + } else { + arb = "no arbitrage"; } + final String arb_final = arb; + EventQueue.invokeLater(new Runnable() { public void run() { + arbitrageResult.setText(arb_final); + }}); + } }