chiark / gitweb /
955add9bda46b56e20dcba84a7bcd3f637be08c9
[ypp-sc-tools.main.git] / yarrg / pages.c
1 /*
2  * Interaction with the YPP client via X11
3  */
4 /*
5  *  This is part of ypp-sc-tools, a set of third-party tools for assisting
6  *  players of Yohoho Puzzle Pirates.
7  * 
8  *  Copyright (C) 2009 Ian Jackson <ijackson@chiark.greenend.org.uk>
9  * 
10  *  This program is free software: you can redistribute it and/or modify
11  *  it under the terms of the GNU General Public License as published by
12  *  the Free Software Foundation, either version 3 of the License, or
13  *  (at your option) any later version.
14  * 
15  *  This program is distributed in the hope that it will be useful,
16  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
17  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  *  GNU General Public License for more details.
19  * 
20  *  You should have received a copy of the GNU General Public License
21  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
22  * 
23  *  Yohoho and Puzzle Pirates are probably trademarks of Three Rings and
24  *  are used without permission.  This program is not endorsed or
25  *  sponsored by Three Rings.
26  */
27
28 /*
29  * Only this file #includes the X11 headers, as they're quite
30  * pollutant of the namespace.
31  */
32
33 #include "structure.h"
34
35 #include <X11/Xlib.h>
36 #include <X11/extensions/XTest.h>
37 #include <X11/keysym.h>
38 #include <X11/Xutil.h>
39
40 #include <X11/extensions/XShm.h>
41 #include <sys/ipc.h>
42 #include <sys/shm.h>
43
44 const char *ocean, *pirate;
45
46 static XWindowAttributes attr;
47 static Window id;
48 static Display *disp;
49 static struct timeval tv_startup;
50 static unsigned wwidth, wheight;
51 static int max_relevant_y= -1;
52 static Point commod_focus_point, commod_page_point, commod_focuslast_point;
53
54 static XImage *shmim;
55 static XShmSegmentInfo shminfo;
56
57 DEBUG_DEFINE_DEBUGF(pages)
58
59 #define xassert(what)                                   \
60   ((what) ? (void)0 :                                   \
61    fatal("X11 operation unexpectedly failed."           \
62          " %s:%d: %s\n", __FILE__,__LINE__,#what))
63
64
65 /*---------- keyboard map ----------*/
66
67 #define KEYS(EACH_KEY)                          \
68   EACH_KEY(XK_slash)                            \
69   EACH_KEY(XK_w)                                \
70   EACH_KEY(XK_Return)                           \
71   EACH_KEY(XK_Prior)                            \
72   EACH_KEY(XK_Next)
73
74 #define MAXMAPCOL 6
75 typedef struct {
76   int len;
77   int kc[3];
78 } MappedKey;
79
80 #define KEY_MAP_DEF(xk) \
81 static MappedKey mk_##xk;
82 KEYS(KEY_MAP_DEF)
83
84 typedef struct {
85   int isdef; /* 'y' if defined; otherwise char indicating why not */
86   int kc;
87 } MappedModifier;
88
89 static int kc_min, kc_max;
90 static MappedModifier mm_shift, mm_modeswitch, mm_isol3shift;
91 static KeySym *mapping;
92 static int mapwidth;
93
94 static void keymap_lookup_modifier(MappedModifier *mm, KeySym sym,
95                                    char ifnot) {
96   int kc;
97
98   mm->isdef= ifnot;
99   for (kc=kc_min; kc <= kc_max; kc++) {
100     if (mapping[(kc-kc_min)*mapwidth] == sym) {
101       mm->isdef= 'y';
102       mm->kc= kc;
103       return;
104     }
105   }
106 }
107
108 static void keymap_lookup_key(MappedKey *mk, KeySym sym, const char *what) {
109   const MappedModifier *shift, *xshift;
110   int col, kc;
111   char cols[MAXMAPCOL+1];
112
113   memset(cols,'.',MAXMAPCOL);
114   cols[MAXMAPCOL]= 0;
115
116   for (col=0; col<mapwidth && col<MAXMAPCOL; col++) {
117
118 #define CHECK_SHIFT(sh) do{                     \
119       if ((sh) && (sh)->isdef!='y') {           \
120         cols[col]= (sh)->isdef;                 \
121         continue;                               \
122       }                                         \
123     }while(0)
124
125     shift= col & 1 ? &mm_shift : 0;
126     CHECK_SHIFT(shift);
127
128     xshift= col >= 4 ? &mm_isol3shift :
129             col >= 2 ? &mm_modeswitch :
130             0;
131     CHECK_SHIFT(xshift);
132
133     for (kc=kc_min; kc <= kc_max; kc++) {
134       if (mapping[(kc-kc_min)*mapwidth + col] == sym)
135         goto found;
136     }
137     cols[col]= '_';
138   }
139   fprintf(stderr,"\n"
140           "Unable to find a key to press to generate %s.\n"
141           "(Technical details: cols:%s sh:%c:%d modesw:%c:%d isol3:%c:%d)\n",
142           what, cols,
143 #define SH_DETAILS(sh) mm_##sh.isdef, mm_##sh.kc
144           SH_DETAILS(shift), SH_DETAILS(modeswitch), SH_DETAILS(isol3shift));
145   fatal("Keymap is unsuitable!");
146
147  found:;
148   int *fill= mk->kc;
149   
150   if (xshift) *fill++ = xshift->kc;
151   if (shift)  *fill++ = shift->kc;
152   *fill += kc;
153   mk->len= fill - mk->kc;
154 }
155
156 static void keymap_startup(void) {
157   xassert( XDisplayKeycodes(disp,&kc_min,&kc_max) );
158   xassert( mapping= XGetKeyboardMapping(disp, kc_min, kc_max-kc_min+1,
159                                         &mapwidth) );
160
161   XModifierKeymap *modmap;
162   xassert( modmap= XGetModifierMapping(disp) );
163
164   /* find a shift keycode */
165   mm_shift.isdef= 'x';
166   int modent;
167   for (modent=0; modent<modmap->max_keypermod; modent++) {
168     KeySym shiftsym= modmap->modifiermap[modent];
169     if (shiftsym==NoSymbol) continue;
170     keymap_lookup_modifier(&mm_shift, shiftsym, 's');
171     if (mm_shift.isdef!='y') break;
172   }
173
174   /* find keycodes for mode_switch (column+=2) and ISO L3 shift (column+=4) */
175   keymap_lookup_modifier(&mm_modeswitch, XK_Mode_switch,      0);
176   keymap_lookup_modifier(&mm_isol3shift, XK_ISO_Level3_Shift, 0);
177   
178   XFreeModifiermap(modmap);
179
180 #define KEY_MAP_LOOKUP(xk) \
181   keymap_lookup_key(&mk_##xk, xk, #xk);
182   KEYS(KEY_MAP_LOOKUP)
183
184   XFree(mapping);
185   mapping= 0;
186 }
187
188 #define SEND_KEY(xk) (send_key(&mk_##xk))
189
190 /*---------- pager ----------*/
191
192 typedef RgbImage Snapshot;
193
194 static double last_input;
195 static const double min_update_allowance= 0.25;
196
197 static double timestamp(void) {
198   struct timeval tv;
199   
200   sysassert(! gettimeofday(&tv,0) );
201   double t= (tv.tv_sec - tv_startup.tv_sec) +
202             (tv.tv_usec - tv_startup.tv_usec) * 1e-6;
203   debugf("PAGING %f\n",t);
204   return t;
205 }
206 static void delay(double need_sleep) {
207   debugf("PAGING     delay %f\n",need_sleep);
208   sysassert(! usleep(need_sleep * 1e6) );
209 }
210
211 static void sync_after_input(void) {
212   xassert( XSync(disp, False) );
213   last_input= timestamp();
214 }
215
216 static void translate_coords_toroot(int wx, int wy, int *rx, int *ry) {
217   Window dummy;
218   xassert( XTranslateCoordinates(disp, id,attr.root, wx,wy, rx,ry, &dummy) );
219 }
220 static void translate_coords_toroot_p(Point w, int *rx, int *ry) {
221   translate_coords_toroot(w.x, w.y, rx, ry);
222 }
223
224 static void check_client_window_all_on_screen(void) {
225   Rect onroot;
226   unsigned rwidth, rheight;
227   Window dummy;
228   unsigned bd, depth;
229   int rxpos, rypos;
230
231   xassert( XGetGeometry(disp,attr.root, &dummy, &rxpos,&rypos,
232                         &rwidth, &rheight,
233                         &bd,&depth) );
234   
235   translate_coords_toroot(0,0, &onroot.tl.x,&onroot.tl.y);
236   translate_coords_toroot(wwidth-1,wheight-1, &onroot.br.x,&onroot.br.y);
237   if (!(onroot.tl.x >= 0 &&
238         onroot.tl.y >= 0 &&
239         onroot.br.x < rwidth &&
240         onroot.br.y < rheight))
241     fatal("YPP client window is not entirely on the screen.");
242 }
243
244 static void check_not_disturbed(void) {
245   XEvent ev;
246   int r;
247   
248   for (;;) {
249     r= XCheckMaskEvent(disp, ~0, &ev);
250     if (r==False) return;
251
252     switch (ev.type) {
253     case VisibilityNotify:
254       if (ev.xvisibility.state != VisibilityUnobscured)
255         fatal("YPP client window has become obscured.");
256       break;
257     case ConfigureNotify:
258       check_client_window_all_on_screen();
259       break;
260     case FocusOut:
261       fatal("Focus left YPP client window.");
262       break;
263     case FocusIn:
264       warning("focus entered YPP client window ?!");
265       break;
266     default:
267       fatal("Received unexpected X11 event (type code %d)!", ev.type);
268     }
269   }
270 }      
271
272 static void check_pointer_not_disturbed(void) {
273   Window got_root, got_child;
274   int got_root_x, got_root_y;
275   int got_win_x, got_win_y;
276   unsigned got_mask;
277
278   int r= XQueryPointer(disp,id, &got_root,&got_child,
279                        &got_root_x, &got_root_y,
280                        &got_win_x, &got_win_y,
281                        &got_mask);
282   if (!r ||
283       got_win_x!=commod_page_point.x ||
284       got_win_y!=commod_page_point.y) {
285     progress("");
286     fprintf(stderr,"\nunexpected mouse position:"
287             " samescreen=%d got=%dx%d want=%dx%d",
288             r, got_win_x,got_win_y,
289             commod_page_point.x,commod_page_point.y);
290     fatal("Mouse pointer moved.");
291   }
292 }
293
294 static void send_key(MappedKey *mk) {
295   int i;
296   check_not_disturbed();
297   for (i=0; i<mk->len; i++) XTestFakeKeyEvent(disp, mk->kc[i], 1, 0);
298   for (i=mk->len; --i>=0; ) XTestFakeKeyEvent(disp, mk->kc[i], 0, 0);
299 }
300 static void send_mouse_1_updown_here(void) {
301   check_not_disturbed();
302   XTestFakeButtonEvent(disp,1,1, 0);
303   XTestFakeButtonEvent(disp,1,0, 0);
304 }
305 static void send_mouse_1_updown(int x, int y) {
306   check_not_disturbed();
307   int screen= XScreenNumberOfScreen(attr.screen);
308   int xpos, ypos;
309   translate_coords_toroot(x,y, &xpos,&ypos);
310   XTestFakeMotionEvent(disp, screen, xpos,ypos, 0);
311   send_mouse_1_updown_here();
312 }
313 static void pgdown_by_mouse(void) {
314   check_not_disturbed();
315   check_pointer_not_disturbed();
316   debugf("PAGING   Mouse\n");
317   send_mouse_1_updown_here();
318   sync_after_input();
319 }
320
321 static int pgupdown;
322
323 static void send_pgup_many(void) {
324   int i;
325   for (i=0; i<25; i++) {
326     SEND_KEY(XK_Prior);
327     pgupdown--;
328   }
329   debugf("PAGING   PageUp x %d\n",i);
330   sync_after_input();
331 }
332 static void send_pgdown_torestore(void) {
333   debugf("PAGING   PageDown x %d\n", -pgupdown);
334   while (pgupdown < 0) {
335     SEND_KEY(XK_Next);
336     pgupdown++;
337   }
338   sync_after_input();
339 }
340
341 static void free_snapshot(Snapshot **io) {
342   free(*io);
343   *io= 0;
344 }
345
346 #define SAMPLEMASK 0xfful
347
348 typedef struct {
349   int lshift, rshift;
350 } ShMask;
351
352 static void compute_shift_mask(ShMask *sm, unsigned long ximage_mask) {
353   sm->lshift= 0;
354   sm->rshift= 0;
355   
356   for (;;) {
357     if (ximage_mask <= (SAMPLEMASK>>1)) {
358       sm->lshift++;  ximage_mask <<= 1;
359     } else if (ximage_mask > SAMPLEMASK) {
360       sm->rshift++;  ximage_mask >>= 1;
361     } else {
362       break;
363     }
364     assert(!(sm->lshift && sm->rshift));
365   }
366   assert(sm->lshift < LONG_BIT);
367   assert(sm->rshift < LONG_BIT);
368   debugf("SHIFTMASK %p={.lshift=%d, .rshift=%d} image_mask=%lx\n",
369          sm, sm->lshift, sm->rshift, ximage_mask);
370 }
371
372 static void rtimestamp(double *t, const char *wh) {
373   double n= timestamp();
374   debugf("PAGING                INTERVAL %f  %s\n", n-*t, wh);
375   *t= n;
376 }
377
378 static void snapshot(Snapshot **output) {
379   XImage *im_use, *im_free=0;
380
381   ShMask shiftmasks[3];
382
383   debugf("PAGING   snapshot\n");
384
385   double begin= timestamp();
386   if (shmim) {
387     rtimestamp(&begin, "XShmGetImage before");
388     xassert( XShmGetImage(disp,id,shmim, 0,0, AllPlanes) );
389     rtimestamp(&begin, "XShmGetImage");
390
391     size_t dsz= shmim->bytes_per_line * shmim->height;
392     im_use= im_free= mmalloc(sizeof(*im_use) + dsz);
393     *im_free= *shmim;
394     im_free->data= (void*)(im_free+1);
395     memcpy(im_free->data, shmim->data, dsz);
396     rtimestamp(&begin, "mmalloc/memcpy");
397   } else {
398     rtimestamp(&begin, "XGetImage before");
399     xassert( im_use= im_free=
400              XGetImage(disp,id, 0,0, wwidth,wheight, AllPlanes, ZPixmap) );
401     rtimestamp(&begin, "XGetImage");
402   }
403
404 #define COMPUTE_SHIFT_MASK(ix, rgb) \
405   compute_shift_mask(&shiftmasks[ix], im_use->rgb##_mask)
406   COMPUTE_SHIFT_MASK(0, blue);
407   COMPUTE_SHIFT_MASK(1, green);
408   COMPUTE_SHIFT_MASK(2, red);
409   
410   if (!*output)
411     *output= alloc_rgb_image(wwidth, wheight);
412
413   rtimestamp(&begin, "compute_shift_masks+alloc_rgb_image");
414
415   int x,y,i;
416   uint32_t *op32= (*output)->data;
417   for (y=0; y<wheight; y++) {
418     if (im_use->xoffset == 0 &&
419         im_use->format == ZPixmap &&
420         im_use->byte_order == LSBFirst &&
421         im_use->depth == 24 &&
422         im_use->bits_per_pixel == 32 &&
423         im_use->red_mask   == 0x0000ffU &&
424         im_use->green_mask == 0x00ff00U &&
425         im_use->blue_mask  == 0xff0000U) {
426       const char *p= im_use->data + y * im_use->bytes_per_line;
427 //      debugf("optimised copy y=%d",y);
428       memcpy(op32, p, wwidth*sizeof(*op32));
429       op32 += wwidth;
430     } else {
431       for (x=0; x<wwidth; x++) {
432         long xrgb= XGetPixel(im_use,x,y);
433         Rgb sample= 0;
434         for (i=0; i<3; i++) {
435           sample <<= 8;
436           sample |=
437             ((xrgb << shiftmasks[i].lshift) >> shiftmasks[i].rshift)
438             & SAMPLEMASK;
439         }
440         *op32++= sample;
441       }
442     }
443   }
444
445   rtimestamp(&begin,"w*h*XGetPixel");
446   if (im_free)
447     XDestroyImage(im_free);
448   
449   rtimestamp(&begin,"XDestroyImage");
450   check_not_disturbed();
451
452   debugf("PAGING   snapshot done.\n");
453 }
454
455 static int identical(const Snapshot *a, const Snapshot *b) {
456   if (!(a->w == b->w &&
457         a->h == b->h))
458     return 0;
459
460   int compare_to= a->h;
461   if (max_relevant_y>=0 && compare_to > max_relevant_y)
462     compare_to= max_relevant_y;
463   
464   return !memcmp(a->data, b->data, a->w * 3 * compare_to);
465 }
466
467 static void wait_for_stability(Snapshot **output,
468                                const Snapshot *previously,
469                                void (*with_keypress)(void),
470                                const char *fmt, ...)
471      FMT(4,5);
472
473 static void wait_for_stability(Snapshot **output,
474                                const Snapshot *previously,
475                                void (*with_keypress)(void),
476                                const char *fmt, ...) {
477   va_list al;
478   va_start(al,fmt);
479
480   Snapshot *last=0;
481   int nidentical=0;
482   /* waits longer if we're going to return an image identical to previously
483    * if previously==0, all images are considered identical to it */
484
485   char *doing;
486   sysassert( vasprintf(&doing,fmt,al) >=0 );
487
488   debugf("PAGING  wait_for_stability"
489           "  last_input=%f previously=%p `%s'\n",
490           last_input, previously, doing);
491
492   double max_interval= 1.000;
493   double min_interval= 0.100;
494   for (;;) {
495     progress_spinner("%s",doing);
496     
497     double since_last_input= timestamp() - last_input;
498     double this_interval= min_interval - since_last_input;
499     if (this_interval > max_interval) this_interval= max_interval;
500
501     if (this_interval >= 0)
502       delay(this_interval);
503
504     snapshot(output);
505
506     if (!last) {
507       debugf("PAGING  wait_for_stability first...\n");
508       last=*output; *output=0;
509     } else if (!identical(*output,last)) {
510       debugf("PAGING  wait_for_stability changed...\n");
511       free_snapshot(&last); last=*output; *output=0;
512       nidentical=0;
513       if (!with_keypress) {
514         min_interval *= 3.0;
515         min_interval += 0.1;
516       }
517     } else {
518       nidentical++;
519       int threshold=
520         !previously ? 3 :
521         identical(*output,previously) ? 5
522         : 1;
523       debugf("PAGING  wait_for_stability nidentical=%d threshold=%d\n",
524               nidentical, threshold);
525       if (nidentical >= threshold)
526         break;
527
528       min_interval += 0.1;
529       min_interval *= 2.0;
530     }
531
532     if (with_keypress)
533       with_keypress();
534   }
535
536   free_snapshot(&last);
537   free(doing);
538   debugf("PAGING  wait_for_stability done.\n");
539   va_end(al);
540 }
541
542 static void raise_and_get_details(void) {
543   int evbase,errbase,majver,minver;
544   int wxpos, wypos;
545   unsigned bd,depth;
546
547   progress("raising and checking YPP client window...");
548
549   debugf("PAGING raise_and_get_details\n");
550
551   int xtest= XTestQueryExtension(disp, &evbase,&errbase,&majver,&minver);
552   if (!xtest) fatal("X server does not support the XTEST extension.");
553
554   xassert( XRaiseWindow(disp, id) );
555   /* in case VisibilityNotify triggers right away before we have had a
556    * change to raise; to avoid falsely detecting lowering in that case */
557   
558   xassert( XSelectInput(disp, id,
559                         StructureNotifyMask|
560                         VisibilityChangeMask
561                         ) );
562
563   xassert( XRaiseWindow(disp, id) );
564   /* in case the window was lowered between our Raise and our SelectInput;
565    * to avoid failing to detect that lowering */
566
567   xassert( XGetWindowAttributes(disp, id, &attr) );
568   xassert( XGetGeometry(disp,id, &attr.root,
569                         &wxpos,&wypos, &wwidth,&wheight,
570                         &bd,&depth) );
571
572   if (!(wwidth >= 320 && wheight >= 320))
573     fatal("YPP client window is implausibly small?");
574
575   if (attr.depth < 24)
576     fatal("Display is not 24bpp.");
577
578   check_client_window_all_on_screen();
579
580   Bool shmpixmaps=0;
581   int major=0,minor=0;
582   int shm= XShmQueryVersion(disp, &major,&minor,&shmpixmaps);
583   debugf("PAGING shm=%d %d.%d pixmaps=%d\n",shm,major,minor,shmpixmaps);
584   if (shm) {
585     xassert( shmim= XShmCreateImage(disp, attr.visual, attr.depth, ZPixmap,
586                                     0,&shminfo, wwidth,wheight) );
587
588     sigset_t oldset, all;
589     sigfillset(&all);
590     sysassert(! sigprocmask(SIG_BLOCK,&all,&oldset) );
591
592     int pfd[2];
593     pid_t cleaner;
594     sysassert(! pipe(pfd) );
595     sysassert( (cleaner= fork()) != -1 );
596     if (!cleaner) {
597       sysassert(! close(pfd[1]) );
598       for (;;) {
599         int r= read(pfd[0], &shminfo.shmid, sizeof(shminfo.shmid));
600         if (!r) exit(0);
601         if (r==sizeof(shminfo.shmid)) break;
602         assert(r==-1 && errno==EINTR);
603       }
604       for (;;) {
605         char bc;
606         int r= read(pfd[0],&bc,1);
607         if (r>=0) break;
608         assert(r==-1 && errno==EINTR);
609       }
610       sysassert(! shmctl(shminfo.shmid,IPC_RMID,0) );
611       exit(0);
612     }
613     sysassert(! close(pfd[0]) );
614
615     sysassert(! sigprocmask(SIG_SETMASK,&oldset,0) );
616
617     assert(shmim->height == wheight);
618     sysassert( (shminfo.shmid=
619                 shmget(IPC_PRIVATE, shmim->bytes_per_line * wheight,
620                        IPC_CREAT|0600)) >= 0 );
621
622     sysassert( write(pfd[1],&shminfo.shmid,sizeof(shminfo.shmid)) ==
623                sizeof(shminfo.shmid) );
624     sysassert( shminfo.shmaddr= shmat(shminfo.shmid,0,0) );
625     shmim->data= shminfo.shmaddr;
626     shminfo.readOnly= False;
627     xassert( XShmAttach(disp,&shminfo) );
628
629     close(pfd[1]); /* causes IPC_RMID */
630   }
631 }
632
633 static void set_focus_commodity(void) {
634   int screen= XScreenNumberOfScreen(attr.screen);
635
636   progress("taking control of YPP client window...");
637
638   debugf("PAGING set_focus\n");
639
640   send_mouse_1_updown(commod_focus_point.x, commod_focus_point.y);
641   sync_after_input();
642
643   delay(0.5);
644   xassert( XSelectInput(disp, id,
645                         StructureNotifyMask|
646                         VisibilityChangeMask|
647                         FocusChangeMask
648                         ) );
649
650   int xpos,ypos;
651   translate_coords_toroot_p(commod_page_point, &xpos,&ypos);
652   XTestFakeMotionEvent(disp, screen, xpos,ypos, 0);
653
654   sync_after_input();
655
656   debugf("PAGING raise_and_set_focus done.\n");
657 }
658
659 static CanonImage *convert_page(const Snapshot *sn, RgbImage **rgb_r) {
660   CanonImage *im;
661   RgbImage *ri;
662
663   fwrite_ppmraw(screenshot_file, sn);
664
665   const Rgb *pixel= sn->data;
666   CANONICALISE_IMAGE(im, sn->w, sn->h, ri, {
667     rgb= *pixel++;
668   });
669
670   sysassert(!ferror(screenshot_file));
671   sysassert(!fflush(screenshot_file));
672
673   if (rgb_r) *rgb_r= ri;
674   else free(ri);
675
676   return im;
677 }
678
679 static void prepare_ypp_client(void) {
680   CanonImage *test;
681   Snapshot *current=0;
682   
683   /* find the window and check it's on the right kind of screen */
684   raise_and_get_details();
685   wait_for_stability(&current,0,0, "checking current YPP client screen...");
686
687   test= convert_page(current,0);
688   find_structure(test,0, &max_relevant_y,
689                  &commod_focus_point,
690                  &commod_page_point,
691                  &commod_focuslast_point);
692   check_correct_commodities();
693   Rect sunshine= find_sunshine_widget();
694
695   progress("poking client...");
696   send_mouse_1_updown((sunshine.tl.x   + sunshine.br.x) / 2,
697                       (sunshine.tl.y*9 + sunshine.br.y) / 10);
698   sync_after_input();
699
700   free(test);
701
702   wait_for_stability(&current,0,0, "checking basic YPP client screen...");
703   send_mouse_1_updown(250, wheight-10);
704   send_mouse_1_updown_here();
705   send_mouse_1_updown_here();
706   sync_after_input();
707   check_not_disturbed();
708   SEND_KEY(XK_slash);
709   SEND_KEY(XK_w);
710   SEND_KEY(XK_Return);
711   sync_after_input();
712
713   Snapshot *status=0;
714   wait_for_stability(&status,current,0, "awaiting status information...");
715   free_snapshot(&current);
716   free_snapshot(&status);
717 }
718
719 static void convert_store_page(Snapshot *current) {
720   RgbImage *rgb;
721   CanonImage *ci;
722   PageStruct *pstruct;
723   
724   progress("page %d prescanning   ...",npages);
725   ci= convert_page(current,&rgb);
726
727   progress("page %d overview      ...",npages);
728   find_structure(ci,&pstruct, 0,0,0,0);
729
730   store_current_page(ci,pstruct,rgb);
731 }
732
733 void take_screenshots(void) {
734   Snapshot *current=0, *last=0;
735
736   prepare_ypp_client();
737   
738   /* page to the top - keep pressing page up until the image stops changing */
739   set_focus_commodity();
740   wait_for_stability(&current,0, send_pgup_many,
741                      "paging up to top of commodity list...");
742
743   /* now to actually page down */
744   for (;;) {
745     debugf("page %d paging\n",npages);
746
747     pgdown_by_mouse();
748
749     if (!(npages < MAX_PAGES))
750       fatal("Paging down seems to generate too many pages - max is %d.",
751             MAX_PAGES);
752
753     convert_store_page(current);
754     free_snapshot(&last); last=current; current=0;
755     debugf("PAGING page %d converted\n",npages);
756     npages++;
757
758     wait_for_stability(&current,last, 0,
759                        "page %d collecting    ...",
760                        npages);
761     if (identical(current,last)) {
762       free_snapshot(&current);
763       break;
764     }
765   }
766   progress("finishing with the YPP client...");
767   send_mouse_1_updown(commod_focuslast_point.x, commod_focuslast_point.y);
768   sync_after_input();
769   send_pgdown_torestore();
770   sync_after_input();
771
772   debugf("PAGING all done.\n");
773   progress_log("collected %d screenshots.",npages);
774   check_pager_motion(0,npages);
775 }    
776
777 void take_one_screenshot(void) {
778   Snapshot *current=0;
779
780   prepare_ypp_client();
781   wait_for_stability(&current,0,0, "taking screenshot...");
782   convert_store_page(current);
783   npages= 1;
784   progress_log("collected single screenshot.");
785 }
786
787 void set_yppclient_window(unsigned long wul) {
788   id= wul;
789 }
790
791 DEBUG_DEFINE_SOME_DEBUGF(findypp,debugfind)
792
793 static int nfound;
794 static Atom wm_name;
795 static int screen;
796
797 static void findypp_recurse(int depth, int targetdepth, Window w) {
798   unsigned int nchildren;
799   int i;
800   Window *children=0;
801   Window gotroot, gotparent;
802
803   static const char prefix[]= "Puzzle Pirates - ";
804   static const char onthe[]= " on the ";
805   static const char suffix[]= " ocean";
806 #define S(x) ((int)sizeof((x))-1)
807
808   debugfind("FINDYPP %d/%d screen %d  %*s %lx",
809             depth,targetdepth,screen,
810             depth,"",(unsigned long)w);
811   
812   if (depth!=targetdepth) {
813     xassert( XQueryTree(disp,w,
814                         &gotroot,&gotparent,
815                         &children,&nchildren) );
816     debugfind(" nchildren=%d\n",nchildren);
817   
818     for (i=0; i<nchildren; i++) {
819       Window child= children[i];
820       findypp_recurse(depth+1, targetdepth, child);
821     }
822     XFree(children);
823     return;
824   }
825     
826
827   int gotfmt;
828   Atom gottype;
829   unsigned long len, gotbytesafter;
830   char *title;
831   unsigned char *gottitle=0;
832   xassert( !XGetWindowProperty(disp,w, wm_name,0,512, False,
833                                AnyPropertyType,&gottype, &gotfmt, &len,
834                                &gotbytesafter, &gottitle) );
835   title= (char*)gottitle;
836
837   if (DEBUGP(findypp)) {
838     debugfind(" gf=%d len=%lu gba=%lu \"", gotfmt,len,gotbytesafter);
839     char *p;
840     for (p=title; p < title+len; p++) {
841       char c= *p;
842       if (c>=' ' && c<=126) fputc(c,debug);
843       else fprintf(debug,"\\x%02x",c & 0xff);
844     }
845     fputs("\": ",debug);
846   }
847
848 #define REQUIRE(pred)                                                   \
849   if (!(pred)) { debugfind(" failed test  %s\n", #pred); return; }      \
850   else
851
852   REQUIRE( gottype!=None );
853   REQUIRE( len );
854   REQUIRE( gotfmt==8 );
855
856   REQUIRE( len >= S(prefix) + 1 + S(onthe) + 1 + S(suffix) );
857
858   char *spc1= strchr(  title        + S(prefix), ' ');  REQUIRE(spc1);
859   char *spc2= strrchr((title + len) - S(suffix), ' ');  REQUIRE(spc2);
860
861   REQUIRE( (title + len) - spc1  >= S(onthe)  + S(suffix) );
862   REQUIRE(  spc2         - title >= S(prefix) + S(onthe) );
863
864   REQUIRE( !memcmp(title,                   prefix, S(prefix)) );
865   REQUIRE( !memcmp(title + len - S(suffix), suffix, S(suffix))  );
866   REQUIRE( !memcmp(spc1,                    onthe,  S(onthe))  );
867
868 #define ASSIGN(what, start, end)                        \
869   what= masprintf("%.*s", (int)((end)-(start)), start); \
870   if (o_##what) REQUIRE( !strcasecmp(o_##what, what) ); \
871   else
872
873   ASSIGN(ocean,  spc1 + S(onthe),   (title + len) - S(suffix));
874   ASSIGN(pirate, title + S(prefix),  spc1);
875
876   debugfind(" YES!\n");
877   id= w;
878   nfound++;
879   progress_log("found YPP client (0x%lx):"
880                " %s ocean - %s.",
881                (unsigned long)id, ocean, pirate);
882 }
883
884 void find_yppclient_window(void) {
885   int targetdepth;
886
887   nfound=0;
888   
889   if (id) return;
890   
891   progress("looking for YPP client window...");
892
893   xassert( (wm_name= XInternAtom(disp,"WM_NAME",True)) != None);
894
895   for (targetdepth=1; targetdepth<4; targetdepth++) {
896     for (screen=0; screen<ScreenCount(disp); screen++) {
897       debugfind("FINDYPP screen %d\n", screen);
898       findypp_recurse(0,targetdepth, RootWindow(disp,screen));
899     }
900     if (nfound) break;
901   }
902
903   if (nfound>1)
904     fatal("Found several possible YPP clients.   Close one,\n"
905           " disambiguate with --pirate or --ocean,"
906           " or specify --window-id.\n");
907   if (nfound<1)
908     fatal("Did not find %sYPP client."
909           " Use --window-id and/or report this as a fault.\n",
910           o_ocean || o_pirate ? "matching ": "");
911 }
912
913 void screenshot_startup(void) {
914   progress("starting...");
915   disp= XOpenDisplay(0);
916   if (!disp) fatal("Unable to open X11 display.");
917   sysassert(! gettimeofday(&tv_startup,0) );
918
919   keymap_startup();
920 }