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.
-*/
+/*
+ * 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 int unknownPCTBcommods = 0;
- private long startTime = 0;
+ 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
+ // YARRG protocol parameters
private final static String YARRG_CLIENTNAME = "jpctb greenend";
private final static String YARRG_CLIENTVERSION =
net.chiark.yarrg.Version.version;
"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<String,Integer> 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();
- }
- }
- };
+ /*****************************************
+ * UPLOAD-TARGET-INDEPENDENT CODE *
+ *****************************************/
+
+
+ /*
+ * UTILITY METHODS AND SUBCLASSES
+ *
+ * Useable on any thread.
+ *
+ */
private int parseQty(String str) {
if (str.equals(">1000")) {
return 1001;
}
}
- private void progresslog(String s) {
+ private void debuglog(String s) {
if (dtxt == null) return;
long now = new Date().getTime();
- dtxt.println("progress "+(now - startTime)+"ms "+s);
+ dtxt.println("progress "+(now - startTime)+"ms "
+ +Thread.currentThread().getName()+": "+s);
}
private void debug_write_stringdata(String what, String 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 = "<html>" + arb + "<br>" + 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.
+ /*
+ * ENTRY POINT AND STARTUP
*
- * @see Buy
- * @see Sell
- */
- abstract class Offer {
- public int commodity, price, quantity, shoppe;
- /**
- * Create an offer from <code>record</code>, determining the shoppe Id from
- * <code>stallMap</code> and the commodity Id from <code>commodMap</code>.
- * <code>priceIndex</code> should be the index of the price in the record
- * (the quantity will be <code>priceIndex + 1</code>).
- *
- * @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<String> record,
- LinkedHashMap<String,Integer> stallMap,
- HashMap<String,Integer> 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.
+ * Main thread and/or event thread
*/
- class Buy extends Offer implements Comparable<Buy> {
- /**
- * Creates a new <code>Buy</code> offer from the given
- * <code>record</code> 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<String> record,
- LinkedHashMap<String,Integer> stallMap,
- HashMap<String,Integer> 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;
- }
- }
- }
-
- /**
- * 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<Sell> {
- /**
- * Creates a new <code>Sell</code> offer from the given
- * <code>record</code> 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<String> record,
- LinkedHashMap<String,Integer> stallMap,
- HashMap<String,Integer> 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 static void main(String[] args) {
+ // This is not normally called, it seems.
+ new MarketUploader();
}
-
- /**
- * Entry point. Read our preferences.
- */
+
public MarketUploader() {
Preferences prefs = Preferences.userNodeForPackage(getClass());
uploadToPCTB=prefs.getBoolean("uploadToPCTB", true);
showArbitrage=prefs.getBoolean("showArbitrage", true);
- if (dtxt!=null) dtxt.println("main on dispatch thread: "+
- EventQueue.isDispatchThread());
+ 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, and if it
+ * every top-level window to see if the Puzzle Pirates window turns up.
*/
public void run() {
- if (dtxt!=null) dtxt.println("MarketUploader run()...");
+ debuglog("MarketUploader run()...");
if (EventQueueMonitor.isGUIInitialized()) {
- if (dtxt!=null) dtxt.println("MarketUploader GUI already ready");
+ debuglog("MarketUploader GUI already ready");
guiInitialized();
} else {
- if (dtxt!=null) dtxt.println("MarketUploader waiting for GUI");
+ debuglog("MarketUploader waiting for GUI");
EventQueueMonitor.addGUIInitializedListener(this);
}
}
Window ws[]= EventQueueMonitor.getTopLevelWindows();
EventQueueMonitor.addTopLevelWindowListener(this);
for (int i=0; i<ws.length; i++) {
- if (dtxt!=null) dtxt.println("MarketUploader existing toplevel "+i);
+ debuglog("MarketUploader existing toplevel "+i);
topLevelWindowCreated(ws[i]);
}
}
public void topLevelWindowDestroyed(Window w) {
- if (dtxt!=null) dtxt.println("MarketUploader destroyed toplevel");
+ debuglog("MarketUploader destroyed toplevel");
}
public void topLevelWindowCreated(Window w) {
// already got it
return;
String name = w.getAccessibleContext().getAccessibleName();
- if (dtxt!=null) dtxt.println("MarketUploader new toplevel "+name);
+ debuglog("MarketUploader checking toplevel "+name);
if (!name.equals("Puzzle Pirates"))
+ // Only if we're running alongside a Window named "Puzzle Pirates"
return;
- if (dtxt!=null) dtxt.println("MarketUploader found toplevel, creating gui");
+ debuglog("MarketUploader found toplevel, creating gui");
window = w;
createGUI();
frame.setVisible(true);
}
- /**
- * 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() {
+ // 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);
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);
- new Thread() {
+ resultSummary.setText("");
+ arbitrageResult.setText("");
+ new Thread("MarketUploader-uploader-"+uploadcounter) {
public void run() {
startTime = new Date().getTime();
- resultSummary.setText("");
- arbitrageResult.setText("");
unknownPCTBcommods = 0;
try {
- runUpload();
+ runUpload(uploadcounter);
} 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);
+ 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();
}
frame.pack();
}
-
- /**
- * 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
+ /*
+ * 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.
+ */
- 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();
- } catch (NullPointerException e) {
- // evidently we're actually on an island
+ private void on_ui_thread() { assert(EventQueue.isDispatchThread()); }
+ private void on_our_thread() { assert(!EventQueue.isDispatchThread()); }
- 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();
+ 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<ReturnType> 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 {
- 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<c;i++) {
- if("notify-field-accept".equals(chatArea.getAccessibleAction()
- .getAccessibleActionDescription(i))) {
- chatArea.getAccessibleAction().doAccessibleAction(i);
- }
+ debuglog("UIX 1 "+what+" (other thread) entry");
+ EventQueue.invokeAndWait(this);
+ debuglog("UIX 4 "+what+" (other thread) exit");
}
+ return return_value;
}
- }
-
- /**
- * 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");
- }
-
+ };
+ private abstract class UIX extends UIXR<Object> implements Runnable {
+ public abstract void body();
+ public Object bodyr() { body(); return null; }
+ };
- /**
- * Shows a dialog with the error <code>msg</code>.
+ /*
+ * ERROR REPORTING AND GENERAL UTILITIES
*
- * @param msg a String describing the error that occured.
- */
- private void error(String msg) {
- JOptionPane.showMessageDialog(frame,msg,"Error",JOptionPane.ERROR_MESSAGE);
+ * 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();
+ }
}
- private void error_html(String msg, String html) {
+ public void error_html(final String msg, String html) {
Pattern body = Pattern.compile("<body>(.*)</body>",
Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
Matcher m = body.matcher(html);
}
String whole_msg = "<html><h1>Error</h1>"+msg
+"<h1>PCTB Server said:</h1><blockquote>"+html+"</blockquote>";
- if (dtxt!=null) dtxt.println("###" + whole_msg + "###");
+ debuglog("###" + whole_msg + "###");
+
+ error(whole_msg);
+ }
- JOptionPane.showMessageDialog(frame,whole_msg,"Error",
- JOptionPane.ERROR_MESSAGE);
+ 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 = "<html>" + arb + "<br>" + s;
+ progmon.setNote(s);
+ }}.exec("progressNote "+s_in);
}
-
- /**
- * Run the data collection process, and upload the results. This
- * is the method that calls most of the other worker methods for
- * the process. If an error occurs, the method will call the
- * error method and return early, freeing up the button to be
- * clicked again.
- *
- * @exception Exception if an error we didn't expect occured
+ 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<Boolean>() { 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 class YarrgTimestampFetcher extends Thread {
- public String ts = null;
- public void run() {
- try {
- ts = getYarrgTimestamp();
- progresslog("(async) yarrg timestamp ready.");
- } catch(Exception e) {
- error("Error getting YARRG timestamp: "+e);
- }
- }
- };
- private void runUpload() throws Exception {
- progresslog("starting");
+ 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();
- ProgressMonitor pm = new ProgressMonitor
- (frame,"Processing Market Data","Getting table data",0,100);
- pm.setMillisToDecideToPopup(0);
- pm.setMillisToPopup(0);
boolean doneyarrg = false, donepctb = false;
YarrgTimestampFetcher yarrgts_thread = null;
+ debuglog("starting");
+
if (uploadToYarrg) {
- progresslog("(async) yarrg timestamp...");
- yarrgts_thread = new YarrgTimestampFetcher();
+ debuglog("(async) yarrg timestamp...");
+ yarrgts_thread = new YarrgTimestampFetcher(counter);
yarrgts_thread.start();
}
- AccessibleTable accesstable = findMarketTable();
- if(accesstable == null) {
- error("Market table not found!"+
- " Please open the Buy/Sell Commodities interface.");
- return;
- }
- if(accesstable.getAccessibleRowCount() == 0) {
- error("No data found, please wait for the table to have data first!");
- return;
- }
- if(!isDisplayAll()) {
- error("Please select \"All\" from the Display: popup menu.");
- return;
- }
+ final AccessibleTable accesstable =
+ new UIXR<AccessibleTable>() { 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");
- progresslog("(async) getisland...");
- getIsland();
- progresslog("getocean...");
- getOcean();
- progresslog("getocean done");
+ return at;
+ }}.exec("accesstable");
+ if (accesstable == null) return;
if (latch != null) {
latch.await(2, java.util.concurrent.TimeUnit.SECONDS);
}
- progresslog("(async) getisland done");
+ debuglog("(async) getisland done");
String yarrgts = null;
if (yarrgts_thread != null) {
- progresslog("(async) yarrg timestamp join...");
+ debuglog("(async) yarrg timestamp join...");
yarrgts_thread.join();
- progresslog("(async) yarrg timestamp joined.");
+ debuglog("(async) yarrg timestamp joined.");
yarrgts = yarrgts_thread.ts;
}
return;
}
- progresslog("table check...");
+ debuglog("table check...");
- String headings_expected[] = new String[]
- { "Commodity", "Trading outlet", "Buy price",
- "Will buy", "Sell price", "Will sell" };
- ArrayList<ArrayList<String>> 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<headings_expected.length; col++) {
- String expd = headings_expected[col];
- String got = headers.get(0).get(col);
- if (expd.compareTo(got) != 0) {
- error("Table heading for column "+col
- +" is not \""+expd+"\" but \""+got+"\".\n\n"
- +"Please do not reorder the table when using this tool.");
- return;
+ final ArrayList<ArrayList<String>> data =
+ new UIXR<ArrayList<ArrayList<String>>>
+ () { public ArrayList<ArrayList<String>> bodyr() {
+ String headings_expected[] = new String[]
+ { "Commodity", "Trading outlet", "Buy price",
+ "Will buy", "Sell price", "Will sell" };
+
+ ArrayList<ArrayList<String>> 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<headings_expected.length; col++) {
+ String expd = headings_expected[col];
+ String got = headers.get(0).get(col);
+ if (expd.compareTo(got) != 0) {
+ error("Table heading for column "+col
+ +" is not \""+expd+"\" but \""+got+"\".\n\n"
+ +"Please do not reorder the table when using this tool.");
+ return null;
+ }
}
- }
- progresslog("table read...");
+ debuglog("table read...");
- ArrayList<ArrayList<String>> data = getData(accesstable);
+ return getData(accesstable);
+ }}.exec("data");
+ if (data == null) return;
if (showArbitrage) {
- progresslog("arbitrage...");
+ debuglog("arbitrage...");
calculateArbitrage(data);
- progresslog("arbitrage done.");
+ debuglog("arbitrage done.");
}
if (uploadToYarrg && yarrgts != null) {
- progresslog("yarrg prepare...");
- progressNote(pm, "Yarrg: Preparing data");
- pm.setProgress(10);
+ debuglog("yarrg prepare...");
+ progressNote("Yarrg: Preparing data");
+ setProgress(10);
StringBuilder yarrgsb = new StringBuilder();
String yarrgdata; // string containing what we'll feed to yarrg
yarrgdata = yarrgsb.toString();
- progressNote(pm, "Yarrg: Uploading");
- progresslog("yarrg upload...");
+ progressNote("Yarrg: Uploading");
+ debuglog("yarrg upload...");
doneyarrg = runYarrg(yarrgts, oceanName, islandName, yarrgdata);
- progresslog("yarrg done.");
+ debuglog("yarrg done.");
}
if (uploadToPCTB) {
- progresslog("pctb prepare...");
- progressNote(pm, "PCTB: Getting stall names");
- pm.setProgress(20);
- if(pm.isCanceled()) {
+ debuglog("pctb prepare...");
+ progressNote("PCTB: Getting stall names");
+ setProgress(20);
+ if(checkCancelled()) {
return;
}
TreeSet<Offer> buys = new TreeSet<Offer>();
TreeSet<Offer> sells = new TreeSet<Offer>();
LinkedHashMap<String,Integer> stallMap = getStallMap(data);
- pm.setProgress(40);
- progressNote(pm, "PCTB: Sorting offers");
- if(pm.isCanceled()) {
+ setProgress(40);
+ progressNote("PCTB: Sorting offers");
+ if(checkCancelled()) {
return;
}
// get commod map
- progresslog("pctb commodmap...");
+ debuglog("pctb commodmap...");
HashMap<String,Integer> commodMap = getCommodMap();
if(commodMap == null) {
return;
}
- progresslog("pctb commodmap done.");
+ debuglog("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);
+ // debuglog(sells);
+ // debuglog("\n\n\n"+buys);
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
- pm.setProgress(60);
- progressNote(pm, "PCTB: Sending data");
- if(pm.isCanceled()) {
+ setProgress(60);
+ progressNote("PCTB: Sending data");
+ if(checkCancelled()) {
return;
}
GZIPOutputStream out = new GZIPOutputStream(outStream);
dos.writeBytes(getAbbrevStallList(stallMap));
writeBuySellOffers(buys,sells,offerCount,out);
out.finish();
- progresslog("pctb send...");
+ debuglog("pctb send...");
byte[] ba = outStream.toByteArray();
debug_write_bytes("pctb-marketdata.gz", ba);
InputStream in = sendInitialData(new ByteArrayInputStream(ba));
- progresslog("pctb sent.");
+ debuglog("pctb sent.");
if (in == null) return;
- pm.setProgress(80);
- if(pm.isCanceled()) {
+ setProgress(80);
+ if(checkCancelled()) {
return;
}
- progressNote(pm, "PCTB: Waiting ...");
- progresslog("pctb finish...");
+ progressNote("PCTB: Waiting ...");
+ debuglog("pctb finish...");
donepctb = finishUpload(in);
- progresslog("pctb done.");
+ debuglog("pctb done.");
}
- pm.setProgress(100);
+ setProgress(99);
+ String summary;
if ((uploadToPCTB && !donepctb) ||
(uploadToYarrg && !doneyarrg)) {
- resultSummary.setText("trouble");
+ summary= "trouble";
} else if (unknownPCTBcommods != 0) {
- resultSummary.setText("PCTB lacks "+unknownPCTBcommods+" commod");
+ summary= "PCTB lacks "+unknownPCTBcommods+" commod(s)";
} else if (donepctb || doneyarrg) {
- resultSummary.setText("Done " + islandName);
+ summary= "Done " + islandName;
} else {
- resultSummary.setText("uploaded nowhere!");
+ summary= "uploaded nowhere!";
}
- progresslog("done.");
- }
-
- /**
- * Get the offer data out of the table and cache it in an
- * <code>ArrayList</code>.
- *
- * @param table the <code>AccessibleTable</code> containing the market data
- * @return an array of record arrays, each representing a row of the table
+ 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
*/
+
private ArrayList<ArrayList<String>> getData(AccessibleTable table) {
+ // Gets the offer data out of the table and returns it as an ArrayList
+
+ on_ui_thread();
ArrayList<ArrayList<String>> data = new ArrayList<ArrayList<String>>();
for (int i = 0; i < table.getAccessibleRowCount(); i++) {
ArrayList<String> row = new ArrayList<String>();
return data;
}
- /**
- * @return the table containing market data if it exists,
- * otherwise <code>null</code>
- */
public AccessibleTable findMarketTable() {
+ // Return the table containing market data if it exists, otherwise null.
+ on_ui_thread();
+
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);
+ // debuglog(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);
+ // debuglog(table);
return table;
}
- /**
- * Utility method to descend through several levels of Accessible children
- * at once.
+ 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;
+ }
+
+ /*
+ * 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<c;i++) {
+ if("notify-field-accept".equals(chatArea.getAccessibleAction()
+ .getAccessibleActionDescription(i))) {
+ chatArea.getAccessibleAction().doAccessibleAction(i);
+ }
+ }
+ }
+ }
+
+ private PropertyChangeListener changeListener = new PropertyChangeListener() {
+ // used by getIsland
+ public void propertyChange(PropertyChangeEvent e) {
+ on_ui_thread();
+ 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;
+ // debuglog(islandName);
+ sidePanel.removePropertyChangeListener(this);
+ latch.countDown();
+ }
+ }
+ };
+
+ /*
+ * UTILITY FUNCTIONS FOR WALKING THE UI
*
- * @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 <code>Accessible</code> reached by following the
- * descent path, or <code>null</code> if the desired path was
- * invalid.
+ * These functions all return null if the specified path or child
+ * was not found.
*/
+
private Accessible descendNodes(Accessible parent, int[] path) {
+ // Descends through several levels of Accessible children in one call.
+ // path[] is an array of ints, each int being the index into the array
+ // of children at a particular point, and thus selects the specific
+ // accessible child to descend to.
+ on_ui_thread();
+
for(int i=0;i<path.length;i++) {
if (null == (parent = descend(parent, path[i]))) return null;
}
return parent;
}
-
- /**
- * Descends one level to the specified child of the parent
- * <code>Accessible</code> "node".
- *
- * @param parent the node with children
- * @param childNum the index of the child of <code>parent</code> to return
- * @return the <code>childNum</code> child of <code>parent</code>
- * or <code>null</code> if the child is not found.
- */
+
private Accessible descend(Accessible parent, int childNum) {
+ // Descends one level to the specified child of the parent
+ // childNum is the index of the child within parent
+ on_ui_thread();
+
if (parent == null) return null;
int children = parent.getAccessibleContext().getAccessibleChildrenCount();
if (childNum >= children) {
- if (dtxt!=null) dtxt.println("DESCEND "+childNum+" > "
- +children+" NOT FOUND");
+ debuglog("DESCEND "+childNum+" > "+children+" NOT FOUND");
return null;
}
Accessible child = parent.getAccessibleContext()
.getAccessibleChild(childNum);
- if (dtxt!=null) dtxt.println("DESCEND "+childNum+" "
- +child.getClass().getName()+" OK");
+ debuglog("DESCEND "+childNum+" "+child.getClass().getName()+" OK");
return child;
}
- /**
- * Descends one level to the child which has the specified class.
- *
- * @param parent the node with children
- * @param classname the name of the class, as a string
- * @return the child or <code>null</code> if the child is not found.
- */
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; i<children; i++) {
Accessible child = ac.getAccessibleChild(i);
if (child.getClass().getName() == classname) {
- if (dtxt!=null) dtxt.println("DESCEND CLASS "+classname+" OK");
+ debuglog("DESCEND CLASS "+classname+" OK");
return child;
}
}
- if (dtxt!=null) dtxt.println("DESCEND CLASS "+classname+" NOT FOUND");
+ debuglog("DESCEND CLASS "+classname+" NOT FOUND");
return null;
}
+
- public static void main(String[] args) {
- new MarketUploader();
- }
+ /*****************************************
+ * PCTB-SPECIFIC HELPER FUNCTIONS ETC. *
+ *****************************************/
/**
- * Returns true if the "Display:" menu on the commodities
- * interface in YPP is set to "All"
+ * An abstract market offer, entailing a commodity being bought or sold by
+ * a shoppe, for a certain price in a certain quantity. Not instantiable.
*
- * @return <code>true</code> if all commodities are displayed,
- * otherwise <code>false</code>
+ * @see Buy
+ * @see Sell
*/
- 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});
+ abstract class Offer {
+ public int commodity, price, quantity, shoppe;
+ /**
+ * Create an offer from <code>record</code>, determining the shoppe Id from
+ * <code>stallMap</code> and the commodity Id from <code>commodMap</code>.
+ * <code>priceIndex</code> should be the index of the price in the record
+ * (the quantity will be <code>priceIndex + 1</code>).
+ *
+ * @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<String> record,
+ LinkedHashMap<String,Integer> stallMap,
+ HashMap<String,Integer> 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();
}
- String display = button.getAccessibleContext().getAccessibleName();
- if(!display.equals("All")) {
- return false;
+
+ /**
+ * 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<Buy> {
+ /**
+ * Creates a new <code>Buy</code> offer from the given
+ * <code>record</code> 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<String> record,
+ LinkedHashMap<String,Integer> stallMap,
+ HashMap<String,Integer> 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;
+ }
}
- return true;
}
+ /**
+ * 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<Sell> {
+ /**
+ * Creates a new <code>Sell</code> offer from the given
+ * <code>record</code> 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<String> record,
+ LinkedHashMap<String,Integer> stallMap,
+ HashMap<String,Integer> 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.
*
* the commodity id.
*/
private HashMap<String,Integer> getCommodMap() {
+ on_our_thread();
if(commodMap != null) {
return commodMap;
}
}
} catch(IllegalArgumentException e) {
unknownPCTBcommods++;
- if (dtxt!=null) dtxt.println("Error: Unsupported Commodity \""
- + offer.get(0) + "\"");
+ debuglog("Error: Unsupported Commodity \"" + offer.get(0) + "\"");
}
}
if (buySellCount[0]==0 && buySellCount[1]==0) {
* @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");
* @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;
}
}
+
+ /*****************************************
+ * YARRG-SPECIFIC HELPER FUNCTIONS ETC. *
+ *****************************************/
+
private InputStream post_for_yarrg(ClientHttpRequest http)
throws IOException {
+ on_our_thread();
if (!http.post()) {
String err = readstreamstring(http.resultstream());
error("<html><h1>Error reported by YARRG server</h1>\n" + err);
return http.resultstream();
}
+ private class YarrgTimestampFetcher extends Thread {
+ public YarrgTimestampFetcher(int counter) {
+ super("MarketUploader-YarrgTimestampFetcher-"+uploadcounter);
+ }
+ 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);
}
private int calculateArbitrageCommodity(ArrayList<SortedSet<int[]>> arb_bs) {
- // if (dtxt!=null) dtxt.println("ARBITRAGE?");
+ // debuglog("ARBITRAGE?");
int profit = 0;
SortedSet<int[]> buys = arb_bs.get(0);
SortedSet<int[]> sells = arb_bs.get(1);
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);
+ // debuglog(" sell @"+sell[0]+" x"+sell[1]
+ // +" buy @"+buy[0]+" x"+buy[1]
+ // +" => x"+count+" @"+unitprofit);
if (unitprofit <= 0)
break;
if (buy[1]==0) buys.remove(buy);
if (sell[1]==0) sells.remove(sell);
}
- // if (dtxt!=null) dtxt.println(" PROFIT "+profit);
+ // debuglog(" PROFIT "+profit);
return profit;
}
+ /*****************************************
+ * ARBITRAGE *
+ *****************************************/
+
private class arbitrageOfferComparator implements Comparator {
public int compare(Object o1, Object o2) {
int p1 = ((int[])o1)[0];
}
private @SuppressWarnings("unchecked")
- void calculateArbitrage(ArrayList<ArrayList<String>> data) {
+ void calculateArbitrage(ArrayList<ArrayList<String>> data)
+ throws InterruptedException {
int arbitrage = 0;
ArrayList<SortedSet<int[]>> arb_bs = null;
String lastcommod = null;
for (ArrayList<String> row : data) {
String thiscommod = row.get(0);
- // if (dtxt!=null) dtxt.println("ROW "+row.toString());
+ // debuglog("ROW "+row.toString());
if (lastcommod == null || !thiscommod.equals(lastcommod)) {
if (lastcommod != null)
arbitrage += calculateArbitrageCommodity(arb_bs);
- // if (dtxt!=null) dtxt.println("ROW rdy");
+ // debuglog("ROW rdy");
arb_bs = new ArrayList<SortedSet<int[]>>(2);
arb_bs.add(0, new TreeSet<int[]>(compar));
arb_bs.add(1, new TreeSet<int[]>(compar));
- // if (dtxt!=null) dtxt.println("ROW init");
+ // debuglog("ROW init");
lastcommod = thiscommod;
}
for (int bs = 0; bs < 2; bs++) {
if (pricestr == null)
continue;
int[] entry = new int[2];
- // if (dtxt!=null) dtxt.println("ROW BS "+bs);
+ // 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) {
- arbitrageResult.setText("<html><strong>arbitrage: "+arbitrage
- +" poe</strong>");
+ arb = "<html><strong>arbitrage: "+arbitrage+" poe</strong>";
} else {
- arbitrageResult.setText("no arbitrage");
+ arb = "no arbitrage";
}
+ final String arb_final = arb;
+ EventQueue.invokeLater(new Runnable() { public void run() {
+ arbitrageResult.setText(arb_final);
+ }});
}
}