#!/usr/bin/wish # show your vessels on a map # This is part of ypp-sc-tools, a set of third-party tools for assisting # players of Yohoho Puzzle Pirates. # # Copyright (C) 2009 Ian Jackson # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # Yohoho and Puzzle Pirates are probably trademarks of Three Rings and # are used without permission. This program is not endorsed or # sponsored by Three Rings. source yarrglib.tcl source panner.tcl package require http #---------- general utilities ---------- set debug 0 proc debug {m} { global debug if {$debug} { puts "DEBUG $m" } } proc badusage {m} { puts stderr "where-vessels: bad usage: $m" exit 1 } proc glset {n val} { upvar #0 $n var set var $val } #---------- expecting certain errors ---------- proc errexpect-setline {lno line} { glset errexpect_lno $lno glset errexpect_line $line } proc errexpect-error {m} { global errexpect_line errexpect_lno error $m "$errexpect_line\n" [list YARRG-ERREXPECT $errexpect_lno] } proc errexpect-arrayget {arrayvar key} { upvar 1 $arrayvar av upvar 1 ${arrayvar}($key) v if {[info exists v]} { return $v } errexpect-error "undefined $key" } proc errexpect-arrayget-boolean {arrayvar key} { switch -exact [uplevel 1 [list errexpect-arrayget $arrayvar $key]] { true { return 1 } false { return 0 } default { errexpect-error "unexpected $key" } } } proc errexpect-catch {code} { global errorInfo errorCode set rc [catch { uplevel 1 $code } rv] debug "ERREXPECT CATCH |$rc|$rv|$errorCode|$errorInfo|" if {$rc==1 && ![string compare YARRG-ERREXPECT [lindex $errorCode 0]]} { return [list 1 $rv [lindex $errorCode 1] $errorInfo] } elseif {$rc==0} { return [list 0 $rv] } else { return -code $rc -errorinfo $errorInfo -errorcode $errorCode $rv } } #---------- argument parsing ---------- proc nextarg {} { global ai argv if {$ai >= [llength $argv]} { badusage "option [lindex $argv [expr {$ai-1}]] needs a value" } set v [lindex $argv $ai] incr ai return $v } set notes_loc vessel-notes set scraper {./yppedia-ocean-scraper --chart} proc parseargs {} { global ai argv global debug scraper set ai 0 while {[regexp {^\-} [set arg [lindex $argv $ai]]]} { incr ai switch -exact -- $arg { -- { break } --pirate { glset pirate [string totitle [nextarg]] } --ocean { glset ocean [string totitle [nextarg]] } --clipboard-file { load-clipboard-file [nextarg] } --local-html-dir { lappend scraper --local-html-dir=[nextarg] } --notes { glset notes_loc [nextarg] } --debug { incr debug } default { badusage "unknown option $arg" } } } set argv [lrange $argv $ai end] if {[llength $argv]} { badusage "non-option args not allowed" } } proc argdefaults {} { global ocean notes_loc pirate scraper if {![info exists ocean] || ![info exists pirate]} { 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 } lappend scraper $ocean } #---------- loading and parsing the vessel notes ---------- proc load-notes {} { global notes_loc notes_data if {[regexp {^\w+\:} $notes_loc]} { update debug "FETCHING NOTES $notes_loc" set req [::http::geturl $notes_loc] switch -glob [::http::status $req].[::http::ncode $req] { ok.200 { } ok.* { error "retrieving vessel-notes: [::http::code $req]" } * { error "Retrieving vessel-notes: [::http::error $req]" } } set newdata [::http::data $req] ::http::cleanup $req } else { debug "READING NOTES $notes_loc" set vn [open $notes_loc] set newdata [read $vn] close $vn } set notes_data $newdata } proc parse-notes {} { global notes_data notes catch { unset notes } set lno 0 foreach l [split $notes_data "\n"] { incr lno errexpect-setline $lno $l set l [string trim $l] if {![string length $l]} continue if {[regexp {^\#} $l]} continue if {![regexp -expanded \ {^ (\d+) (?: \s+([^=]*?) )? \s* = (?: \s* (\S+) (?: \s+ (\S+) )?)? $} \ $l dummy vid vname owner note]} { errexpect-error "badly formatted" } set vname [string trim $vname] if {[info exists notes($vid)]} { errexpect-error "duplicate vesselid $vid" } set notes($vid) [list $lno $vname $owner $note] } } proc note-info {lno vid name description} { global note_infos lappend note_infos [list $lno $vid $name $description] } proc display-note-infos {} { global note_infos note_missings notes set nmissing [llength $note_missings] debug "display-note-infos $nmissing [array size notes]" if {[llength $note_infos]} { set tiny "[llength $note_infos] warnings" } elseif {$nmissing && [array size notes]} { set tiny "$nmissing missing" } else { return } set infodata {} foreach info $note_infos { manyset $info lno vid name description append infodata "vessel" append infodata " $vid" if {[string length $name]} { append infodata " $name" } append infodata ": " $description "\n" } if {$nmissing} { append infodata "$nmissing vessels not mentioned in notes:\n" set last_island {} foreach info [lsort $note_missings] { manyset $info island name vid if {[string compare $island $last_island]} { append infodata "# $island:\n" set last_island $island } append infodata [format "%-9d %-29s =\n" $vid $name] } } parser-control-failed-core .ctrl.notes notes \ white blue 0 \ $tiny \ "[llength $note_infos] warnings;\ $nmissing vessels missing" \ "Full description of warnings and missing vessels:" \ $infodata } #---------- vessel properties ---------- proc vesselclasses-init {} { global vc_game2code vc_code2abbrev vc_abbrev2full foreach {game code abbrev full} { smsloop am sl Sloop lgsloop bm ct Cutter dhow cm dh Dhow longship dm ls Longship baghlah em bg Baghlah merchbrig fm mb {Merchant Brig} warbrig gm wb {War Brig} xebec hm xe Xebec warfrig im wf {War Frigate} merchgal jm mg {Merchant Galleon} grandfrig km gf {Grand Frigate} } { set vc_game2code($game) $code set vc_code2abbrev($code) $abbrev set vc_abbrev2full($abbrev) $full } } proc code2abbrev {code} { global vc_code2abbrev manyset [split $code _] inport class subclass lockown xabbrev manyset [split $lockown ""] lock own set abbrev {} append abbrev [lindex {? {}} $inport] append abbrev $vc_code2abbrev($class) append abbrev $subclass append abbrev [lindex {* + -} $lock] append abbrev [lindex {- = ?} [regsub {\D} $own 2]] append abbrev $xabbrev debug "CODE2ABBREV $code $abbrev" return $abbrev } #---------- loading and parsing the clipboard (vessel locations) ---------- proc vessel {vin} { global pirate notes_used note_missings newnotes upvar 1 $vin vi set codel {} lappend codel [errexpect-arrayget-boolean vi inPort] set gameclass [errexpect-arrayget vi vesselClass] upvar #0 vc_game2code($gameclass) class if {![info exists class]} { errexpect-error "unexpected vesselClass"} lappend codel $class set subclass [errexpect-arrayget vi vesselSubclass] switch -exact $subclass { null { lappend codel {} } icy { lappend codel F } default { lappend codel ($subclass) } } switch -exact [errexpect-arrayget vi isLocked]/[ \ errexpect-arrayget vi isBattleReady] { true/false { set lock 2 } false/false { set lock 1 } false/true { set lock 0 } default { errexpect-error "unexpected isLocked/isBattleReady" } } set vid [errexpect-arrayget vi vesselId] upvar #0 notes($vid) note set realname [errexpect-arrayget vi vesselName] set island [errexpect-arrayget vi islandName] set owner {} set xabbrev {} if {[info exists note]} { manyset $note lno notename owner xabbrev if {[string compare -nocase $realname $notename]} { note-info $lno $vid $realname $island \ "notes say name is $notename - perhaps renamed" } if {[string length $owner]} { if {![string compare $owner $pirate]} { set own 1 } else { set own 0 } } else { set own U } append abbrev $xabbrev set notes_used($vid) 1 } else { set own M lappend note_missings [list $island $realname $vid] } lappend codel "$lock$own" $xabbrev lappend newnotes [list $vid $realname $owner $xabbrev] set kk "$island [join $codel _]" upvar #0 count($kk) k if {![info exists k]} { set k 0 } incr k debug "CODED $kk $vid $realname" } set clipboard {} proc parse-clipboard {} { global clipboard count notes notes_used newnotes catch { unset count } catch { unset notes_used } glset note_infos {} glset note_missings {} set newnotes {} set itemre { (\w+) = ([^=]*) } set manyitemre "^\\\[ $itemre ( (?: ,\\ $itemre)* ) \\]\$" debug $manyitemre set lno 0 foreach l [split $clipboard "\n"] { incr lno errexpect-setline $lno $l if {![string length $l]} continue catch { unset vi } while 1 { if {![regexp -expanded $manyitemre $l dummy \ thiskey thisval rhs]} { errexpect-error "badly formatted" } set vi($thiskey) $thisval if {![string length $rhs]} break regsub {^, } $rhs {} rhs set l "\[$rhs\]" } vessel vi } if {[llength $newnotes]} { foreach vid [lsort [array names notes]] { if {![info exists notes_used($vid)]} { manyset $notes($vid) lno notename note-info $lno $vid $notename {} \ "vessel in notes no longer found" } } } } proc load-clipboard-file {fn} { set f [open $fn] glset clipboard [read $f] close $f } #---------- loading and parsing the chart ---------- proc load-chart {} { global chart scraper debug "FETCHING CHART" set chart [eval exec $scraper [list | perl -we { use strict; use CommodsScrape; use IO::File; use IO::Handle; yppedia_chart_parse(\*STDIN, (new IO::File ">/dev/null"), sub { sprintf "%d %d", @_; }, sub { printf "archlabel %d %d %s\n", @_; }, sub { printf "island %s %s\n", @_; }, sub { printf "league %s %s %s.\n", @_; }, sub { printf STDERR "warning: %s: incomprehensible: %s", @_; } ); STDOUT->error and die $!; }]] } set scale 16 proc coord {c} { global scale return [expr {$c * $scale}] } proc chart-got/archlabel {args} { } proc chart-got/island {x y args} { # debug "ISLE $x $y $args" global canvas isleloc set isleloc($args) [list $x $y] set sz 5 # $canvas create oval \ # [expr {[coord $x] - $sz}] [expr {[coord $y] - $sz}] \ # [expr {[coord $x] + $sz}] [expr {[coord $y] + $sz}] \ # -fill blue $canvas create text [coord $x] [coord $y] \ -text $args -anchor s } proc chart-got/league {x1 y1 x2 y2 kind} { # debug "LEAGUE $x1 $y1 $x2 $y2 $kind" global canvas set l [$canvas create line \ [coord $x1] [coord $y1] \ [coord $x2] [coord $y2]] if {![string compare $kind .]} { $canvas itemconfigure $l -dash . } } proc draw {} { global chart count isleloc canvas $canvas delete all foreach l [split $chart "\n"] { # debug "CHART-GOT $l" set proc [lindex $l 0] eval chart-got/$proc [lrange $l 1 end] } set lastislandname {} foreach key [lsort [array names count]] { set c $count($key) # debug "SHOWING $key $c" regexp {^(.*) (\S+)$} $key dummy islandname code set abbrev [code2abbrev $code] if {[string compare $lastislandname $islandname]} { manyset $isleloc($islandname) x y set x [coord $x] set y [coord $y] set lastislandname $islandname # debug "START Y $y" } set text $abbrev regsub -all {[0-9]} $text {} text if {$c > 1} { set text [format "%2d%s" $c $text] } else { set text [format " %s" $text] } set id [$canvas create text $x $y \ -anchor nw -font fixed \ -text $text] set bbox [$canvas bbox $id] set bid [eval $canvas create rectangle $bbox -fill white] $canvas lower $bid $id manyset $bbox dummy dummy dummy y # debug "NEW Y $y" } panner::updatecanvas-bbox .ctrl.pan } #---------- parser error reporting ---------- proc parser-control-create {w base invokebuttontext etl_title} { frame $w button $w.do -text $invokebuttontext -command invoke_$base frame $w.resframe -width 120 -height 32 button $w.resframe.res -text {} -anchor nw \ -padx 1 -pady 1 -borderwidth 0 -justify left glset deffont_$base [$w.resframe.res cget -font] place $w.resframe.res -relx 0.5 -y 0 -anchor n pack $w.do -side top 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" 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 -text "Error:" text $eb.emsg.text -height 1 pack $eb.emsg.text -side bottom pack $eb.emsg.lab -side left pack $eb.emsg -side top -pady 2 frame $eb.text -bd 2 -relief groove pack $eb.text -side bottom -pady 2 label $eb.text.lab text $eb.text.text -width 85 \ -xscrollcommand [list $eb.text.xscroll set] \ -yscrollcommand [list $eb.text.yscroll set] $eb.text.text tag configure error \ -background red -foreground white scrollbar $eb.text.xscroll -orient horizontal \ -command [list $eb.text.text xview] scrollbar $eb.text.yscroll -orient vertical \ -command [list $eb.text.text yview] grid configure $eb.text.lab -row 0 -column 0 -sticky w grid configure $eb.text.text -row 1 -column 0 grid configure $eb.text.yscroll -sticky ns -row 1 -column 1 grid configure $eb.text.xscroll -sticky ew -row 2 -column 0 } proc parser-control-ok-core {w base background show} { debug "parser-control-ok-core $w $base $background $show" upvar #0 deffont_$base deffont $w.resframe.res configure \ -background $background -disabledforeground black -font $deffont \ -state disabled -command {} \ -text $show } proc parser-control-ok {w base show} { parser-control-ok-core $w $base green $show } proc parser-control-none {w base show} { parser-control-ok-core $w $base blue $show } proc parser-control-failed-core {w base foreground background smallfont tiny summary fulldesc fulldata} { debug "parser-control-failed-core $w $base $summary $fulldesc" upvar #0 deffont_$base deffont set eb .err_$base $eb.emsg.text delete 0.0 end $eb.emsg.text insert end $summary $eb.text.lab configure -text $fulldesc $eb.text.text delete 0.0 end $eb.text.text insert end $fulldata regsub -all {.{18}} $tiny "&\n" ewrap if {$smallfont} { set font fixed } else { set font $deffont } $w.resframe.res configure \ -background $background -foreground $foreground -font $font \ -state normal -command [list wm deiconify $eb] \ -text $ewrap } proc parser-control-failed-expected {w base emsg lno ei fulldesc newdata} { set eb .err_$base set line [lindex [split $ei "\n"] 0] debug "parser-control-failed-expected: $w $base: $lno: $emsg\n $line" parser-control-failed-core $w $base \ white red 1 \ "err: [string trim $emsg]: \"$line\"" \ "at line $lno: $emsg" \ $fulldesc $newdata $eb.text.text tag add error $lno.0 $lno.end $eb.text.text see $lno.0 } proc parser-control-failed-unexpected {w base emsg ei} { global errorInfo parser-control-failed-core $w $base \ black yellow 1 \ $emsg $emsg "Details and stack trace:" $ei } proc reparse {base varname old fulldesc okshow noneshow parse ok} { upvar #0 $varname var manyset [errexpect-catch { uplevel 1 $parse if {[string length [string trim $var]]} { parser-control-ok .ctrl.$base $base $okshow } else { parser-control-none .ctrl.$base $base $noneshow } }] failed emsg lno ei if {$failed} { parser-control-failed-expected .ctrl.$base $base \ $emsg $lno $ei $fulldesc $var set var $old uplevel 1 $parse } else { uplevel 1 $ok } } #---------- main user interface ---------- proc widgets-setup {} { global canvas debug pirate ocean frame .f -border 1 -relief groove set canvas .f.c canvas $canvas pack $canvas -expand 1 -fill both pack .f -expand 1 -fill both -side left frame .ctrl pack .ctrl -side right debug "BBOX [$canvas bbox all]" panner::canvas-scroll-bbox .f.c panner::create .ctrl.pan .f.c 120 120 $debug pack .ctrl.pan -side top -pady 10 -padx 5 frame .ctrl.zoom pack .ctrl.zoom -side top button .ctrl.zoom.out -text - -font {Courier 16} -command {zoom /2} button .ctrl.zoom.in -text + -font {Courier 16} -command {zoom *2} pack .ctrl.zoom.out .ctrl.zoom.in -side left parser-control-create .ctrl.acquire \ acquire Acquire \ "Clipboard parsing error" \ pack .ctrl.acquire -side top -pady 2 parser-control-create .ctrl.notes \ notes "Reload notes" \ "Vessel notes loading report" \ pack .ctrl.notes -side top -pady 2 wm geometry . 1024x480 wm title . "where-vessels - $pirate on the $ocean ocean" } proc zoom {extail} { global scale canvas set nscale [expr "\$scale $extail"] debug "ZOOM $scale $nscale" if {$nscale < 1 || $nscale > 200} return set scale $nscale draw } proc invoke_acquire {} { global clipboard errorInfo set old $clipboard if {[catch { set clipboard [clipboard get] } emsg]} { parser-control-failed-unexpected .ctrl.acquire acquire \ $emsg "fetching clipboard:\n\n$errorInfo" return } reparse acquire \ clipboard $old "Clipboard contents:" { acquired ok } { no vessels } { parse-clipboard } { display-note-infos } draw } proc invoke_notes {} { global notes_data errorInfo notes_loc set old $notes_data if {[catch { load-notes } emsg]} { parser-control-failed-unexpected .ctrl.notes notes \ $emsg "loading $notes_loc:\n\n$errorInfo" return } reparse notes \ notes_data $old "Vessel notes:" "loaded ok" { no notes } { parse-notes parse-clipboard } { display-note-infos } draw } #---------- main program ---------- vesselclasses-init parseargs argdefaults httpclientsetup where-vessels load-chart widgets-setup set notes_data {} if {[catch { parse-clipboard } emsg]} { puts stderr "$emsg\n$errorInfo" exit 1 } after idle invoke_notes draw