chiark / gitweb /
Add rum spice and allspice t origin/unstable
authorIan Jackson <ijackson@chiark.greenend.org.uk>
Fri, 7 Apr 2023 18:12:44 +0000 (19:12 +0100)
committerIan Jackson <ijackson@chiark.greenend.org.uk>
Fri, 7 Apr 2023 18:12:53 +0000 (19:12 +0100)
43 files changed:
.gitignore
yarrg/Commods.pm
yarrg/CommodsDatabase.pm
yarrg/CommodsScrape.pm
yarrg/README
yarrg/README.charset
yarrg/README.devel
yarrg/README.where-vessels
yarrg/commod-email-processor
yarrg/commod-results-processor
yarrg/commod-update-receiver
yarrg/convert.h
yarrg/database-info-fetch
yarrg/db-idempotent-populate
yarrg/decode-pctb-marketdata
yarrg/dictionary-update-receiver
yarrg/icons/fc.xbm [new file with mode: 0644]
yarrg/icons/vessel-info
yarrg/ocean-topology-graph
yarrg/source-info.txt
yarrg/update-master-info
yarrg/web/autohandler
yarrg/web/devel
yarrg/web/docs
yarrg/web/footer
yarrg/web/intro
yarrg/web/pirate-island
yarrg/web/profitable_trades
yarrg/web/qtextstringcheck
yarrg/web/query_age
yarrg/web/query_commod
yarrg/web/query_routesearch
yarrg/web/robots.txt [new file with mode: 0755]
yarrg/web/routetrade
yarrg/web/scraper
yarrg/web/upload
yarrg/where-vessels
yarrg/yppedia-chart-parser
yarrg/yppedia-ocean-scraper
yoweb-scrape
yoweb-scrape.txt
ypp-chart-memshow
ypp-chatlog-alerter [new file with mode: 0755]

index 0f48304d266f6722a14806ae84d4de3c3d81b315..4a7181ea968f25ca3946b5f7ee63dea7a21b4d5d 100644 (file)
@@ -3,6 +3,8 @@ t
 
 core
 vgcore.*
+*.orig
+*.rej
 
 yarrg/*.o
 yarrg/t.*
index bce9a8e13b2cb33afd05feb7ada3d2410aa5ea02..7f4061048c5e7360c4be9a62197d13d335ee76ff 100644 (file)
@@ -30,6 +30,8 @@ use LWP::UserAgent;
 use strict;
 use warnings;
 
+no warnings qw(exec);
+
 BEGIN {
     use Exporter ();
     our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS);
@@ -422,17 +424,18 @@ sub check_tsv_line ($$) {
     
     chomp($l) or &$bad_data('missing end-of-line');
 
-    $l !~ m/\P{IsPrint}/ or &$bad_data('nonprinting char(s)');
+    $l =~ m/[\t [:graph:]]/ or &$bad_data('nonprinting char(s) '.sprintf "%#x", ord $&);
     my @v= split /\t/, $l, -1;
     @v==6 or &$bad_data('wrong number of fields');
+    $v[1] =~ s/^\s+//; $v[1] =~ s/\s+$//; # ooo don't check :-(
     my ($commod,$stall) = @v;
 
     !keys %commods or
        defined $commods{$commod} or
        &$bad_data("unknown commodity ".errsan($commod));
     
-    $stall =~ m/^\p{IsUpper}|^[0-9]/ or
-       &$bad_data("stall not capitalised ".errsan($stall));
+    $stall =~ m/\p{IsAlnum}/ or
+       &$bad_data("stall does not contain with alphanumeric".errsan($stall));
     !exists $check_tsv_done{$commod,$stall} or
        &$bad_data("repeated data ".errsan($commod).",".errsan($stall));
     $check_tsv_done{$commod,$stall}= 1;
index 3cb543dc43b3866971cff2cb28e39de7f6061631..1f977d40b7cca3d40e8dd030b3a253165793512f 100644 (file)
@@ -35,6 +35,7 @@ use warnings;
 
 use DBI;
 use POSIX;
+use DBD::SQLite;
 
 use Commods;
 
@@ -64,10 +65,35 @@ sub dbr_connect ($$) {
 
 sub db_connect_core ($) {
     my ($fn)= @_;
-    my $h= DBI->connect("dbi:SQLite:$fn",'','',
-                      { AutoCommit=>0,
-                        RaiseError=>1, ShowErrorStatement=>1,
-                        unicode=>1 })
+    my $opts = { AutoCommit=>0,
+                RaiseError=>1, ShowErrorStatement=>1,
+                sqlite_unicode=>1 };
+
+    # DBI now wants to start a transaction whenever we even say
+    # SELECT.  But this doesn't work if the DB is readonly.  We can
+    # work around this by setting autocommit, in which case there is
+    # no need for a transaction for read-only db commands.  Autocommit
+    # is (obviously) safe with readonly operations.  But callers in
+    # yarrg do not specify to us whether they intend to write.  So we
+    # decide, by looking at the file mode.  And as belt-and-braces we
+    # set sqlite's own readonly flag as well.
+    # http://stackoverflow.com/questions/30082008/attempt-to-write-a-readonly-database-but-im-not
+    # http://stackoverflow.com/questions/35208727/can-sqlite-db-files-be-made-read-only
+    # http://cpansearch.perl.org/src/ISHIGAKI/DBD-SQLite-1.39/Changes
+    # (see entry for 1.38_01)
+    # http://stackoverflow.com/questions/17793672/perl-dbi-treats-setting-sqlite-db-cache-size-as-a-write-operation-when-subclassi
+    # https://rt.cpan.org/Public/Bug/Display.html?id=56444#
+    my $readonly =
+       (access $fn, POSIX::W_OK) ? 0 :
+       ($! == EACCES) ? 1 :
+       ($! == ENOENT) ? 0 :
+       die "$fn access(,W_OK) $!";
+    if ($readonly) {
+       $opts->{sqlite_open_flags} = DBD::SQLite::OPEN_READONLY;
+       $opts->{AutoCommit}=1;
+    }
+
+    my $h= DBI->connect("dbi:SQLite:$fn",'','',$opts)
        or die "$fn $DBI::errstr ?";
     return $h;
     # default timeout is 30s which is plenty
@@ -188,12 +214,13 @@ sub db_check_referential_integrity ($) {
        nooutput(<<END);
 
  # Every buy/sell must refer to an entry in commods, islands, and stalls:
- SELECT * FROM $bs NATURAL LEFT JOIN commods WHERE commodname IS NULL;
- SELECT * FROM $bs NATURAL LEFT JOIN islands WHERE islandname IS NULL;
- SELECT * FROM $bs LEFT JOIN STALLS USING (stallid) WHERE stallname IS NULL;
+ SELECT * FROM $bs LEFT JOIN commods USING (commodid) WHERE commodname IS NULL;
+ SELECT * FROM $bs LEFT JOIN islands USING (islandid) WHERE islandname IS NULL;
+ SELECT * FROM $bs LEFT JOIN stalls USING (stallid, islandid)
+                                                      WHERE stallname IS NULL;
 
  # Every buy/sell must be part of an upload:
- SELECT * FROM $bs NATURAL LEFT JOIN uploads WHERE timestamp IS NULL;
+ SELECT * FROM $bs LEFT JOIN uploads USING (islandid) WHERE timestamp IS NULL;
 
  # The islandid in stalls must be the same as the islandid in buy/sell:
  SELECT * FROM $bs JOIN stalls USING (stallid)
@@ -205,8 +232,10 @@ END
     nooutput(<<END);
 
  # Every stall and upload must refer to an island:
- SELECT * FROM stalls NATURAL LEFT JOIN islands WHERE islandname IS NULL;
- SELECT * FROM uploads NATURAL LEFT JOIN islands WHERE islandname IS NULL;
+ SELECT * FROM stalls LEFT JOIN islands USING (islandid)
+                                        WHERE islandname IS NULL;
+ SELECT * FROM uploads LEFT JOIN islands USING (islandid)
+                                         WHERE islandname IS NULL;
 
 END
     if ($full) {
@@ -229,9 +258,9 @@ END
        WHERE dist IS NULL;
 
  # Every commod must refers to a commodclass and vice versa:
- SELECT * FROM commods NATURAL LEFT JOIN commodclasses
+ SELECT * FROM commods LEFT JOIN commodclasses USING (commodclassid)
        WHERE commodclass IS NULL;
- SELECT * FROM commodclasses NATURAL LEFT JOIN commods
+ SELECT * FROM commodclasses LEFT JOIN commods USING (commodclassid)
        WHERE commodname IS NULL;
 
  # Ordvals which are not commodclass ordvals are unique:
@@ -244,17 +273,17 @@ END
  # For every class, posinclass is dense from 1 to maxposinclass,
  # apart from the commods for which it is zero.
  SELECT commodclass,commodclassid,posinclass,count(*)
-       FROM commods NATURAL JOIN commodclasses
+       FROM commods JOIN commodclasses USING (commodclassid)
        WHERE posinclass > 0
        GROUP BY commodclassid,posinclass
        HAVING count(*) > 1;
  SELECT commodclass,commodclassid,count(*)
-       FROM commods NATURAL JOIN commodclasses
+       FROM commods JOIN commodclasses USING (commodclassid)
        WHERE posinclass > 0
        GROUP BY commodclassid
        HAVING count(*) != maxposinclass;
  SELECT *
-       FROM commods NATURAL JOIN commodclasses
+       FROM commods JOIN commodclasses USING (commodclassid)
        WHERE posinclass < 0 OR posinclass > maxposinclass;
 
 END
index d6951cf6b905e99c792a280013cd94395b1ba521..2cd1148f9a3f1c60a97e22cf3b0d1f60d5a84e47 100644 (file)
@@ -55,13 +55,14 @@ sub yppedia_chart_parse ($$ $$$$ $) {
        s/^\s*//; chomp; s/\s+$//; s/\s+/ /g;
        s/\<\/?(?:b|em)\>//g;
        s/\{\{(?:chart\ style|Chart league difficulty)\|[^{}]*\}\}//gi;
+       s/^\{\{(?:testing|current)\}\}//;
        next unless m/\{\{/; # only interested in chart template stuff
 
        if (($x,$y,$arch) =
            m/^\{\{ chart\ label \|(\d+)\|(\d+)\| .*
                    (?: \<(?: big|center )\>)* \'+
                    (?: \[\[ | \{\{ )
-                   [^][\']* \| ([^][\'|]+)\ archipelago
+                   [^][]* \| ([^][|]+)\ archipelago
                    (?: \]\] | \}\} )
                    \'+ (?: \<\/(?: big|center )\>)* \}\}$/xi) {
            printf $debugfh "%2d,%-2d arch %s\n", $x,$y,$arch;
index 526d466aaf541525b79339e3c39d5cb05102ac6d..295952f4e120b01eb2f5832a0adf4deb7d834410 100644 (file)
@@ -155,7 +155,7 @@ Disclaimers, authorship and copyright
 The PCTB server is a project of Joel Lord and various others.
 
 The YARRG server was inspired by PCTB and is a project of the crew
-Special Circumstances (on the Midnight Ocean) and of the Sinister
+Special Circumstances (on the Cerulean Ocean) and of the Sinister
 Greenend Organisation.
 
 PCTB and YARRG are both completely unofficial and Three Rings, the
@@ -212,4 +212,4 @@ from my dictionary server when run in the most ordinary way.
 
  - Ian Jackson
    ijackson@chiark.greenend.org.uk
-   Aristarchus on the Midnight ocean
+   Aristarchus on the Cerulean ocean
index 4650aa00d3a4506ed4cd5b4c6787cbc9b759f7cc..f5926b091d4654a62464e3008d4318417ee96a6a 100644 (file)
@@ -180,6 +180,6 @@ server so that they can be checked by me and shared with other users.
 See README.privacy.
 
 If you need help please ask me (ijackson@chiark.greenend.org.uk, or
-Aristarchus on Midnight in game if I'm on line, or ask any pirate of
+Aristarchus on Cerulean in game if I'm on line, or ask any pirate of
 the crew Special Circumstances if they happen to know where I am
 and/or can get in touch).
index 4c6444f25cfd8d0a58cb11c0fb3118e040bf1762..bef470ae6ac844e71c5fd700287f954568edc4b4 100644 (file)
@@ -101,7 +101,7 @@ Format for emails from the yarrg exploder
 Uploads checked and accepted by the yarrg server are distributed by
 email to operator(s) of databases doing interesting commodity stuff.
 Currently there's only one such database, the yarrg pirate trader
-website.  (Work in progress.)
+website.
 
 But if you want to get copies, please just email us saying what you
 plan to do.
@@ -152,5 +152,5 @@ any such additional information.
 Contacting the yarrg server operator
 ------------------------------------
 
-Talk to Aristarchus on Midnight, or ask any officer of the crew
+Talk to Aristarchus on Cerulean, or ask any officer of the crew
 Special Circumstances, or email ijackson@chiark.greenend.org.uk.
index 40df2b48aebf8bab18615d28faec3527448db622..0ec3642dab05549843c0e28816dda09d70819df7 100644 (file)
@@ -13,6 +13,7 @@ USAGE:
     Then, in your YPP client:
       * go to the docks of any island
       * click on the button "Where are my vessels"
+      * click anywhere in the main part of the screen
       * press Ctrl-A Ctrl-C
 
     In where-vessels, click "Acquire"
index a75864a57e2ff4f0d3d48514697f78d900a14d24..ff0880e9c876d6f14118d38c00149130de511f18 100755 (executable)
@@ -34,6 +34,8 @@ BEGIN {
     my $selfdir= $0;
     $selfdir =~ s,/+[^/]*$,,;
     chdir("$selfdir") or die "$selfdir $!";
+
+    unshift @INC, qw(.);
 }
 
 use Commods;
@@ -197,6 +199,7 @@ sub main () {
 #    print "\n";
     db_chkcommit(0);
 
+    # Warning!  Below runes are bogus.  Do not use NATURAL JOIN!
     # select * from ((buy natural join commods) natural join stalls) natural join islands;
     # select * from ((sell natural join commods) natural join stalls) natural join islands;
 
index 8ede37166a7c1215731769ccab8993437259634d..fe05fb8722c207acb14c44f63d0d15b31a20bf17 100755 (executable)
@@ -24,6 +24,7 @@
 # are used without permission.  This program is not endorsed or
 # sponsored by Three Rings.
 
+BEGIN { unshift @INC, qw(.) }
 
 use strict (qw(vars));
 use HTTP::Request;
@@ -550,19 +551,17 @@ sub main__uploadpctb () {
 
     my $islandid;
     while ($resptxt =~
- m/^islands\[\d+\]\[\d+\]\=new\s+option\(\"(.*)\"\,(\d+)\)\s*$/mig
+ m/^islands\[(\d+)\]\[\d+\]\=new\s+option\(\"(.*)\"\,(\d+)\)\s*$/mig
           ) {
-       next unless $1 eq $island;
-       $islandid= $2;
+       next unless $1 eq $oceanids[0];
+       next unless $2 eq $island;
+       $islandid= $3;
     }
     defined $islandid or die;
 
     die "@filenames ?" if grep { $_ ne $filename } @filenames;
     die "@forcerls ?" if grep { $_ ne $forcerl } @forcerls;
 
-    my $setisland= {
-    };
-
     print STDERR "Setting ocean and island...\n";
 
     my $siurl= ($url . "?action=setisland".
index 2059ad792416710584c279eb1d2de63032220339..0258a51e04130c96ff24a3151095132dbfc3faee 100755 (executable)
 # are used without permission.  This program is not endorsed or
 # sponsored by Three Rings.
 
+BEGIN { unshift @INC, qw(.) }
+
 use strict (qw(vars));
 use POSIX;
 use MIME::Entity;
 
 use Commods;
 
+no warnings qw(exec);
+
 $CGI::POST_MAX= 3*1024*1024;
 
 use CGI qw/:standard -private_tempfiles/;
index 893008735889d94d0f345c138e8401c154bb1fc5..b6a263e3ac5540680157721e9566a33e9790b95d 100644 (file)
@@ -49,6 +49,7 @@
 #include <pam.h>
 #include <time.h>
 #include <limits.h>
+#include <ctype.h>
 
 #include <sys/time.h>
 
index 041e008f4220e0a775c8ce4aeab18f5c7d4535ff..655d2ecdb05871502eed20e102476b0e4e80b274 100755 (executable)
@@ -24,6 +24,8 @@
 # are used without permission.  This program is not endorsed or
 # sponsored by Three Rings.
 
+BEGIN { unshift @INC, qw(.) }
+
 use strict (qw(vars));
 use JSON;
 #use Data::Dumper;
index eb1a30b43a581047e330b4f226670083366e3fb0..2b63f2b9b110f0b334c2fafade4eeba24c6380e3 100755 (executable)
@@ -29,6 +29,8 @@
 # are used without permission.  This program is not endorsed or
 # sponsored by Three Rings.
 
+BEGIN { unshift @INC, qw(.) }
+
 use strict (qw(vars));
 
 use DBI;
index f7af70dbf200104883e6b9ce392bd45ca5dd9773..d3f4a4e287df0591352159ef15ba363760c849e2 100755 (executable)
@@ -3,6 +3,8 @@
 # This specific file is hereby placed in the public domain, or nearest
 # equivalent in law, by me, Ian Jackson.  5th July 2009.
 
+BEGIN { unshift @INC, qw(.) }
+
 use IO::File;
 use strict (qw(vars));
 
@@ -70,11 +72,22 @@ unshift @stalls, undef;
 
 $|=1;
 
+sub chk_ascending ($$$) {
+    my ($this,$lastref,$desc) = @_;
+    printf "# WARNING - $desc $this < $$lastref\n"
+       if defined($$lastref) and $this < $$lastref;
+    $$lastref= $this;
+}
+
 foreach my $bs qw(Buy Sell) {
     my $alloffers_want= getint("Buy ncommods");
     my $alloffers_done=0;
+    my $commodix_last;
+    my $price_last;
     while ($alloffers_done < $alloffers_want)  {
        my $commodix= getint("Buy $alloffers_done/$alloffers_want commodix");
+       $price_last=undef unless
+           defined($commodix_last) && $commodix == $commodix_last;
        my $offers= getint("Buy $commodix offers");
        my $offernum;
        for ($offernum=0; $offernum<$offers; $offernum++) {
@@ -85,10 +98,14 @@ foreach my $bs qw(Buy Sell) {
                   $bs,
                   inmap('commod',@pctb_commodmap,$commodix),
                   inmap('stall',@stalls,$stallix)) or die $!;
-           if ($bs eq 'Sell') { print "\t\t" or die $!; }
+           my $chk_asc_price_neg;
+           my $pricesort;
+           if ($bs eq 'Sell') { $pricesort=1; print "\t\t" or die $!; }
            printf("\t%d\t%d", $price, $qty) or die $!;
-           if ($bs eq 'Buy') { print "\t\t" or die $!; }
+           if ($bs eq 'Buy') { $pricesort=-1; print "\t\t" or die $!; }
            print "\n" or die $!;
+           chk_ascending($commodix,\$commodix_last,'commodix');
+           chk_ascending($pricesort*$price,\$price_last,'price');
            $alloffers_done++;
            die if $alloffers_done > $alloffers_want;
        }
@@ -188,3 +205,9 @@ $b and die;
 #  0060                                        01 00 1e 00    OI 30
 #  0070   02 00                                               qty 2
 #
+# buy offers need to be sorted first by commodity index, then by
+# prices (prices ascending)
+# sell offers need to be sorted first by commodity index, then by
+# prices (prices descending)
+# [ however, it seems that this is wrong and the price sort order is
+#   supposed to be descending for buy and ascending for sell ]
index 3baef669ddf6b4eefcc7a7942cb709118a9fc9dc..feef04b641e23a2ccbfa04a309f8a9dafd42ccce 100755 (executable)
 # YPPSC_YARRG_DICT_UPDATE=./ YPPSC_YARRG_DICT_SUBMIT=./ ./yarrg --ocean midnight --pirate aristarchus --find-island --same --raw-tsv >raw.tsv  
 # ./dictionary-manager --debug --approve-updates '' . .
 
+BEGIN { unshift @INC, qw(.) }
+
 use strict (qw(vars));
 use POSIX;
 
+no warnings qw(exec);
+
 $CGI::POST_MAX= 1024*1024;
 $CGI::DISABLE_UPLOADS= 1;
 
diff --git a/yarrg/icons/fc.xbm b/yarrg/icons/fc.xbm
new file mode 100644 (file)
index 0000000..2f92031
--- /dev/null
@@ -0,0 +1,5 @@
+#define fc_width 12
+#define fc_height 10
+static unsigned char fc_bits[] = {
+   0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x60, 0x00, 0x98, 0x01, 0x60, 0x00,
+   0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
index c9542f9b87a69a9ec616e7c5b52094f763780232..17a044fdf846dd2e8aef02ce5af0e3222b76d2ab 100644 (file)
@@ -2,6 +2,7 @@
        smsloop         am      sl      Sloop
        lgsloop         bm      ct      Cutter
        dhow            cm      dh      Dhow
+       fanchuan        cw      fc      Fanchuan
        longship        dm      ls      Longship
        baghlah         em      bg      Baghlah
        junk            eo      jk      Junk
@@ -19,4 +20,8 @@
        verdant         V       {Verdant class}
        inferno         I       {Inferno class}
        fancy           M       {Midas class}
+       phantom         G       {Phantom class}
+       imperial        P       {Imperial class}
+       fortune         O       {Fortune class}
+       lagoon          L       {Lagoon class}
 }
index 2336b43c453a717363049a18e4132cecd14b5293..e234189c53986745c27f96375d781eb8cbea282a 100755 (executable)
@@ -22,6 +22,8 @@
 # are used without permission.  This program is not endorsed or
 # sponsored by Three Rings.
 
+BEGIN { unshift @INC, qw(.) }
+
 use strict (qw(vars));
 
 use CommodsDatabase;
index d591ac0c1afee09617268e6526caf7c6c44ead56..e160fcbad6631d004b6f9845ca30121b095738ee 100644 (file)
@@ -8,6 +8,8 @@ vessels
  |----------------+--------+-------+-------|
  |Dhow            |medium  |20,250 |13,500 |
  |----------------+--------+-------+-------|
+ |Fanchuan        |large   |20,250 |13,500 |
+ |----------------+--------+-------+-------|
  |Longship        |small   |20,250 |13,500 |
  |----------------+--------+-------+-------|
  |Baghlah         |medium  |27,000 |18,000 |
@@ -43,6 +45,19 @@ commodclasses
  *enamel
  *forageables
 
+# Commodities are defined in `commods` sections
+# Each line is
+#  <name> <facts>
+# and the <facts> can be
+#  <mass>kg
+#  <volume>l
+#  *<class> (see commodclasses, above)
+#  @<sort-number>    sort order value, for where in the commods list it goes
+#  @<sort-number>+   sort order value; 10x line number is added
+# The <name> can contain `%<reference>` which is then defined in
+# a `%<reference>` section.
+# Volume defaults to 1l.
+
 commods
  kraken's blood                1kg             *dye                    @105
  %d dye                        1kg             *dye                    @0
@@ -141,14 +156,17 @@ commods
  lapis lazuli                                                  @200000+
  quartz                                                                @200000+
  tigereye                                                      @200000+
+ topaz                                                         @200000+
 
 commods
  swill                 1kg             *ship_supplies          @0+
  grog                  1kg             *ship_supplies          @0+
  fine rum              1kg             *ship_supplies          @0+
+ rum spice             800g            *ship_supplies          @0+
  small cannon balls    7100g           *ship_supplies          @0+
  medium cannon balls   14200g 2l       *ship_supplies          @0+
  large cannon balls    21300g 3l       *ship_supplies          @0+
+ lifeboats             25kg 100l       *ship_supplies          @0+
 
  madder                        400g            *herbs                  @0+
  old man's beard       800g            *herbs                  @0+
@@ -165,6 +183,7 @@ commods
  lily of the valley    300g            *herbs                  @0+
  nettle                        300g            *herbs                  @0+
  butterfly weed                100g            *herbs                  @0+
+ allspice              800g            *herbs                  @0+
 
  bananas               125kg 100l      *forageables            @300000+
  carambolas            125kg 100l      *forageables            @300000+
@@ -197,49 +216,42 @@ commods
  hemp oil              1kg             *basic_commodities      @160
  varnish               1kg             *basic_commodities      @180
  lacquer               1kg             *basic_commodities      @190
+ kraken's ink          100g 1l         *basic_commodities      @200
 
 
 client ypp-sc-tools yarrg
  lastpage
 
 client jpctb greenend
-
+ bug-094
 
 #---------- OCEANS ----------
 # subscriber oceans
 
-ocean Midnight
-
-ocean Cobalt
+ocean Cerulean
  Garnet
   Jubilee Island
 
-ocean Ice
- Vilya
-  Winking Wall Island
-
 # doubloon oceans
 
-ocean Hunter
- Eagle
-  Ix Chel
-  Manu Island
-
-ocean Malachite
- Draco
-  Cetus Island
-  Threewood Island
-
-ocean Sage
+ocean Emerald
+ Crab
+  The Beaufort Islands
  Osprey
   Scurvy Reef
   Gauntlet Island
+ Pleiades
+  Morgana Island
 
-ocean Viridian
-
-# family oceans
-
-ocean Crimson
+ocean Meridian
+ Draco
+  Cetus Island
+  Threewood Island
+  Wyvern Island
+ Basilisk
+  Zechstein Island
+ Komodo
+  Buyan's Vortice
 
 # International oceans (doubloon oceans)
 
@@ -255,3 +267,15 @@ ocean Crimson
 # Canis
 #  Atchafalaya-Insel
 
+# Test ocean
+
+ocean Ice
+ Vilya
+  Winking Wall Island
+
+ocean Obsidian
+ Ye Bloody Bounding Main
+  Loggerhead Island
+  Melanaster Island
+  Picklepine Ridge
+  Woodtick Island
index adad34c81f42fbd29dbc2d0f9b24a7a392bda272..797eb9da52706a1cc44bbb5a3915a955387f20df 100755 (executable)
@@ -52,6 +52,8 @@
 # are used without permission.  This program is not endorsed or
 # sponsored by Three Rings.
 
+BEGIN { unshift @INC, qw(.) }
+
 use strict (qw(vars));
 use DBI;
 use Commods;
index 8ff09966b953143dfd474a4a0d43c66913f47c38..64e31fc9a66c93dc0ec7d4d362e96064fc576e4b 100644 (file)
@@ -35,6 +35,8 @@
 
 </%doc><%perl>
 
+no warnings qw(exec);
+
 use CommodsWeb;
 
 my $printable= printable($m);
index 4ff6d04f3b9801172e23c39b36d062a7337cc465..fa40d1bc066f0c06b207ccfdfcd98b1640cb41aa 100755 (executable)
@@ -50,9 +50,6 @@ Note that there is <strong>NO WARRANTY</strong> !
 
 <h2>Resources for developers</h2>
 
-For information about the YARRG JPCTB uploader, see the
-<a href="upload">page about the uploader</a>.
-
 <h3>Source code</h3>
 
 <kbd>git clone git://git.yarrg.chiark.net/ypp-sc-tools.main.git</kbd><br>
@@ -73,6 +70,16 @@ the source for their version, the website code itself lets you download
 an up-to-date of its actually-running source code.  So this link
 gives you the source code for the pages you are now looking at.
 
+<p>All of the above trees are available via the
+<a href="http://www.chiark.greenend.org.uk/ucgi/~yarrgweb/git">gitweb
+source code and history browser</a>.
+
+<h4>Uploader</h4>
+
+The uploader is also Free Software, but under a slightly different
+licence.  The <a href="upload">uploader page</a> has
+<a href="upload#source">complete information on how to get its source code</a>.
+
 <h3>YARRG website code instances</h3>
 
 <h4>Lookup website</h4>
@@ -110,7 +117,8 @@ download their source directly:
 <h3>Data</h3>
 
 <kbd>rsync rsync.yarrg.chiark.net::yarrg/</kbd><br>
-accesses files published for the benefit of the yarrg upload client,
+accesses files published for the benefit of the old yarrg upload client
+and other members of the ypp-sc-tools family.
 
 <p>
 
@@ -136,10 +144,10 @@ has the specification of the mechanism and format for uploading to YARRG.
 
 If you would like to run a (perhaps modified) copy of the YARRG
 website it would be very easy for us to make our system send you
-copies of updates submitted by users of the official YARRG client, in
-the format expected by the YARRG code.  Please just ask us - at our
-end it's just a matter of us adding your database instance's special
-email address to our alias file.
+copies of updates submitted by users of the YARRG clients including
+JARRG, in the format expected by the YARRG code.  Please just
+ask us - at our end it's just a matter of us adding your database
+instance's special email address to our alias file.
 
 <p>
 
@@ -152,7 +160,7 @@ your website to download the up to date code you are running.
 
 Email Ian Jackson ijackson (at) chiark.greenend.org.uk.  Or talk to
 any Fleet Officer or above of the crew Special Circumstances on the
-Midnight Ocean.
+Cerulean Ocean.
 
 <p>
 </div>
index 23ab7334ef6488ce185e4092d237629cf8c79f40..82b2adde2f2fc7e4133dbf2b26f97b53745f7d26 100755 (executable)
@@ -246,7 +246,7 @@ fact be possible.  In practice this is unlikely to be a problem!
 In the Voyage Trading Plan, YARRG indicates after the commodity name
 where in the YPP commodity UI each commodity can be found.  First
 comes the initial letter of the category:
-%     my $dbh= dbw_connect('Midnight');
+%     my $dbh= dbw_connect('Cerulean');
 %     my $getclasses= $dbh->prepare(
 %        "SELECT commodclass FROM commodclasses ORDER BY commodclass");
 %     $getclasses->execute();
index 75fcd156268ed9d9001d90c8a37b72b50c2c4c06..33fe9ad24663a685ab8bc51bd5f0609b636bf9d3 100644 (file)
@@ -39,7 +39,7 @@ $isdevel => 0
 <hr>
 <address>
 YARRG is Yet Another Revenue Research Gatherer, a project of the
-crew Special Circumstances on the Midnight Ocean
+crew Special Circumstances on the Cerulean Ocean
 and of the Sinister Greenend Organisation.
 <p>
 
index a28bf0d8dcd514bc5ddbdef404cbc657aae38d0a..b7a3b5fa39ecc528d6212dc2e0c81567a806fb9a 100755 (executable)
@@ -347,7 +347,7 @@ YARRG data (perhaps you don't like our colour choices)
 
 Email Ian Jackson ijackson (at) chiark.greenend.org.uk.  Or talk to
 any Fleet Officer or above of the crew Special Circumstances on the
-Midnight Ocean.
+Cerulean Ocean.
 <p>
 
 </div>
index 67b4cfbc531d8e6f4ad1b48b80655d7d13583041..7f5b24e66e893acd7f9f1a122c526bdb88ba5cae 100755 (executable)
@@ -17,6 +17,6 @@ $sth->execute();
 
 <%init>
 use CommodsWeb;
-db_setocean('Midnight');
+db_setocean('Cerulean');
 db_connect();
 </%init>
index 313a04ae6928165711ebe8034ad8d1a3c376b74c..16e45b6e9ffdccde4ba870df5d49a076b0e793a6 100644 (file)
@@ -30,5 +30,5 @@ $sth->execute();
 
 <%init>
 use CommodsWeb;
-my $dbh= dbw_connect('Midnight');
+my $dbh= dbw_connect('Cerulean');
 </%init>
index 539abce6a550d8387c0171b845ef20b50b0d651c..bb3fc31730293b44d4124ed751379ddf4850f791 100755 (executable)
@@ -33,7 +33,7 @@
  text string parsers/checkers like check_routestring.
 
 # typical url for this script:
-#  http://www.chiark.greenend.org.uk/ucgi/~clareb/mason/pirates/qtextstring?what=routestring?format=json&ocean=Midnight&string=d
+#  http://www.chiark.greenend.org.uk/ucgi/~clareb/mason/pirates/qtextstring?what=routestring?format=json&ocean=Cerulean&string=d
 
 </%doc>
 
index 4f8ba814b7f1e5ddf2b0ce0fc22549ad560b728d..a115bbdd562dd53286be49ff175f5519f366cd28 100644 (file)
@@ -47,7 +47,7 @@ $dbh
 my $now= time;
 
 my $sth= $dbh->prepare("SELECT archipelago, islandid, islandname, timestamp
-                               FROM uploads NATURAL JOIN islands
+                               FROM uploads JOIN islands USING (islandid)
                                ORDER BY archipelago, islandname");
 $sth->execute();
 
index 8d55f2686130568ba92d2c578dd787d2362e368e..c801a631e65855d4e762aa247bb702bf883ecfc1 100644 (file)
@@ -99,7 +99,7 @@ foreach my $bs (split /_/, $ARGS{BuySell}) {
 
        my $offers= $dbh->prepare(
        "SELECT stallname, price, qty
-               FROM $bs NATURAL JOIN stalls
+               FROM $bs JOIN stalls USING (stallid, islandid)
                WHERE commodid = ? AND islandid = ?
                ORDER BY price $ascdesc"
                );
index 6cf098ed82c63c8cfce1fe93fe6e889460415675..126c4e804d860a807d5abfb57b5dd36dd66a728c 100644 (file)
@@ -51,6 +51,8 @@ $allargs
 </%args>
 
 <%perl>
+no warnings qw(exec);
+
 use BSD::Resource;
 
 my $emsg;
@@ -59,7 +61,7 @@ my @islandids;
 
 my $destspec;
 
-my @maxmaxdist= qw(35 35 60);
+my @maxmaxdist= qw(100 100 100);
 my $maxcpu=90;
 my $concur_lim=5;
 
@@ -181,7 +183,7 @@ foreach my $k (qw(MaxMass MaxVolume MaxCapital)) {
 }
 push @rsargs, defined $routeparams->{LossPerLeaguePct}
        ? $routeparams->{LossPerLeaguePct}*0.01 : 1e-9;
-push @rsargs, $routeparams->{MinProfit};
+push @rsargs, $routeparams->{MinProfit} // 0;
 push @rsargs, 'search',$maxdist, $maxcountea,$maxcountea;
 push @rsargs, $destspec;
 push @rsargs, @islandids;
@@ -228,7 +230,7 @@ while (<$fh>) {
        }
        die unless m/^ \@ *\d+ ([ap])\# *\d+ \|.*\| *(\d+)lg *\| *\d+ +(\d+) +(\d+) *\| ([0-9 ]+)$/;
        my ($ap,$isles) = (uc $1,$5);
-       next if $results{$ap} && %{$results{$ap}} >= $maxcountea;
+       next if $results{$ap} && keys %{$results{$ap}} >= $maxcountea;
        my $item= { A => $3, P => $4, Leagues => $2 };
        my (@i, @fi, @a);
        foreach (split / /, $isles) {
diff --git a/yarrg/web/robots.txt b/yarrg/web/robots.txt
new file mode 100755 (executable)
index 0000000..38a5ff7
--- /dev/null
@@ -0,0 +1,11 @@
+<%flags>
+inherit => undef
+</%flags>
+<%perl>
+$r->content_type('text/plain');
+</%perl>
+User-Agent: *
+Disallow: /lookup
+Disallow: /test/code/lookup
+Disallow: /test/data/lookup
+Disallow: /test/both/lookup
index 03ef3e1e8dc4ab99bfc8b29cfc8b69750268845c..c62ca60b7f89a7956008cacc7c8998ae944a72c7 100644 (file)
@@ -1010,12 +1010,12 @@ END
 <%perl>
        my $sth_i= $dbh->prepare(<<END);
                SELECT archipelago, islandid, islandname, timestamp
-                       FROM uploads NATURAL JOIN islands
+                       FROM uploads JOIN islands USING (islandid)
                        WHERE islandid = ?
 END
        my $sth_a= $dbh->prepare(<<END);
                SELECT archipelago, islandid, islandname, timestamp
-                       FROM uploads NATURAL JOIN islands
+                       FROM uploads JOIN islands USING (islandid)
                        WHERE archipelago = ?
                        ORDER BY islandname
 END
index e39ec2ecd783c3b2650c9faf8ac0567a2558b682..c5236b7a0910805b915140dc71a94fa8ed16e04f 100755 (executable)
@@ -37,8 +37,8 @@
 <h1>"yarrg" screenscraper for YARRG and PCTB</h1>
 
 There is an obsolete screenscraper for Linux, known as "yarrg".  It is
-much slower and less reliable than JPCTB.  We recommend you use
-<a href="upload">YARRG JPCTB</a> instead.
+much slower and less reliable than JARRG.  We recommend you use
+<a href="upload">JARRG</a> instead.
 
 <h2>Instructions for using obsolete "yarrg"</h2>
 
index f6e0fce843ec0f4d67154fac767429caf7d475a4..a0c40fe2360e3c23b49472c5489060ef4ef9b2f4 100755 (executable)
 
 </%doc>
 <& docshead &>
-<%perl>
-my $url_base_base= 'http://yarrg.chiark.net/download/jpctb';
-my $download_version= 'test';
-my $url_base= "$url_base_base/$download_version";
-</%perl>
+<%shared>
+my $url_base_base= 'http://yarrg.chiark.net/download/jarrg';
+</%shared>
+<%def downloadurl><%args>
+$leaf
+$test => 0
+#        ^ change this to change the default version
+</%args><%perl>
+my $download_version= $test ? '/test' : '';
+my $url_base= "$url_base_base$download_version";
+my $url= $url_base.'/'.$leaf;
+</%perl><a href="<% $url %>"><%
+    $m->has_content ? $m->content : $url
+%></a></%def>
 <h1>Uploading to YARRG</h1>
 <p>
 
 The YARRG system has two main parts: this website which maintains a
 searchable database of commodity prices, and an upload client (known
-as YARRG JPCTB), which screenscrapes the commodity data from the
+as JARRG), which screenscrapes the commodity data from the
 Puzzle Pirates game client and uploads it to the database.
 
 <p>
@@ -56,45 +65,52 @@ you want a plan based on recent data.
 
 <p>
 
-The YARRG JPCTB upload client uploads both to YARRG and
+The JARRG upload client uploads both to YARRG and
 <a href="http://pctb.crabdance.com/">PCTB</a>.  It is intended to
 replace both the <a href="scraper">"yarrg"</a> and "PPAOCR" screenscrapers.
 
 <h2>Instructions</h2>
 
-First you must install YARRG JPCTB, following the instructions for
+First you must install JARRG, following the instructions for
 your operating system, below.  This will not modify your Puzzle
 Pirates installation.  Instead, it provides you with a new way to run
-Puzzle Pirates which integrates it with JPCTB to provides a YARRG/PCTB
+Puzzle Pirates which integrates it with JARRG to provides a YARRG/PCTB
 Upload facility.
 
 <p>
 
-To upload data, run the Puzzle Pirates client with JPCTB (as detailed
-below).  You should see both the Puzzle Pirates window and the JPCTB
+To upload data, run the Puzzle Pirates client with JARRG (as detailed
+below).  You should see both the Puzzle Pirates window and the JARRG
 window appear.  Log in normally, and visit a commodity trading screen
 (at a market, shoppe or vessel hold).  Select "All Commodities" in the
-YPP client.  Then press "Upload" in the JPCTB window.
+YPP client.  Then press "Upload Market Data" in the JARRG window.
 
 <p>
 
-The JPCTB integration is done via fully supported Java Accessibility
+It is OK to switch away from the commodity list in Puzzle Pirates as
+soon as the Jarrg progress bar appears.  The uploader has taken a copy
+of the data, so you can do something else while the upload takes
+place.
+
+<p>
+
+The JARRG integration is done via fully supported Java Accessibility
 interfaces, and should not disturb the normal running of Puzzle
 Pirates.  However, please make sure that if your YPP client misbehaves
-at all, you try starting it in the vanilla way (without JPCTB
-integration) before asking for help.  In particular, if the JPCTB
+at all, you try starting it in the vanilla way (without JARRG
+integration) before asking for help.  In particular, if the JARRG
 startup does not work, but the ordinary Puzzle Pirates startup does,
 report the problem to us, not to Three Rings.
 
 <h3>Installing and running on Linux</h3>
 
 Download
-<a href="<% $url_base %>/jpctb.tar.gz"><% $url_base %>/jpctb.tar.gz</a>
-and unpack it.  You run jpctb from the command line.  Change
-(<code>cd</code>) to the top level <code>jpctb</code> directory
+<& downloadurl, leaf => 'jarrg-linux.tar.gz' &>
+and unpack it.  You run jarrg from the command line.  Change
+(<code>cd</code>) to the top level <code>jarrg</code> directory
 which was created when you unpacked the tarball, and then run
 <pre>
-./jpctb /path/to/yohoho/yohoho
+./jarrg /path/to/yohoho/yohoho
 </pre>
 where <code>/path/to/yohoho/yohoho</code> is the location of the
 ordinary Puzzle Pirates startup script, which is normally
@@ -103,11 +119,11 @@ ordinary Puzzle Pirates startup script, which is normally
 <h3>Installing on Windows</h3>
 
 Download
-<a href="<% $url_base %>/jpctb-setup.exe"><% $url_base %>/jpctb-setup.exe</a>
+<& downloadurl, leaf => 'jarrg-setup.exe' &>
 and double-click on it.  It will either:
 <ol>
 <li>Just work, in which case you'll have a new icon on your desktop which
-   runs Puzzle Pirates with JPCTB integrated.
+   runs Puzzle Pirates with JARRG integrated.
 <li>Fail, and tell you what to do next.  Usually this means installing a
    Java Runtime Environment (or JVM) and then uninstalling and re-
    installing Puzzle Pirates.  (Don't worry about reinstalling; you
@@ -117,19 +133,24 @@ and double-click on it.  It will either:
 <h3>Installing on Macs</h3>
 
 We believe that it should be straightforward for a MacOS expert to get
-YARRG JPCTB working properly on MacOS but we have not been able to
+JARRG working properly on MacOS but we have not been able to
 test this ourselves.  The Linux installation method is probably the
 best starting point.
 
 <h2>How does it work?  Is it a violation of the Terms of Service?</h2>
 
-Essentially, JPCTB is a specialised "screen reader" which instead of
+JARRG (and the old OCR clients) comply with Three Rings' official
+<a href="http://yppedia.puzzlepirates.com/Official:Third_Party_Software">Third Party Software Policy</a>.
+
+<p>
+
+Essentially, JARRG is a specialised "screen reader" which instead of
 reading information out loud, uploads it to the YARRG and PCTB
 databases.
 
 <p>
 
-YARRG JPCTB uses the Java Accessibility API, which is a part of the
+JARRG uses the Java Accessibility API, which is a part of the
 Java platform.  It's a facility in Java, available for all Java
 programs, intended to help make applications available to users with
 disabilities: for example, it permits hooking in screen readers.  The
@@ -139,7 +160,7 @@ help getting information out of Duty Reports).
 
 <p>
 
-Installing YARRG JPCTB does not modify any game files, and does not
+Installing JARRG does not modify any game files, and does not
 hook into the game itself.  It makes no permanent or global changes to
 your overall computer, operating system or Java configuration.  And it
 does not access (indeed, we don't think it could access) any of the
@@ -147,46 +168,48 @@ core game functionality which would make cheating possible.
 
 <p>
 
-JPCTB runs the unmodified Puzzle Pirates game, but with a separate
+JARRG runs the unmodified Puzzle Pirates game, but with a separate
 copy of your system's JVM (Java Virtual Machine); that copy of the JVM
-is configured to use the JPCTB accessibility plugin.  We do this
+is configured to use the JARRG accessibility plugin.  We do this
 (rather than configuring the computer's main JVM to use the
 accessibility plugin) to avoid interfering with other uses of Java on
 your computer - in particular, so that it is always possible to launch
-Puzzle Pirates <em>without</em> JPCTB (for example, in case the plugin
+Puzzle Pirates <em>without</em> JARRG (for example, in case the plugin
 should cause any kind of problem).
 
-<h2>Authorship, source code and other versions</h2>
+<h2><a name="source">Authorship, source code and other versions</a></h2>
 
-Thanks to Burninator for writing the core of the JPCTB client.  Ian
-Jackson and Owen Dunn adapted it to improve the installation setup (in
-particular, to avoid modifying any of the YPP client's startup files).
-Owen Dunn added support for uploading to YARRG, updated the build
-system, and wrote a Windows installer.
+Thanks to Burninator for writing the core of the JARRG client.  Ian
+Jackson and Owen Dunn adapted it to fix bugs and to improve the
+installation setup (in particular, to avoid modifying any of the YPP
+client's startup files).  Owen Dunn added support for uploading to
+YARRG, updated the build system, and wrote a Windows installer.
 
 <p>
 
-YARRG JPCTB is
+JARRG is
 Copyright 2006-2009 Burninator,
 Copyright 2009-2010 Owen Dunn and
-Copyright 2009-2010 Ian Jackson.
+Copyright 2009-2011 Ian Jackson.
 It is Free Software with <strong>NO WARRANTY</strong>, released under
 the MIT-style two-clause licence.
 
 <p>
 
-The download directory can contains other versions (eg unreleased test
-versions), which you can see here:
- <a href="<% $url_base_base %>/"><% $url_base_base %>/</a> .
+The code for the downloadable binaries is in
+in <& downloadurl, leaf=>'jarrg-source.tar.gz' &>.
+The download directory also sometimes contains other versions
+(eg unreleased test versions), which you can see here:
+ <& downloadurl, leaf => '', test => 0 &>
 
 <p>
 
-The source code for the downloadable released binaries is in the
-<a href="<% $url_base %>/jpctb.tar.gz">Linux
-tarball</a>.  We maintain YARRG JPCTB in git, and you can get
-source code for recent and perhaps unreleased versions from
-<code>git://git.yarrg.chiark.net/jpctb.git</code> and/or
-<code>git://git.chiark.greenend.org.uk/~ijackson/jpctb.git</code> .
+We maintain JARRG in git, and you can get
+source code for recent and perhaps unreleased versions from one of:
+<pre>
+git://git.yarrg.chiark.net/jarrg-ian.git</code> <a href="http://www.chiark.greenend.org.uk/ucgi/~yarrgweb/git?p=jarrg-ian.git;a=summary">(gitweb)</a>
+git://git.yarrg.chiark.net/jarrg-owen.git</code>&nbsp;<a href="http://www.chiark.greenend.org.uk/ucgi/~yarrgweb/git?p=jarrg-owen.git;a=summary">(gitweb)</a>
+</pre>
 
 
 </div>
index bebdf79f370f2869af534b220046dbb260217c64..045f5ad76bb06f591777eeb753c69dc4244c88d2 100755 (executable)
@@ -147,7 +147,13 @@ proc argdefaults {} {
        set cmd {./yarrg --find-window-only --quiet}
        if {[info exists ocean]} { lappend cmd --ocean $ocean }
        if {[info exists pirate]} { lappend cmd --pirate $pirate }
-       manyset [split [eval exec $cmd] " "] ocean pirate
+       if {[catch {
+           manyset [split [eval exec $cmd] " "] ocean pirate
+       } emsg]} {
+           puts stderr "yarrg: [string trim $emsg]"
+           puts stderr "Alternatively pass, --ocean and perhaps --pirate options to where-vessels"
+           exit 1
+       }
        if {![llength $ocean] || ![llength $pirate]} {
            error "$ocean $pirate ?"
        }
@@ -624,7 +630,7 @@ proc smash-code {code} {
 proc smash-prepare {} {
     global vc_codes smash_sizemap smash_size smash_sizeinexact
     set mapto {}
-    catch { unset smash_sizeplus }
+    catch { unset smash_sizeinexact }
     foreach size $vc_codes {
        if {!$smash_size($size)} {
            set mapto $size
@@ -1149,6 +1155,18 @@ proc draw {} {
 }
 
 
+#---------- info toplevel ----------
+
+proc info-toplevel-create {info title} {
+    toplevel $info
+    wm withdraw $info
+    wm title $info "where-vessels - $title"
+    wm protocol $info WM_DELETE_WINDOW [list wm withdraw $info]
+
+    button $info.close -text Close -command [list wm withdraw $info]
+    pack $info.close -side bottom
+}
+
 #---------- parser error reporting ----------
 
 proc parser-control-create {w base invokebuttontext etl_title} {
@@ -1165,17 +1183,11 @@ proc parser-control-create {w base invokebuttontext etl_title} {
     pack $w.resframe -side top -expand y -fill both
 
     set eb .err_$base
-    toplevel $eb
-    wm withdraw $eb
-    wm title $eb "where-vessels - $etl_title"
-    wm protocol $eb WM_DELETE_WINDOW [list wm withdraw $eb]
+    info-toplevel-create $eb $etl_title
 
     label $eb.title -text $etl_title
     pack $eb.title -side top
 
-    button $eb.close -text Close -command [list wm withdraw $eb]
-    pack $eb.close -side bottom
-
     frame $eb.emsg -bd 2 -relief groove
     label $eb.emsg.lab -anchor nw -text "Error:"
     text $eb.emsg.text -height 1
@@ -1316,6 +1328,29 @@ proc islandnames-handler {offset maxchars} {
                $offset [expr {$offset+$maxchars-1}]]
 }
 
+#---------- print to postscript ----------
+
+proc print-to-postscript {} {
+    global canvas ocean
+    set postscript_fontmap(fixed) {Courier 12}
+    manyset [$canvas bbox all] xmin ymin xmax ymax
+    set file where-vessels.$ocean.ps
+    $canvas postscript -file $file -rotate 1 \
+       -width [expr {$xmax-$xmin}] \
+       -height [expr {$ymax-$ymin}] \
+       -fontmap postscript_fontmap
+    .printed.info configure -text "Printed to $file
+
+Usually, the next thing would be something like"
+    set text "epsffit 0 0 595.276 841.89 <$file >t.ps"
+    .printed.rune configure -state normal
+    .printed.rune delete 1.0 end
+    .printed.rune insert end $text
+    .printed.rune configure -width [string length $text] -state disabled
+    update idletasks
+    wm deiconify .printed
+}
+
 #---------- main user interface ----------
 
 proc widgets-setup {} {
@@ -1399,8 +1434,20 @@ proc widgets-setup {} {
 
     if {![have-notes]} {
        .cp.ctrl.notes.do configure -state disabled
-    }  
+    }
+
+    button .cp.ctrl.print -text {Print to file} -command print-to-postscript
+    pack .cp.ctrl.print -side top
        
+    #----- message saying we've printed -----
+
+    info-toplevel-create .printed "printed"
+
+    label .printed.info
+    text .printed.rune -state disabled -height 1 -borderwidth 0
+    pack .printed.info -side top
+    pack .printed.rune -side top
+
     #----- island name count and copy -----
 
     label .islands.count
index 76100dc5ccf88a2931a00ea694399f9929634d71..52bd4f7e60e5920be0f278fcc7b29e4378e8f4ed 100755 (executable)
@@ -221,7 +221,7 @@ sub yppedia_archs_chart_labels () {
            next unless exists $winode2island{$vertex};
            my $ccix= $wiarchs->connected_component_by_vertex($vertex);
            my @cc= $wiarchs->connected_component_by_index($ccix);
-           my ($vx,$vy) = split /,/, $vertex;
+           my ($vx,$vy) = split /,/, $vertex;  # /
            my $d2= ($vx-$ax)*($vx-$ax) + ($vy-$ay)*($vy-$ay);
            my $cmp= $best_d2 <=> $d2;
            printf $debugfh "%2d,%-2d arch-island-search %5s d2=%4d cc%-2d".
@@ -464,6 +464,7 @@ sub shortest_path_reduction ($$) {
         not essential and is therefore unnecessary.
 
 END
+    # `
     
     printf $debugfh "spr %s before %d\n", $what, scalar($g->edges());
 
@@ -555,7 +556,7 @@ sub yppedia_ocean_fetch_text () {
            die unless defined $arch;
            $wtisland2arch{$'}= $arch;
        } elsif (m/^ /) {
-           $arch= $';
+           $arch= $'; # '
        } else {
            die;
        }
index ba145eafcc247b6bae90d13244b159d62570928d..5e5e0901f563b097b6554b891e04cf184f89b45a 100755 (executable)
@@ -99,7 +99,7 @@ def fetch():
        soup = BeautifulSoup(dataf)
 
 title_arch_re = regexp.compile('(\\S.*\\S) Archipelago \\((\\S+)\\)$')
-title_any_re = regexp.compile('(\\S.*\\S) \((\\S+)\\)$')
+title_any_re = regexp.compile('(\\S.*\\S) \((\\S+)\\)(?: \(page does not exist\))?$')
 href_img_re = regexp.compile('\\.png$')
 
 def title_arch_info(t):
index 840a6d913582b2aa6d32f783cecc756afce3bffb..717b539131d8ffb0e4ba6abba5f6cddc3b38b199 100755 (executable)
@@ -44,6 +44,8 @@ import random
 import curses
 import termios
 import random
+import subprocess
+import copy
 from optparse import OptionParser
 from StringIO import StringIO
 
@@ -98,28 +100,55 @@ def format_time_interval(ti):
        if ti < 86400: return '%dh' % (ti / 3600)
        return '%dd' % (ti / 86400)
 
+def yppsc_dir():
+       lib = os.getenv("YPPSC_YARRG_SRCBASE")
+       if lib is not None: return lib
+       lib = sys.argv[0] 
+       lib = regexp.sub('/[^/]+$', '', lib)
+       os.environ["YPPSC_YARRG_SRCBASE"] = lib
+       return lib
+
+soup_massage = copy.copy(BeautifulSoup.MARKUP_MASSAGE)
+soup_massage.append(
+               (regexp.compile('(\<td.*") ("center")'),
+                lambda m: m.group(1)+' align='+m.group(2))
+       )
+
+def make_soup(*args, **kwargs):
+       return BeautifulSoup(*args,
+               convertEntities=BeautifulSoup.HTML_ENTITIES,
+               markupMassage=soup_massage,
+                        **kwargs)
+
 #---------- caching and rate-limiting data fetcher ----------
 
 class Fetcher:
-       def __init__(self, ocean, cachedir):
+       def __init__(self, cachedir):
                debug('Fetcher init %s' % cachedir)
-               self.ocean = ocean
                self.cachedir = cachedir
                try: os.mkdir(cachedir)
                except (OSError,IOError), oe:
                        if oe.errno != errno.EEXIST: raise
                self._cache_scan(time.time())
 
-       def default_ocean(self, ocean='ice'):
-               if self.ocean is None:
-                       self.ocean = ocean
+       def _match_url_normalise(self, url):
+               without_scheme = regexp.sub('^[a-z]+://', '', url)
+               without_tail = regexp.sub('/.*', '', without_scheme)
+               return without_tail
 
-       def _cache_scan(self, now):
+       def _cache_scan(self, now, match_url=None):
                # returns list of ages, unsorted
+               if match_url is not None:
+                       match_url = self._match_url_normalise(match_url)
                ages = []
                debug('Fetcher   scan_cache')
                for leaf in os.listdir(self.cachedir):
                        if not leaf.startswith('#'): continue
+                       if match_url is not None:
+                               leaf_url = urllib.unquote_plus(leaf.strip('#'))
+                               leaf_url = self._match_url_normalise(leaf_url)
+                               if leaf_url != match_url:
+                                       continue
                        path = self.cachedir + '/' + leaf
                        try: s = os.stat(path)
                        except (OSError,IOError), oe:
@@ -135,8 +164,8 @@ class Fetcher:
                        ages.append(age)
                return ages
 
-       def need_wait(self, now, imaginary=[]):
-               ages = self._cache_scan(now)
+       def need_wait(self, now, imaginary=[], next_url=None):
+               ages = self._cache_scan(now, match_url=next_url)
                ages += imaginary
                ages.sort()
                debug('Fetcher   ages ' + `ages`)
@@ -148,15 +177,14 @@ class Fetcher:
                                        (min_age, age))
                                need_wait = max(need_wait, min_age - age)
                        min_age += 3
-                       min_age *= 1.25
                if need_wait > 0:
                        need_wait += random.random() - 0.5
                return need_wait
 
-       def _rate_limit_cache_clean(self, now):
-               need_wait = self.need_wait(now)
+       def _rate_limit_cache_clean(self, now, next_url=None):
+               need_wait = self.need_wait(now, next_url=next_url)
                if need_wait > 0:
-                       debug('Fetcher   wait %d' % need_wait)
+                       debug('Fetcher   wait %f' % need_wait)
                        sleep(need_wait)
 
        def fetch(self, url, max_age):
@@ -182,7 +210,7 @@ class Fetcher:
                        return data
 
                debug('Fetcher  fetch')
-               self._rate_limit_cache_clean(now)
+               self._rate_limit_cache_clean(now, next_url=url)
 
                stream = urllib2.urlopen(url)
                data = stream.read()
@@ -195,12 +223,38 @@ class Fetcher:
                debug('Fetcher  stored')
                return data
 
+class Yoweb(Fetcher):
+       def __init__(self, ocean, cachedir):
+               debug('Yoweb init %s' % cachedir)
+               self.ocean = ocean
+               Fetcher.__init__(self, cachedir)
+
+       def default_ocean(self, ocean='ice'):
+               if self.ocean is None:
+                       self.ocean = ocean
+
        def yoweb(self, kind, tail, max_age):
                self.default_ocean()
+               assert(self.ocean)
                url = 'http://%s.puzzlepirates.com/yoweb/%s%s' % (
                        self.ocean, kind, tail)
                return self.fetch(url, max_age)
 
+class Yppedia(Fetcher):
+       def __init__(self, cachedir):
+               debug('Yoweb init %s' % cachedir)
+               self.base = 'http://yppedia.puzzlepirates.com/'
+               self.localhtml = opts.localhtml
+               Fetcher.__init__(self, cachedir)
+
+       def __call__(self, rhs):
+               if self.localhtml is None:
+                       url = self.base + rhs
+                       debug('Yppedia retrieving YPP '+url);
+                       return self.fetch(url, 3000)
+               else:
+                       return file(opts.localhtml + '/' + rhs, 'r')
+
 #---------- logging assistance for troubled screenscrapers ----------
 
 class SoupLog:
@@ -222,9 +276,7 @@ class SomethingSoupInfo(SoupLog):
        def __init__(self, kind, tail, max_age):
                SoupLog.__init__(self)
                html = fetcher.yoweb(kind, tail, max_age)
-               self._soup = BeautifulSoup(html,
-                       convertEntities=BeautifulSoup.HTML_ENTITIES
-                       )
+               self._soup = make_soup(html)
 
 #---------- scraper for pirate pages ----------
 
@@ -331,12 +383,14 @@ u'\\s*\\S*/([-A-Za-z]+)\\s*$|\\s*\\S*/\\S*\\s*\\(ocean\\-wide(?:\\s|\\xa0)+([-A-
 
 class CrewInfo(SomethingSoupInfo):
        # Public data members:
+       #  ci.crewid
        #  ci.crew = [ ('Captain',        ['Pirate', ...]),
        #              ('Senior Officer', [...]),
        #               ... ]
        #  pi.msgs = [ 'message describing problem with scrape' ]
 
        def __init__(self, crewid, max_age=300):
+               self.crewid = crewid
                SomethingSoupInfo.__init__(self,
                        'crew/info.wm?crewid=', crewid, max_age)
                self._find_crew()
@@ -377,6 +431,322 @@ class CrewInfo(SomethingSoupInfo):
        def __str__(self):
                return `(self.crew, self.msgs)`
 
+class FlagRelation():
+       # Public data members (put there by hand by creater)
+       #       other_flagname
+       #       other_flagid
+       #       yoweb_heading
+       #       this_declaring
+       #       other_declaring_min
+       #       other_declaring_max
+       # where {this,other}_declaring{,_min,_max} are:
+       #       -1      {this,other} is declaring war
+       #        0      {this,other} is not doing either
+       #       +1      {this,other} is allying
+       def __repr__(self):
+               return '<FlagRelation %s %d/%d..%d %s %s>' % (
+                       self.yoweb_heading, self.this_declaring,
+                       self.other_declaring_min, self.other_declaring_max,
+                       self.other_flagname, self.other_flagid)
+
+class FlagInfo(SomethingSoupInfo):
+       # Public data members (after init):
+       #
+       #   flagid
+       #   name        #               string
+       #
+       #   relations[n] = FlagRelation
+       #   relation_byname[otherflagname] = relations[some_n]
+       #   relation_byid[otherflagname] = relations[some_n]
+       #
+       #   islands[n] = (islandname, islandid)
+       #
+       def __init__(self, flagid, max_age=600):
+               self.flagid = flagid
+               SomethingSoupInfo.__init__(self,
+                       'flag/info.wm?flagid=', flagid, max_age)
+               self._find_flag()
+
+       def _find_flag(self):
+               font2 = self._soup.find('font',{'size':'+2'})
+               self.name = font2.find('b').contents[0]
+
+               self.relations = [ ]
+               self.relation_byname = { }
+               self.relation_byid = { }
+               self.islands = [ ]
+
+               magnate = self._soup.find('img',{'src':
+                       '/yoweb/images/repute-MAGNATE.png'})
+               warinfo = (magnate.findParent('table').findParent('tr').
+                       findNextSibling('tr').findNext('td',{'align':'left'}))
+
+               def warn(m):
+                       print >>sys.stderr, 'WARNING: '+m
+
+               def wi_warn(head, waritem):
+                       warn('unknown warmap item: %s: %s' % 
+                               (`head`, ``waritem``))
+
+               def wihelp_item(waritem, thing):
+                       url = waritem.get('href', None)
+                       if url is None:
+                               return ('no url for '+thing,None,None)
+                       m = regexp.search('\?'+thing+'id=(\d+)$', url)
+                       if not m: return ('no '+thing+'id',None,None)
+                       tid = m.group(1)
+                       tname = waritem.string
+                       if tname is None:
+                               return (thing+' name not just string',None,None)
+                       return (None,tid,tname)
+
+               def wi_alwar(head, waritem, thisdecl, othermin, othermax):
+                       (err,flagid,flagname) = wihelp_item(waritem,'flag')
+                       if err: return err
+                       rel = self.relation_byid.get(flagid, None)
+                       if rel: return 'flag id twice!'
+                       if flagname in self.relation_byname:
+                               return 'flag name twice!'
+                       rel = FlagRelation()
+                       rel.other_flagname = flagname
+                       rel.other_flagid = flagid
+                       rel.yoweb_heading = head
+                       rel.this_declaring = thisdecl
+                       rel.other_declaring_min = othermin
+                       rel.other_declaring_max = othermax
+                       self.relations.append(rel)
+                       self.relation_byid[flagid] = rel
+                       self.relation_byname[flagid] = rel
+
+               def wi_isle(head, waritem):
+                       (err,isleid,islename) = wihelp_item(waritem,'island')
+                       if err: return err
+                       self.islands.append((isleid,islename))
+
+               warmap = {
+                       'Allied with':                  (wi_alwar,+1,+1,+1),
+                       'Declaring war against':        (wi_alwar,-1, 0,+1),
+                       'At war with':                  (wi_alwar,-1,-1,-1),
+                       'Trying to form an alliance with': (wi_alwar,+1,-1,0),
+                       'Islands controlled by this flag': (wi_isle,),
+                       }
+
+               how = (wi_warn, None)
+
+               for waritem in warinfo.findAll(['font','a']):
+                       if waritem is None: break
+                       if waritem.name == 'font':
+                               colour = waritem.get('color',None)
+                               if colour.lstrip('#') != '958A5F':
+                                       warn('strange colour %s in %s' %
+                                               (colour,``waritem``))
+                                       continue
+                               head = waritem.string
+                               if head is None:
+                                       warn('no head string in '+``waritem``)
+                                       continue
+                               head = regexp.sub('\\s+', ' ', head).strip()
+                               head = head.rstrip(':')
+                               how = (head,) + warmap.get(head, (wi_warn,))
+                               continue
+                       assert(waritem.name == 'a')                             
+
+                       debug('WARHOW %s(%s, waritem, *%s)' %
+                               (how[1], `how[0]`, `how[2:]`))
+                       bad = how[1](how[0], waritem, *how[2:])
+                       if bad:
+                               warn('bad waritem %s: %s: %s' % (`how[0]`,
+                                       bad, ``waritem``))
+
+       def __str__(self):
+               return `(self.name, self.islands, self.relations)`
+
+#---------- scraper for ocean info incl. embargoes etc. ----------
+
+class IslandBasicInfo():
+       # Public data attributes:
+       #  ocean
+       #  name
+       # Public data attributes maybe set by caller:
+       #  arch
+       def __init__(self, ocean, islename):
+               self.ocean = ocean
+               self.name = islename
+       def yppedia(self):
+               def q(x): return urllib.quote(x.replace(' ','_'))
+               url_rhs = q(self.name) + '_(' + q(self.ocean) + ')'
+               return yppedia(url_rhs)
+       def __str__(self):
+               return `(self.ocean, self.name)`
+
+class IslandExtendedInfo(IslandBasicInfo):
+       # Public data attributes (inherited):
+       #  ocean
+       #  name
+       # Public data attributes (additional):
+       #  islandid
+       #  yoweb_url
+       #  flagid
+       def __init__(self, ocean, islename):
+               IslandBasicInfo.__init__(self, ocean, islename)
+               self.islandid = None
+               self.yoweb_url = None
+               self._collect_yoweb()
+               self._collect_flagid()
+
+       def _collect_yoweb(self):
+               debug('IEI COLLECT YOWEB '+`self.name`)
+               self.islandid = None
+               self.yoweb_url = None
+
+               soup = make_soup(self.yppedia())
+               content = soup.find('div', attrs = {'id': 'content'})
+               yoweb_re = regexp.compile('^http://\w+\.puzzlepirates\.com/'+
+                       'yoweb/island/info\.wm\?islandid=(\d+)$')
+               a = soup.find('a', attrs = { 'href': yoweb_re })
+               if a is None:
+                       debug('IEI COLLECT YOWEB '+`self.name`+' NONE')
+                       return
+
+               debug('IEI COLLECT YOWEB '+`self.name`+' GOT '+``a``)
+               self.yoweb_url = a['href']
+               m = yoweb_re.search(self.yoweb_url)
+               self.islandid = m.group(1)
+
+       def _collect_flagid(self):
+               self.flagid = None
+
+               yo = self.yoweb_url
+               debug('IEI COLLECT FLAGID '+`self.name`+' URL '+`yo`)
+               if yo is None: return None
+               dataf = fetcher.fetch(yo, 1800)
+               soup = make_soup(dataf)
+               ruler_re = regexp.compile(
+                       '/yoweb/flag/info\.wm\?flagid=(\d+)$')
+               ruler = soup.find('a', attrs = { 'href': ruler_re })
+               if not ruler: 
+                       debug('IEI COLLECT FLAGID '+`self.name`+' NONE')
+                       return
+               debug('IEI COLLECT FLAGID '+`self.name`+' GOT '+``ruler``)
+               m = ruler_re.search(ruler['href'])
+               self.flagid = m.group(1)
+
+       def __str__(self):
+               return `(self.ocean, self.islandid, self.name,
+                       self.yoweb_url, self.flagid)`
+
+class IslandFlagInfo(IslandExtendedInfo):
+       # Public data attributes (inherited):
+       #  ocean
+       #  name
+       #  islandid
+       #  yoweb_url
+       #  flagid
+       # Public data attributes (additional):
+       #  flag
+       def __init__(self, ocean, islename):
+               IslandExtendedInfo.__init__(self, ocean, islename)
+               self.flag = None
+               self._collect_flag()
+
+       def _collect_flag(self):
+               if self.flagid is None: return
+               self.flag = FlagInfo(self.flagid, 1800)
+
+       def __str__(self):
+               return IslandExtendedInfo.__str__(self) + '; ' + str(self.flag)
+
+class NullProgressReporter():
+       def doing(self, msg): pass
+       def stop(self): pass
+
+class TypewriterProgressReporter():
+       def __init__(self):
+               self._l = 0
+       def doing(self,m):
+               self._doing(m + '...')
+       def _doing(self,m):
+               self._write('\r')
+               self._write(m)
+               less = self._l - len(m)
+               if less > 0:
+                       self._write(' ' * less)
+                       self._write('\b' * less)
+               self._l = len(m)
+               sys.stdout.flush()
+       def stop(self):
+               self._doing('')
+               self._l = 0
+       def _write(self,t):
+               sys.stdout.write(t)
+
+class OceanInfo():
+       # Public data attributes:
+       #   oi.islands[islename] = IslandInfo(...)
+       #   oi.arches[archname][islename] = IslandInfo(...)
+       def __init__(self, isleclass=IslandBasicInfo):
+               self.isleclass = isleclass
+               self.ocean = fetcher.ocean.lower().capitalize()
+
+               progressreporter.doing('fetching ocean info')
+
+               cmdl = ['./yppedia-ocean-scraper']
+               if opts.localhtml is not None:
+                       cmdl += ['--local-html-dir',opts.localhtml]
+               cmdl += [self.ocean]
+               debug('OceanInfo collect running ' + `cmdl`)
+               oscraper = subprocess.Popen(
+                       cmdl,
+                       stdout = subprocess.PIPE,
+                       cwd = yppsc_dir()+'/yarrg',
+                       shell=False, stderr=None,
+                       )
+               h = oscraper.stdout.readline()
+               debug('OceanInfo collect h '+`h`)
+               assert(regexp.match('^ocean ', h))
+               arch_re = regexp.compile('^ (\S.*)')
+               island_re = regexp.compile('^  (\S.*)')
+
+               oscraper.wait()
+               assert(oscraper.returncode == 0)
+
+               self.islands = { }
+               self.arches = { }
+               archname = None
+
+               isles = [ ]
+               progressreporter.doing('parsing ocean info')
+
+               for l in oscraper.stdout:
+                       debug('OceanInfo collect l '+`l`)
+                       l = l.rstrip('\n')
+                       m = island_re.match(l)
+                       if m:
+                               assert(archname is not None)
+                               islename = m.group(1)
+                               isles.append((archname, islename))
+                               continue
+                       m = arch_re.match(l)
+                       if m:
+                               archname = m.group(1)
+                               assert(archname not in self.arches)
+                               self.arches[archname] = { }
+                               continue
+                       assert(False)
+
+               for i in xrange(0, len(isles)-1):
+                       (archname, islename) = isles[i]
+                       progressreporter.doing(
+                               'fetching isle info %2d/%d (%s: %s)'
+                               % (i, len(isles), archname, islename))
+                       isle = self.isleclass(self.ocean, islename)
+                       isle.arch = archname
+                       self.islands[islename] = isle
+                       self.arches[archname][islename] = isle
+
+       def __str__(self):
+               return `(self.islands, self.arches)`
+
 #---------- pretty-printer for tables of pirate puzzle standings ----------
 
 class StandingsTable:
@@ -681,6 +1051,56 @@ class ChatLogTracker:
                else:
                        return None
 
+       def local_command(self, metacmd):
+               # returns None if all went well, or problem message
+               return self._command(self._myself.name, metacmd,
+                       "local", time.time(), 
+                       (lambda m: debug('CMD %s' % metacmd)))
+
+       def _command(self, cmdr, metacmd, chan, timestamp, d):
+               # returns None if all went well, or problem message
+               metacmd = regexp.sub('\\s+', ' ', metacmd).strip()
+               m2 = regexp.match(
+                   '/([adj]) (?:([A-Za-z* ]+)\\s*:)?([A-Za-z ]+)$',
+                   metacmd)
+               if not m2: return "unknown syntax or command"
+
+               (cmd, pattern, targets) = m2.groups()
+               dml = ['cmd', chan, cmd]
+
+               if cmd == 'a': each = self._onboard_event
+               elif cmd == 'd': each = disembark
+               else: each = lambda *l: self._onboard_event(*l,
+                               **{'jobber':'applied'})
+
+               if cmdr == self._myself.name:
+                       dml.append('self')
+                       how = 'cmd: %s' % cmd
+               else:
+                       dml.append('other')
+                       how = 'cmd: %s %s' % (cmd,cmdr)
+
+               if cmd == 'j':
+                       if pattern is not None:
+                               return "/j command does not take a vessel"
+                       v = None
+               else:
+                       v = self._find_matching_vessel(
+                               pattern, timestamp, cmdr,
+                               dml, create=True)
+
+               if cmd == 'j' or v is not None:
+                       targets = targets.strip().split(' ')
+                       dml.append(`len(targets)`)
+                       for target in targets:
+                               each(v, timestamp, target.title(), how)
+                       self._vessel_updated(v, timestamp)
+
+               dm = ' '.join(dml)
+               return d(dm)
+
+               return None
+
        def chatline(self,l):
                rm = lambda re: regexp.match(re,l)
                d = lambda m: self._debug_line_disposition(timestamp,l,m)
@@ -783,46 +1203,12 @@ class ChatLogTracker:
 
                def chat_metacmd(chan):
                        (cmdr, metacmd) = m.groups()
-                       metacmd = regexp.sub('\\s+', ' ', metacmd).strip()
-                       m2 = regexp.match(
-                           '/([adj]) (?:([A-Za-z* ]+)\\s*:)?([A-Za-z ]+)$',
-                           metacmd)
-                       if not m2: return chat(chan)
-
-                       (cmd, pattern, targets) = m2.groups()
-                       dml = ['cmd', chan, cmd]
-
-                       if cmd == 'a': each = self._onboard_event
-                       elif cmd == 'd': each = disembark
-                       else: each = lambda *l: self._onboard_event(*l,
-                                       **{'jobber':'applied'})
-
-                       if cmdr == self._myself.name:
-                               dml.append('self')
-                               how = 'cmd: %s' % cmd
+                       whynot = self._command(
+                               cmdr, metacmd, chan, timestamp, d)
+                       if whynot is not None:
+                               return chat(chan)
                        else:
-                               dml.append('other')
-                               how = 'cmd: %s %s' % (cmd,cmdr)
-
-                       if cmd == 'j':
-                               if pattern is not None:
-                                       return chat(chan)
-                               v = None
-                       else:
-                               v = self._find_matching_vessel(
-                                       pattern, timestamp, cmdr,
-                                       dml, create=True)
-
-                       if cmd == 'j' or v is not None:
-                               targets = targets.strip().split(' ')
-                               dml.append(`len(targets)`)
-                               for target in targets:
-                                       each(v, timestamp, target.title(), how)
-                               self._vessel_updated(v, timestamp)
-
-                       dm = ' '.join(dml)
-                       chat_core(cmdr, 'cmd '+chan)
-                       return d(dm)
+                               chat_core(cmdr, 'cmd '+chan)
 
                m = rm('(\\w+) (?:issued an order|ordered everyone) "')
                if m: return ob1('general order');
@@ -1010,16 +1396,29 @@ def do_pirate(pirates, bu):
                print '%s: %s,' % (`pirate`, info)
        print '}'
 
-def prep_crew_of(args, bu, max_age=300):
-       if len(args) != 1: bu('crew-of takes one pirate name')
+def prep_crewflag_of(args, bu, max_age, selector, constructor):
+       if len(args) != 1: bu('crew-of etc. take one pirate name')
        pi = PirateInfo(args[0], max_age)
-       if pi.crew is None: return None
-       return CrewInfo(pi.crew[0], max_age)
+       cf = selector(pi)
+       if cf is None: return None
+       return constructor(cf[0], max_age)
+
+def prep_crew_of(args, bu, max_age=300):
+       return prep_crewflag_of(args, bu, max_age,
+               (lambda pi: pi.crew), CrewInfo)
+
+def prep_flag_of(args, bu, max_age=300):
+       return prep_crewflag_of(args, bu, max_age,
+               (lambda pi: pi.flag), FlagInfo)
 
 def do_crew_of(args, bu):
        ci = prep_crew_of(args, bu)
        print ci
 
+def do_flag_of(args, bu):
+       fi = prep_flag_of(args, bu)
+       print fi
+
 def do_standings_crew_of(args, bu):
        ci = prep_crew_of(args, bu, 60)
        tab = StandingsTable(sys.stdout)
@@ -1032,6 +1431,101 @@ def do_standings_crew_of(args, bu):
                        pi = PirateInfo(p, random.randint(900,1800))
                        tab.pirate(pi)
 
+def do_ocean(args, bu):
+       if (len(args)): bu('ocean takes no further arguments')
+       fetcher.default_ocean()
+       oi = OceanInfo(IslandFlagInfo)
+       print oi
+       for islename in sorted(oi.islands.keys()):
+               isle = oi.islands[islename]
+               print isle
+
+def do_embargoes(args, bu):
+       if (len(args)): bu('ocean takes no further arguments')
+       fetcher.default_ocean()
+       oi = OceanInfo(IslandFlagInfo)
+       wr = sys.stdout.write
+       print ('EMBARGOES:  Island    | Owning flag'+
+               '                    | Embargoed flags')
+
+       def getflname(isle):
+               if isle.islandid is None: return 'uncolonisable'
+               if isle.flag is None: return 'uncolonised'
+               return isle.flag.name
+
+       progressreporter.stop()
+
+       for archname in sorted(oi.arches.keys()):
+               print 'ARCHIPELAGO: ',archname
+               for islename in sorted(oi.arches[archname].keys()):
+                       isle = oi.islands[islename]
+                       wr(' %-20s | ' % isle.name)
+                       flname = getflname(isle)
+                       wr('%-30s | ' % flname)
+                       flag = isle.flag
+                       if flag is None: print ''; continue
+                       delim = ''
+                       for rel in flag.relations:
+                               if rel.this_declaring >= 0: continue
+                               wr(delim)
+                               wr(rel.other_flagname)
+                               delim = '; '
+                       print ''
+
+def do_embargoes_flag_of(args, bu):
+       progressreporter.doing('fetching flag info')
+       fi = prep_flag_of(args, bu)
+       if fi is None:
+               progressreporter.stop()
+               print 'Pirate is not in a flag.'
+               return
+
+       oi = OceanInfo(IslandFlagInfo)
+
+       progressreporter.stop()
+       print ''
+
+       any = False
+       for islename in sorted(oi.islands.keys()):
+               isle = oi.islands[islename]
+               flag = isle.flag
+               if flag is None: continue
+               for rel in flag.relations:
+                       if rel.this_declaring >= 0: continue
+                       if rel.other_flagid != fi.flagid: continue
+                       if not any: print 'EMBARGOED:'
+                       any = True
+                       print "  %-30s (%s)" % (islename, flag.name)
+       if not any:
+               print 'No embargoes.'
+       print ''
+
+       war_flag(fi)
+       print ''
+
+def do_war_flag_of(args, bu):
+       fi = prep_flag_of(args, bu)
+       war_flag(fi)
+
+def war_flag(fi):
+       any = False
+       for certain in [True, False]:
+               anythis = False
+               for rel in fi.relations:
+                       if rel.this_declaring >= 0: continue
+                       if (rel.other_declaring_max < 0) != certain: continue
+                       if not anythis:
+                               if certain: m = 'SINKING PvP'
+                               else: m = 'RISK OF SINKING PvP'
+                               print '%s (%s):' % (m, rel.yoweb_heading)
+                       anythis = True
+                       any = True
+                       print " ", rel.other_flagname
+       if not any:
+               print 'No sinking PvP.'
+
+#----- modes which use the chat log parser are quite complex -----
+
 class ProgressPrintPercentage:
        def __init__(self, f=sys.stdout):
                self._f = f
@@ -1047,8 +1541,6 @@ class ProgressPrintPercentage:
                self._f.write('                   \r')
                self._f.flush()
 
-#----- modes which use the chat log parser are quite complex -----
-
 def prep_chat_log(args, bu,
                progress=ProgressPrintPercentage(),
                max_myself_age=3600):
@@ -1220,15 +1712,27 @@ def ship_aid_core(myself, track, displayer, kreader):
        rotate_nya = '/-\\'
 
        sort = NameSorter()
+       clicmd = None
+       clierr = None
+       cliexec = None
 
        while True:
                track.catchup()
                now = time.time()
 
-               (vn, s) = find_vessel()
-               s = track.myname() + s
-               s += " at %s" % time.strftime("%Y-%m-%d %H:%M:%S")
-               s += kreader.info()
+               (vn, vs) = find_vessel()
+
+               s = ''
+               if cliexec is not None:
+                       s += '...'
+               elif clierr is not None:
+                       s += 'Error: '+clierr
+               elif clicmd is not None:
+                       s += '/' + clicmd
+               else:
+                       s = track.myname() + vs
+                       s += " at %s" % time.strftime("%Y-%m-%d %H:%M:%S")
+                       s += kreader.info()
                s += '\n'
 
                tbl_s = StringIO()
@@ -1269,11 +1773,34 @@ def ship_aid_core(myself, track, displayer, kreader):
                displayer.show(s)
                tbl_s.close()
 
+               if cliexec is not None:
+                       clierr = track.local_command("/"+cliexec.strip())
+                       cliexec = None
+                       continue
+
                k = kreader.getch()
                if k is None:
                        rotate_nya = rotate_nya[1:3] + rotate_nya[0]
                        continue
 
+               if clierr is not None:
+                       clierr = None
+                       continue
+
+               if clicmd is not None:
+                       if k == '\r' or k == '\n':
+                               cliexec = clicmd
+                               clicmd = clicmdbase
+                       elif k == '\e' and clicmd != "":
+                               clicmd = clicmdbase
+                       elif k == '\33':
+                               clicmd = None
+                       elif k == '\b' or k == '\177':
+                               clicmd = clicmd[ 0 : len(clicmd)-1 ]
+                       else:
+                               clicmd += k
+                       continue
+
                if k == 'q': break
                elif k == 'g': sort = SkillSorter('Gunning')
                elif k == 'c': sort = SkillSorter('Carpentry')
@@ -1283,6 +1810,8 @@ def ship_aid_core(myself, track, displayer, kreader):
                elif k == 'd': sort = SkillSorter('Battle Navigation')
                elif k == 't': sort = SkillSorter('Treasure Haul')
                elif k == 'a': sort = NameSorter()
+               elif k == '/': clicmdbase = ""; clicmd = clicmdbase
+               elif k == '+': clicmdbase = "a "; clicmd = clicmdbase
                else: pass # unknown key command
 
 #---------- individual keystroke input ----------
@@ -1316,7 +1845,7 @@ class KeystrokeReader(DummyKeystrokeReader):
 #---------- main program ----------
 
 def main():
-       global opts, fetcher
+       global opts, fetcher, yppedia, progressreporter
 
        pa = OptionParser(
 '''usage: .../yoweb-scrape [OPTION...] ACTION [ARGS...]
@@ -1325,6 +1854,8 @@ actions:
  yoweb-scrape [--ocean OCEAN ...] crew-of PIRATE
  yoweb-scrape [--ocean OCEAN ...] standings-crew-of PIRATE
  yoweb-scrape [--ocean OCEAN ...] track-chat-log CHAT-LOG
+ yoweb-scrape [--ocean OCEAN ...] ocean|embargoes
+ yoweb-scrape [--ocean OCEAN ...] war-flag-of|embargoes-flag-of PIRATE
  yoweb-scrape [options] ship-aid CHAT-LOG  (must be .../PIRATE_OCEAN_chat-log*)
 
 display modes (for --display) apply to ship-aid:
@@ -1345,6 +1876,9 @@ display modes (for --display) apply to ship-aid:
        ao('--display', action='store', dest='display',
                type='choice', choices=['dumb','overwrite'],
                help='how to display ship aid')
+       ao('--local-ypp-dir', action='store', dest='localhtml',
+               help='get yppedia pages from local directory LOCALHTML'+
+                       ' instead of via HTTP')
 
        ao_jt = lambda wh, t: ao(
                '--timeout-sa-'+wh, action='store', dest='timeout_'+wh,
@@ -1395,7 +1929,13 @@ display modes (for --display) apply to ship-aid:
                else:
                        opts.display = 'overwrite'
 
-       fetcher = Fetcher(opts.ocean, opts.cache_dir)
+       fetcher = Yoweb(opts.ocean, opts.cache_dir)
+       yppedia = Yppedia(opts.cache_dir)
+
+       if opts.debug or not os.isatty(0):
+                progressreporter = NullProgressReporter()
+       else:
+               progressreporter = TypewriterProgressReporter()
 
        mode_fn(args[1:], pa.error)
 
index f8f881a4adfe6116a3da5029a731b3b9b78f6947..4576c39bde3cb7e554243f59ea37de05c4f71712 100644 (file)
@@ -42,6 +42,9 @@ Key features:
    are weaker.  This functionality is disabled if stdin is not a tty.
    Other keystrokes:
       q: quit the program (^C works too)
+      /: start entering a local extra commands (see below)
+      +: start entering local extra commands starting "/a"
+      ESC: clear local extra command or abandon local extra commands entry
 
 Things you need to know:
 
index 44b6fac800f18af36ed57cc1d8c765e16d101d3a..ccf26e53c74fb5651f90882cb20c3f763efa9112 100755 (executable)
@@ -118,7 +118,7 @@ sub watch_dir ($) {
            next unless $size;
            last if $size==$lastsize;
            $lastsize= $size;
-           select undef,undef,undef, 0.100;
+           select undef,undef,undef, 0.300;
        }
 
        $child= fork;  defined $child or die $!;
diff --git a/ypp-chatlog-alerter b/ypp-chatlog-alerter
new file mode 100755 (executable)
index 0000000..05a9c20
--- /dev/null
@@ -0,0 +1,340 @@
+#!/usr/bin/wish
+
+proc manyset {list args} {
+    foreach val $list var $args {
+        upvar 1 $var my
+        set my $val
+    }
+}
+
+set progname ypp-chatlog-alerter
+set height 5
+
+proc menuent {w l a x} {
+    set c [list .mbar.$w add command -label $l -command $x]
+    if {[string length $a]} { lappend c -accel Command-$a }
+    eval $c
+}
+proc menus {} {
+    global height
+    
+    menu .mbar -tearoff 0
+    foreach w {file edit} l {File Edit} {
+       menu .mbar.$w -tearoff 0
+       .mbar add cascade -menu .mbar.$w -label $l
+    }
+    foreach l {Open Quit} a {O Q} x {newfile exit} {
+       menuent file $l $a $x
+    }
+    foreach l {Cut Copy Paste Clear} a {X C V {}} {
+       menuent edit $l $a [list event generate {[focus]} <<$l>>]]
+    }
+    . configure -menu .mbar
+}
+
+proc nonportability {} {
+    global progname defaultfile
+    
+    switch -exact [tk windowingsystem] {
+       aqua {
+           set defaultfile ~/Library/Preferences/$progname.prefs
+       }
+       x11 {
+           set defaultfile ~/.$progname.rc
+       }
+       default {
+           error ?
+       }
+    }
+}
+
+set lw_ls {times pirates messages}
+set lw_ws {.time .pirate .msg}
+
+proc widgets {} {
+    global height lw_ws prtimes
+    listbox .time   -width  5 -background black \
+       -listvariable prtimes -foreground white
+    listbox .pirate -width 14 -background black
+    listbox .msg    -width 80
+    for_lw {
+       $w configure -height $height -borderwidth 0 -activestyle none \
+           -highlightthickness 0
+    }
+    eval pack $lw_ws -side left
+    label .overlay -relief raised -foreground white
+}
+
+proc shownotice {colour message} {
+    .overlay configure -text $message -background $colour
+    place .overlay -relx 0.5 -rely 0.5 -anchor center
+}
+proc hidenotice {} {
+    place forget .overlay
+}
+
+proc newfile {} {
+    global currentfile defaultfile logfile
+    
+    set newfile [tk_getOpenFile -multiple 0 -initialfile $currentfile \
+                    -title "Select YPP log to track"]
+    if {![string length $newfile]} return
+
+    catch { close $logfile }
+    catch { unset logfile }
+
+    set currentfile $newfile
+    set newdefaults [open $defaultfile.new w]
+    puts $newdefaults "[list set currentfile $currentfile]"
+    close $newdefaults
+    file rename -force $defaultfile.new $defaultfile
+
+    clearlists
+    pollfile
+}
+
+proc for_lw {args} {
+    global lw_ls lw_ws
+    set body [lindex $args end]
+    set args [lreplace $args end end]
+    uplevel 1 [list \
+                  foreach l $lw_ls \
+                          w $lw_ws] \
+                          $args \
+                        [list $body]
+}
+
+set e_life 120
+set tint_switch 90
+set bell_again 60
+set tint_switched [expr { exp( -($tint_switch+0.0) / $e_life ) }]
+
+proc retint {} {
+    global times e_life retint_after otherevent prtimes
+    catch { after cancel $retint_after }
+    set i 0
+    set now [clock seconds]
+    set latest $otherevent
+    set newprtimes {}
+    foreach time $times {
+       set age [expr {$now-$time}]
+       if {!$time} {
+           lappend newprtimes {}
+       } elseif {$age < 60} {
+           lappend newprtimes [format "%3ds" $age]
+       } elseif {$age < 3600} {
+           lappend newprtimes [format "%3dm" [expr {$age/60}]]
+       } else {
+           lappend newprtimes [format "%3dh" [expr {$age/3600}]]
+       }
+       set latest [expr { $time > $latest ? $time : $latest }]
+       set tint [expr { exp( (-($age >= 0 ? $age : 0) + 0.0) / $e_life ) }]
+#puts "AGE $age LA $latest TI $tint"
+       tintentries .msg $i $tint
+       incr i
+    }
+    set prtimes $newprtimes
+    set next [expr { ($now - $latest < 10 ? 10 :
+                     $now - $latest > 3000 ? 3000 :
+                     $now - $latest
+                     ) * 10 }]
+#puts "nexting $latest $now $next"
+    set retint_after [after $next retint]
+}
+
+proc tintentries {ws y tint} {
+    global tint_switched
+    #puts "$tint $tint_switched"
+    set yellow [format "%02x" [expr {round( 255 *
+        ( $tint >= $tint_switched ? $tint : 0 )
+                                           )}]]
+    set black [format "%02x" [expr {round( 255 *
+       ( $tint >= $tint_switched ? 0 : ($tint / $tint_switched)*0.75 + 0.25 )
+                                          )}]]
+    set fg [format "#${black}${black}${black}"]
+    set bg [format "#${yellow}${yellow}00"]
+    foreach w $ws { $w itemconfigure $y -foreground $fg -background $bg }
+}
+
+proc clearlists {} {
+    global height otherevent
+    global times prtimes pirates messages
+    set currentfile {}
+    
+    for_lw { $w delete 0 end; set $l {} }
+    set ntimes {}
+    set prtimes {}
+    for {set i 0} {$i<$height} {incr i} {
+       for_lw { lappend $l {}; $w insert end {} }
+       lappend ntimes 0
+       lappend prtimes {}
+    }
+    set times $ntimes
+    set otherevent [clock seconds]
+    retint
+}
+
+proc showtints {} {
+    global e_life
+    set divs 20
+    listbox .tints -width 60 -height [expr {$divs+1}]
+    for {set y 0} {$y <= $divs} {incr y} {
+       set tint [expr {($y+0.0)/$divs}]
+       .tints insert end \
+           "[format "#%2d   %6f   %4ds" $y $tint [expr {round(
+                $tint > 0 ? -log($tint) * $e_life : "9999"
+            )}]]  The quick brown fox jumped over the lazy dog"
+       tintentries .tints $y $tint
+    }
+    pack .tints -side bottom
+}
+
+proc file-read-lines {lvar body} {
+    upvar 1 $lvar l
+    global logfile poll_after lastactivity bufdata
+    
+#puts f-r-l
+    if {![info exists logfile]} {
+       return
+    }
+    while 1 {
+       if {[catch { read $logfile } got]} {
+           file-error $got
+           return
+       }
+#puts "f-r-l [string length $got]"
+        if {![string length $got] && [eof $logfile]} {
+           set ago [expr { [clock seconds] - $lastactivity }]
+           set interval [expr {( $ago < 10 ? 10 :
+                                 $ago > 3000 ? 3000 :
+                                 $ago ) * 10}]
+#puts "requeue filepoll $interval"
+           set poll_after [after $interval pollfile]
+           return
+       }
+       set lastactivity [clock seconds]
+
+       while {[regexp {^(.*?)[\r\n]+(.*)$} $got dummy lhs rhs]} {
+           set l "$bufdata$lhs"
+           set bufdata {}
+           set got $rhs
+#puts ">>$l<<"
+           uplevel 1 $body
+       }
+       append bufdata $got
+    }
+}
+
+proc file-error {emsg} {
+    global logfile
+    
+    shownotice red "Error reading logfile $currentfile:\n$emsg"
+    catch { close $logfile }
+    catch { unset logfile }
+}      
+
+proc pollfile {} {
+    global poll_after logfile currentfile
+    global errorCode errorInfo bufdata lastactivity
+
+    catch { after cancel $poll_after }
+    if {![string length $currentfile]} {
+       shownotice red "No log file selected.  Use File / Open."
+       return
+    }
+    if {![info exists logfile]} {
+       set bufdata {}
+       set lastactivity [clock seconds] 
+       if {[catch { set logfile [open $currentfile r] } emsg]} {
+           shownotice red "Error opening logfile $currentfile:\n$emsg"
+           return
+       }
+       shownotice \#000080 "Reading $currentfile"
+       if {[catch {
+           seek $logfile -1024 end
+       } emsg]} {
+           if {![string match {POSIX EINVAL *} $errorCode]} {
+               file-error $emsg
+           }
+       }
+       file-read-lines l { }
+    }
+    file-read-lines l {
+       hidenotice
+       if {[regexp {^\[\d+:\d+:\d+\] (.*)} $l dummy rhs]} {
+#puts PROCLINE
+           process-line $rhs
+       }
+    }
+}
+
+proc process-line {l} {
+    if {[regexp {^(\w+) tells ye, \"(.*)\"$} $l dummy pirate msg]} {
+#puts "MESSAGE $l"
+       message $pirate $msg
+    }
+}
+
+proc message {pirate msg} {
+    global times pirates messages height
+    global lw_ls lw_ws bell_again
+    
+    set ix [lsearch -exact $pirates $pirate]
+    set now [clock seconds]
+    
+    if {$bell_again > -2 &&
+       ($ix<0 || [lindex $times $ix] < $now-$bell_again)} {
+       bell -nice
+    }
+    if {$ix < 0} {
+       set cix 0
+       set oldest $now
+       foreach time $times {
+           if {$time < $oldest} {
+               set oldest $time
+               set ix $cix
+           }
+           incr cix
+       }
+       for_lw {
+           set $l [lreplace [set $l] $ix $ix]
+           lappend $l {}
+           $w delete $ix
+           $w insert end {}
+       }
+       set ix [expr {$height-1}]
+    }
+    for_lw new [list $now $pirate $msg] {
+       set $l [lreplace [set $l] $ix $ix $new]
+       $w delete $ix
+       $w insert $ix $new
+    }
+#puts "TIMES $times"
+    .pirate itemconfigure $ix -foreground white
+    retint
+}
+
+proc parseargs {} {
+    global argv
+    foreach arg $argv {
+       if {![string compare $arg --test-tints]} {
+           showtints
+       } elseif {![string compare $arg --no-bell]} {
+           set bell_again -2
+       } else {
+           error "unknown option $arg"
+       }
+    }
+}
+
+menus
+nonportability
+parseargs
+widgets
+clearlists
+
+if {[file exists $defaultfile]} {
+    source $defaultfile
+}
+
+pollfile