[comp.sources.amiga] Journal and Playback

dpvc@ur-tut.UUCP (Davide P. Cervone) (07/10/87)

    Here is the documentation to a neat utility that allows you to
record and playback events that happen in a window.  Binaries and
sources available in respective groups!
    -Doc


#	This is a shell archive.
#	Remove everything above and including the cut line.
#	Then run the rest of the file through sh.
#----cut here-----cut here-----cut here-----cut here----#
#!/bin/sh
# shar:	Shell Archiver
#	Run the following text with /bin/sh to create:
#	README
#	journal.doc
# This archive created: Fri Jul 10 13:53:16 1987
# By:	Craig Norborg (Purdue University Computing Center)
cat << \SHAR_EOF > README
Well, when I was working on MonIDCMP, I wanted to provide a real-life example
of how it could be used for something useful (like HARDCOPY was to MONPROC).
I thought, "Wouldn't it be great to be able to record and play back the events
that went on in a window?"  You could make "Guided Tour" demonstrations, or
turnkey systems that perform all kinds of mouse and keyboard activity, or
you could record the events that demonstrate a bug that you have been able to
produce and send the recorded events to Commodore as documentation of the bug.

But MonIDCMP doesn't have the power to accomplish this:  it can trick a
window into thinking that things have happened, but gadgets would not REALLY be
pressed, and windows would not REALY be resized.  To do it right you have to go
directly to the Input Device and look at the events that are being passed to
Intuition.

I set out to do this.  What I came up with was JOURNAL and PLAYBACK.  To see
what they do and what they are good for, read the following document, which
describes in detail how to use JOURNAL and PLAYBACK.  

With a little more work, these could become important documentation tools for
any Amiga software company.  I am willing to do that additional work, but only 
if there is sufficient interest.  If you want to see the enhancements I
discuss below (or any of your own design), please contact me and let me know. 
I did this mainly as an exercise, but think it would be valuable to continue
work on it, but not if I'm the only one who will use it.

I hope you find these little programs interesting.  I think they're kind of
spooky myself.  I'm not sure I like it when my computer types things for me 
without my pressing the keys...

Davide P. Cervone
dpvc@tut.cc.rochester.EDU
dpvc@ur-tut.UUCP
DPVC@UORDBV.BITNET
SHAR_EOF
cat << \SHAR_EOF > journal.doc
OVERVIEW:

JOURNAL is a program that records a sequence of mouse and keyboard events as 
they occur and stores them in a file.  The file can be played back via the
program PLAYBACK, thus causing the same sequence of events to occur again. 
This is useful for creating demonstrations of programs (much like the "Guided
Tours" on the Macintosh computer).  It also is useful for documenting
repeatable bugs:  rather than trying to describe to CATS (or some third-party
vender) what you did that revealed the bug, you could record the actions with
JOURNAL, and send them the output file instead.  Finally, you can use JOURNAL
to set up turnkey demo-disks for conferences and computer shows that will run
unattended while you talk to customers.  It's even possible for PLAYBACK to
restart itself when it comes to the end of the file.  In a future version,
there will be a journal editor that allows you to add "special effects" like
explanation windows that tell the viewer what you are doing, or that call up
the narrator device to "speak" some information as your demonstration
progresses. 

JOURNAL may not work with some programs the "take over" the system or bypass
the Input.Device (see USAGE NOTES for additional information).


HOW TO USE JOURNAL:

To run JOURNAL, type:

    1> RUN JOURNAL TO <file>

where <file> is the name of the journal file that you wish to produce.  JOURNAL
should respond by displaying its version number and telling you how to cancel
it, and by moving the pointer to the upper, left-hand corner of the screen. 
Once the pointer has been moved, JOURNAL will record all the key presses and
mouse movements that you perform, by writing them to the file that you
specified.

This file will be used A LOT, so if it is on floppy-disk, be sure that the
disk can be left in the drive, and that it has enough space available for
the journal file.  The journal grows FAST, so do some exparimenting before
you create a long journal file.  RAM: is a good place to write small journals,
but may not be appropriate for long ones, as you may need your RAM space for
the programs that you are running.

When you are done creating your journal, press CTRL-LEFTAMIGA-E.  This signals
JOURNAL to stop recording events.  Once you get the "Joural Complete" message,
any further actions will not be added to the journal file.


HOW JOURNAL WORKS:

JOURNAL installs an input handler into the handler chain of the Input.Device. 
This handler's priority puts it ahead of Intuition, so it sees the "undigested"
events as they arrive from the Keyboard.Device, Gameport.Device, and
Timer.Device.  When keyboard or mouse events are passed to the handler, it
copies them into a linked list of events and signals JOURNAL that new events
are ready, then passes the unmodified event list on to Intuition.  JOURNAL
picks up the list of events and packs them into "TinyEvents" that take up
fewer bytes, and records these in the output file.  To save even more space in
the file, JOURNAL compresses multiple mouse move events into single output file
records when possible, but records the number of events that were compressed,
so that they can be recreated by PLAYBACK, if desired.  The amount of 
compression is controlled by command-line options (see below).

When the handler sees that CTRL-AMIGA-E has been pressed, it signals JOURNAL,
which tells the Input.Device to remove the handler from the chain and closes
the journal file.


JOURNAL OPTIONS:

JOURNAL supports a number of command-line options that modify its behavior:

    TO <file>       Designates <file> as the JOURNAL output file (the keyword
                    "TO" is optional).
    
    DX x            Specifies the amount of x movement that can be compressed
                    into one output event (mouse movements are added together
                    until their conbined x-offsets exceed DX or their
                    combined y-offsets exceed DY).  The default value is 8.

    DY y            Specifies the amount of y movement that can be compressed
                    into one output event (mouse movements are added together
                    until their conbined x-offsets exceed DX or their
                    combined y-offsets exceed DY).  The default value is 8.

    SMOOTHX x       Specifies an alternate value for DX that is used when extra
                    precision is needed (for example, when you need to draw
                    a free-form line in a paint program).  This value is in
                    effect when the SMOOTH and TRIGGER mask conditions are
                    met (see below).  The default value is 1.

    SMOOTHY y       Specifies an alternate value for DY that is used when extra
                    precision is needed (for example, when you need to draw
                    a free-form line in a paint program).  This value is in
                    effect when the SMOOTH and TRIGGER mask conditions are
                    met (see below).  The default value is 1.

    SMOOTH mask     Indicates which qualifier keys MUST be pressed in order to 
                    activate the SMOOTHX and SMOOTHY values.  Whan ALL the
                    keys specified by the SMOOTH mask are pressed, and AT LEAST
                    ONE of the keys designated by the TRIGGER mask is pressed,
                    then the SMOOTHX and SMOOTHY values are used in place of 
                    DX and DY.  By default, the left Amiga key specifies
                    smoothing.

    TRIGGER mask    Indicates which qualifier keys trigger the use of SMOOTHX
                    and SMOOTHY.  When ANY ONE of the keys indicated by TRIGGER
                    mask is pressed, and ALL of the keys specified by the
                    SMOOTH mask are pressed, then SMOOTHX and SMOOTHY are used
                    in place of DX and DY.  By default, any qualifier key 
                    triggers smoothing.

For people who understand "C" syntax, the expression that determines whether
to use DX and DY or SMOOTHX and SMOOTHY is the following:

  if (((Qualifiers & SmoothMask) == SmoothMask) && (Qualifiers & TriggerMask))
    use SMOOTHX and SMOOTHY
   else
    use DX and DY

Note that this provides considerable flexibility in specifying what will
activate smoothing.  For example, to specify that pressing either button will
cause smoothing, use:

    1> JOURNAL TO <file> SMOOTH 0 TRIGGER 0x6000

To specify that left Amiga plus left shift together with either button 
should cause smoothing, use:

    1> JOURNAL TO <file> SMOOTH 0x0041 TRIGGER 0x6000

The complete set of qualifier values is listed in the include file 
"DEVICES/INPUTEVENT.H".  Important values are listed here:

    Left Shift          0x0001
    Right Shift         0x0002
    Caps Lock           0x0004
    Control             0x0008
    Left ALT            0x0010
    Right ALT           0x0020
    Left Amiga          0x0040
    Right Amiga         0x0080
    Left Button         0x2000    
    Right Button        0x4000

To determine the mask value, take the bitwise OR of the values for the
qualifiers you want to use.   Note that the SMOOTH and TRIGGER masks should be
specified in HEX, not decimal.


HOW TO USE PLAYABCK:

To use playback, type:

    1> RUN PLAYBACK FROM <file>

where <file> is the name of a journal file recorded with the JOURNAL command.
PLAYBACK should print a message telling you its version number and how to
abort the playback, and then should begin playing back the recorded events. 
All you have to do is sit back and watch. 

When PLAYBACK is running, you will not be able to control the mouse yourself,
and the keyboard will have no effect (except for CTRL-C, which cancels 
PLAYBACK).  This is so that you do not disturb the sequence of the journal
being played back.

When the last event is played back, PLAYBACK will inform you that it is done.

To cancel PLAYBACK at any time, press CTRL-C.  This is the only key that has
any effect while PLAYBACK is running.


HOW PLAYBACK WORKS:

PLAYBACK installs an input handler into the handler chain of the Input.Device.
This handler's priority puts it ahead of Intuition, so it can insert new
events into or remove events from the stream seen by Intuition.  PLAYBACK
reads packed events from the journal file and unpacks them into an event
buffer where the input handler adds them to the event stream.  Since multiple
mouse movements are compressed into single journal records, PLAYBACK must
uncompress these by extrapolating intermediate mouse positions.  The input
handler removes all keyboard and mouse events sent to it by the Input.Device
so that these do not interfere with the journal being played back.  If the
handler sees a CTRL-C it signals PLAYBACK that the user wants to abort the
playback before it is complete.  PLAYBACK then requests the input.Device to
remove the handler.

PLAYBACK uses a FIFO event queue to supply events to the input handler.  Since
the handler may have to wait for some time to pass before it posts the next
event in the queue, this allows PLAYBACK to "read ahead" in the journal file, 
and prepare additional events for posting.  This makes the mouse movements
smooth and un-interrupted.  When PLAYBACK fills the queue, it waits for the
handler to post some events (freeing space in the queue).  You can control 
the size of the queue via the EVENTS command-line option.


PLAYBACK OPTIONS:

PLAYBACK supports a number of command-line options that modify its behavior:

    FROM <file>     Specifies the journal file to be played back (the keyword
                    "FROM" is optional).

    EVENTS n        Specifies the size (in events) of the events buffer
                    used to pass events from PLAYBACK to the input handler.
                    EVENTS must be at least two.  The default value is 50,
                    but it can be set as low as 10 and still provide acceptable
                    performance for some tasks.  The number of events may need
                    to be increased if you have many other tasks running, or
                    are accessing the disk containing the journal file
                    frequently, or have specified small DX and DY values when
                    you recorded the journal file.

    SMOOTH          Specifies that compressed mouse moves should be expanded
                    into multiple mouse moves of smaller offsets.  This makes
                    the playback look smoother, and makes precise mouse moves
                    possible without taking up space in the journal file.
                    The default is SMOOTH.

    NOSMOOTH        Specifies that compressed mouse moves should be left
                    compressed (i.e., no extrapolated events should be posted).
                    This may be necessary if you need extremely quick response
                    time or if you have specified a small DX and DY during
                    recording and do not need to uncompress mouse moves.
                    The default is SMOOTH.


HOW TO COMPILE AND LINK JOURNAL AND PLAYBACK:

JOURNAL AND PLAYBACK were developed using the Lattice C compiler version 3.10. 
I have tried to make it as generic as possible, but since I don't have the 
Aztex C compiler, I don't know whether it works with Manx.  For Lattice C, all 
you have to do is:

    1> lc -v journal playback
    1> asm handlerstub
    1> blink with journal.lnk
    1> blink with playback.lnk

The '-v' option is so that no stack checking code will be produced.  Since the
input handler runs on the Input.Device's stack, this would confuse the stack
check code and probably crash the system (I never tried it, though).


USAGE NOTES:

Since PLAYBACK plays back exactly what was recorded by JOURNAL, it is
important that the initial conditions be the same for both JOURNAL and
PLAYBACK.  If even one window is not where it was when your ran JOURNAL, then
PLAYBACK may become hopelessly out of synchronization.

It is best to run JOURNAL from a freshly booted system, or a newly opened
NewCLI or AmigaDOS window.   If the journal needs programs or other windows to
be open, don't open them first and then run JOURNAL.  Instead, run JOURNAL and
then open the windows or run the programs.  This way you can be sure that
what you want will be there, and in the proper places.  If you are recording
actions on the Workbench, you should start with all the disk windows closed,
and the disks in their default positions then open them within the journal. 
Beware that disk positions may change depending on the order in which they
were inserted.

One "feature" of JOURNAL and PLAYBACK is that they do not know about disk
insert requesters.  If a disk requester comes up, JOURNAL keeps right on
recording mouse moves and key strokes.  If you cancel the requester, so will
PLAYBACK.  But if you put in a disk and the requester goes away, there is no
guarantee that the user who is playing back your journal will replace the disk
in the same amount of time you did (in fact, he may never replace it), so the
rest of the journal may become out-of-synch.  Worse yet, the user can pop a
disk out of the drive at any time, which may cause a disk requester to come up
that did not appear when you recorded the journal, or the user might have
fewer disk drives than you do, so he may have to swap drives differently from
how  you did.   All of these cases cause trouble for JOURNAL and PLAYBACK, so
be careful when you are using multiple disks with journal.  (See FUTURES for
additional comments on disk-requesters).

Since JOURNAL will be writing lots of data to its output file, and PLAYBACK
will be reading losts of data from the journal file, the journal file must be
on a disk that can remain in a drive.  RAM: is a good candidate for small
journals, but may not have room for a large journal plus the programs that will
be running.

JOURNAL's input handler allocates memory for the events as it copies them.  
This memory is not freed again until JOURNAL actually records the event.  If
events are coming in very fast, JOURNAL may not be able to keep up with them. 
This may use up considerable memory until JOURNAL can catch up.  If you are
having memory problems while using JOURNAL, try slowing down your mouse
movements to give JOURNAL some time to catch up.

You may need to run JOURNAL and PLAYBACK at higher priorities than your normal
processes in order to give them enough time to get the events to and from the
input handler in a timely fashion.  Both progams use the Wait() function and
neither "busy waits," so they should cooperate with other tasks even when they
are at a higher priority.

If your journal runs a program that has to be loaded from disk, be sure to
wait long enough for it to be loaded during the PLAYBACK.  Some disks take
longer than others, and sometimes there are slight timing differences between
JOURNAL and PLAYBACK.  Give a little extra time after disk accesses before you
move the mouse or press a key.

JOURNAL only records mouse and keyboard events that come through the input
device.  It does NOT record events that are received directly by a program
through a message port from devices opened explicitly by the program.  For
example, if your program opens the timer device and gets timer messages, these
are NOT recorded by JOURNAL.  Similarly, JOURNAL does not record messages from
the serial port, parallel port, or second mouse port.  Note that this means
that JOURNAL will not record joystick motions from the second mouse port, so
PLAYBACK can not reproduce them.  This may make it impossible to make a journal 
record of some games.


FUTURES:

The disk insert problem mentioned in USAGE NOTES above needs to be fixed.  The 
only solution I can think of is to use SetFunction to replace AutoRequest with 
a function that signals the JOURNAL or PLAYBACK process that a request has 
occured, then calls the original AutoRequest, then signals when the request is 
finished.  JOURNAL and PLAYBACK would "pause" until the AutoRequest was done,
and then continue recording or playing back.  Since AutoRequest allows you to
specify what kinds of flags will cancel the request, the replacement routine
could check these to see if DISKINSERTED messages will satisfy it.  If not,
then the original AutoRequest could be called without signalling the JOURNAL
or PLAYBACK processes.  This way we would not be trapping the wrong requesters.
Unfortunately, Workbench disk requests do not seem to use the DISKINSERTED
flag, so something more sophsticated may be needed.  I am open to suggestions.

In the future (if there is enough interest), I plan to write a journal editor
that allows you to step through a recorded journal and modify it by changing
the events, adding new events, or deleting events.  Also, you will be able to
add "special effects" like pop-up windows that contain explanations of what
your program is doing, what qualifier keys are being pressed, etc.  I also
hope to allow you to do this via the narrator device rather than through a
window, if desired.  If I get real ambitious, I might even add something
to play music from a file, but that will have to be far future for now.  If
you want these features, please let me know.


AUTHOR:

Davide P. Cervone
University of Rochester Computing Center        DPVC@UORDBV.BITNET
Taylor Hall                                     dpvc@tut.cc.rochester.EDU
Rochester, New York  14627                      dpvc@ur-tut.UUCP
(716) 275-2811
SHAR_EOF
#	End of shell archive
exit 0

dpvc@ur-tut.UUCP (Davide P. Cervone) (07/10/87)

    Here are the sources to a neat utility that allows you to record
and playback events that happen in a window.  Binaries available in
comp.binaries.amiga, documentation available in another article in this
group.
    -Doc


#	This is a shell archive.
#	Remove everything above and including the cut line.
#	Then run the rest of the file through sh.
#----cut here-----cut here-----cut here-----cut here----#
#!/bin/sh
# shar:    Shell Archiver
#	Run the following text with /bin/sh to create:
#	journal.c
#	playback.c
#	journal.h
#	handlerstub.a
#	journal.lnk
#	playback.lnk
# This archive created: Sun Jun 21 22:39:23 1987
# By:	 (Davide P. Cervone)
cat << \SHAR_EOF > journal.c
/*
 *  JOURNAL.C  -  Records all mouse and keyboard activity so that
 *                it can be played back for demonstration of products,
 *                reporting errors, etc.
 *
 *             Copyright (c) 1987 by Davide P. Cervone
 *  You may use this code provided this copyright notice is kept intact.
 */

#include "journal.h"

/*
 *  Version number and author:
 */
char version[32] = "Journal v1.0 (June 1987)";
char *author     = "Copyright (c) 1987 by Davide P. Cervone";


/*
 *  Macros used to check for end-of-journal
 */
#define CTRL_AMIGA      (IEQUALIFIER_CONTROL | IEQUALIFIER_LCOMMAND)
#define KEY_E           0x12
#define CTRL_AMIGA_E(e) ((((e)->ie_Qualifier & CTRL_AMIGA) == CTRL_AMIGA) &&\
                          ((e)->ie_Code == KEY_E))

/*
 *  Match a command-line argument against a string (case insensitive)
 */
#define ARGMATCH(s)   (stricmp(s,*Argv) == 0)

/*
 *  Functions that JOURNAL can perform
 */
#define SHOW_USAGE      0
#define WRITE_JOURNAL   1
#define JUST_EXIT       2

/*
 *  Largest mouse move we want to record
 */
#define MAXMOUSEMOVE   32

/*
 *  Macros to tell whether a mouse movement event can be compressed with
 *  other mouse movement events
 */
#define MOUSEMOVE(e)\
   ((e)->ie_Class == IECLASS_RAWMOUSE && (e)->ie_Code == IECODE_NOBUTTON &&\
   ((e)->ie_Qualifier & IEQUALIFIER_RELATIVEMOUSE))
#define BIGX(e)         ((e)->ie_X >= XMINMOUSE || (e)->ie_X <= -XMINMOUSE)
#define BIGY(e)         ((e)->ie_Y >= YMINMOUSE || (e)->ie_Y <= -YMINMOUSE)
#define BIGTICKS(e)     ((e)->my_Ticks > LONGTIME)
#define NOTSAVED(e)     ((e)->my_Saved == FALSE)
#define SETSAVED(e)     ((e)->my_Saved = TRUE)


/*
 *  Global Variables:
 */

struct MsgPort *InputPort = NULL;     /* Port used to talk to Input.Device */
struct IOStdReq *InputBlock = NULL;   /* request block used with Input.Device */
struct Task *theTask = NULL;          /* pointer to our task */
LONG InputDevice = 0;                 /* flag whether Input.Device is open */
LONG theSignal = 0;                   /* signal used when an event is ready */
LONG ErrSignal = 0;                   /* signal used when an error occured */
LONG theMask;                         /* 1 << theSignal */
LONG ErrMask;                         /* 1 << ErrSignal */

UWORD Ticks = 0;                      /* number of timer ticks between events */
LONG  TimerMics = 0;                  /* last timer event's micros field */

WORD xmove = XDEFMIN;                 /* distance to compress into one event */
WORD ymove = YDEFMIN;                 /* distance to compress into one event */
WORD smoothxmove = 1;                 /* distance for smoothed events */
WORD smoothymove = 1;                 /* distnace for smoothed events */
WORD xminmove, yminmove;              /* distance actually in use */
UWORD SmoothMask = IEQUALIFIER_LCOMMAND;
                                      /* what keys are required for smoothing */
UWORD SmoothTrigger = 0xFFFF;         /* any of these keys trigger smoothing */

int Action = WRITE_JOURNAL;           /* action to be perfomed by JOURNAL */
int ArgMatched = FALSE;               /* TRUE if a parameter matched OK */
int Argc;                             /* global version of argc */
char **Argv;                          /* global version of argv */

struct InputEvent **EventPtr = NULL;  /* pointer to (pointer to next event) */
struct InputEvent *OldEvent = NULL;   /* pointer to last event */
struct SmallEvent TinyEvent;          /* packed event (ready to record) */

FILE *OutFile = NULL;                 /* where the events will be written */
char *JournalFile = NULL;             /* name of the output file */

int NotDone = TRUE;                   /* continue looking for events? */
int NotFirstEvent = FALSE;            /* TRUE after an event was recorded */
int PointerNotHomed = TRUE;           /* TRUE until pointer is moved */

struct Interrupt HandlerData =        /* used to add an input handler */
{
   {NULL, NULL, 0, 51, NULL},           /* Node structure (nl_Pri = 51) */
   NULL,                                /* data pointer */
   &myHandlerStub                       /* code pointer */
};

struct InputEvent PointerToHome =     /* event to put pointer in upper-left */
{
   NULL,                                /* pointer to next event */
   IECLASS_RAWMOUSE,                    /* ie_Class = RAWMOUSE */
   0,                                   /* ie_SubClass */
   IECODE_NOBUTTON,                     /* ie_Code = NOBUTTON (just a move) */
   IEQUALIFIER_RELATIVEMOUSE,           /* ie_Qualifier = relative move */
   {-1000,-1000},                       /* move far to left and top */
   {0L,0L}                              /* seconds and micros */
};

struct SmallEvent TimeEvent =         /* pause written at beginning of file */
{
   {0xE2, 0, 0},                        /* MOUSEMOVE, NOBUTTON, X=0, Y=0 */
   IEQUALIFIER_RELATIVEMOUSE,           /* Qualifier */
   0x00A00000                           /* 1 second pause */
};


/*
 *  myHandler()
 *
 *  This is the input handler that makes copies of the input events and sends 
 *  them the to main process to be written to the output file.
 *
 *  The first time around, we add the PointerToHome event into the stream
 *  so that the pointer is put into a known position.
 *
 *  We check the event type of each event in the list, and do the following:
 *  for Timer events, we increment the tick count (which tells how many ticks
 *  have occured since the last recorded event); for raw key events, we check
 *  whether a CTRL-AMIGA-E has been pressed (if so, we signal the main process
 *  with a CTRL-E which tells it to remove the handler and quit); for raw 
 *  mouse and raw key events, we allocate memory for a new copy of the event
 *  (and signal an error if we can't), and copy the pertinent information
 *  from the current event into the copy event and mark it as not-yet-saved.
 *  We link it into the copied-event list (via EventPtr), and signal the
 *  main task that a new event is ready, and then zero the tick count.
 *
 *  Any other type of event is ignored.
 *
 *  When we are through with the event list, we return it so that Intuition
 *  can use it to do its thing.
 */

struct InputEvent *myHandler(event,data)
struct InputEvent *event;
APTR data;
{
   struct InputEvent *theEvent = event;
   struct InputEvent *theCopy;

   Forbid();
   if (PointerNotHomed)
   {
      PointerToHome.ie_NextEvent = event;
      event = &PointerToHome;
      PointerNotHomed = FALSE;
   }
   while(theEvent)
   {
      switch(theEvent->ie_Class)
      {
         case IECLASS_TIMER:
            Ticks++;
            TimerMics = theEvent->ie_Mics;
            break;

         case IECLASS_RAWKEY:
            if (CTRL_AMIGA_E(theEvent)) Signal(theTask,SIGBREAKF_CTRL_E);

         case IECLASS_RAWMOUSE:
            theCopy = NEWEVENT;
            if (theCopy == NULL)
            {
               Signal(theTask,ErrMask);
            } else {
               theCopy->ie_NextEvent    = NULL;
               theCopy->ie_Class        = theEvent->ie_Class;
               theCopy->ie_Code         = theEvent->ie_Code;
               theCopy->ie_Qualifier    = theEvent->ie_Qualifier;
               theCopy->ie_EventAddress = theEvent->ie_EventAddress;
               theCopy->my_Time         = TIME;
               theCopy->my_Ticks        = Ticks;
               theCopy->my_Saved        = FALSE;
               *EventPtr = theCopy;
               EventPtr = &(theCopy->ie_NextEvent);
               Signal(theTask,theMask);
               Ticks = 0;
            }
            break;
      }
      theEvent = theEvent->ie_NextEvent;
   }
   Permit();
   return(event);
}


/*
 *  Ctrl_C()
 *
 *  Dummy routine to disable Lattice-C CTRL-C trapping.
 */

#ifndef MANX
int Ctrl_C()
{
   return(0);
}
#endif


/*
 *  DoExit()
 *
 *  General purpose exit routine.  If 's' is not NULL, then print an
 *  error message with up to three parameters.  Free any memory, close
 *  any open files, delete any ports, free any used signals, etc.
 */

void DoExit(s,x1,x2,x3)
char *s, *x1, *x2, *x3;
{
   long status = 0;
   
   if (s != NULL)
   {
      printf(s,x1,x2,x3);
      printf("\n");
      status = RETURN_ERROR;
   }
   if (OldEvent)    FREEVENT(OldEvent);
   if (OutFile)     fclose(OutFile);
   if (InputDevice) CloseDevice(InputBlock);
   if (InputBlock)  DeleteStdIO(InputBlock);
   if (InputPort)   DeletePort(InputPort);
   if (theSignal)   FreeSignal(theSignal);
   if (ErrSignal)   FreeSignal(ErrSignal);
   exit(status);
}


/*
 *  CheckNumber()
 *
 *  Check a command-line argument for the given keyword, and if it matches,
 *  makes sure that the next parameter is a positive numeric value that
 *  is less than the maximum expected value.
 */

void CheckNumber(keyword,value)
char *keyword;
WORD *value;
{
   long lvalue = *value;

   if (Argc > 1 && ARGMATCH(keyword))
   {
      ArgMatched = TRUE;
      Argc--;
      if (sscanf(*(++Argv),"%ld",&lvalue) != 1)
      {
         printf("%s must be numeric:  '%s'\n",keyword,*Argv);
         Action = JUST_EXIT;
      }
      if (lvalue < 1 || lvalue > MAXMOUSEMOVE)
      {
         printf("%s must be positive and less than %d:  '%ld'\n",
            keyword,MAXMOUSEMOVE,lvalue);
         Action = JUST_EXIT;
      }
      *value = lvalue;
   }
}


/*
 *  CheckHexNum()
 *
 *  Check a command-line argument for the given keyword, and if it
 *  matches, make sure that the next parameter is a legal HEX value.
 */

void CheckHexNum(keyword,value)
char *keyword;
WORD *value;
{
   ULONG lvalue = *value;

   if (Argc > 1 && ARGMATCH(keyword))
   {
      ArgMatched = TRUE;
      Argc--;
      if (sscanf(*(++Argv),"%lx",&lvalue) != 1)
      {
         printf("%s must be a HEX number:  '%s'\n",keyword,*Argv);
         Action = JUST_EXIT;
      }
      *value = lvalue;
   }
}


/*
 *  ParseArguements()
 *
 *  Check that all the command-line arguments are valid and set the 
 *  proper variables as requested by the user.  If no keyword is specified,
 *  assume "TO".  If no file is specified, then show the usage.  DX and DY
 *  set the "granularity" of the mouse moves recorded (moves are combined
 *  into a single event until it's movement excedes either DX or DY).
 *  Similarly, SMOOTHX and SMOOTHY set alternate DX and DY values
 *  for when extra precision is needed (i.e., when drawing curves in a paint
 *  program).  SMOOTH specifies what qualifier keys MUST be present to 
 *  activate the SMOOTHX and SMOOTHY values, and TRIGGER specifies a set of
 *  qualifiers any one of which (together with the SMOOTH qualifiers)
 *  will active the SMOOTHX and SMOOTHY values.  In other words, all the
 *  SMOOTH qualifiers plus at least one of the TRIGGER qualifiers must be
 *  pressed in order to activate the smooth values.  For example, if SMOOTH
 *  is 0 and TRIGGER is 0x6000, then holding down either the left or the
 *  right button will activate the smooth values.  If SMOOTH is 0x0040 rather
 *  than 0, then the left Amiga button must also be held down in order to 
 *  activate SMOOTHX and SMOOTHY.  The qualifier flags are listed in
 *  DEVICES/INPUTEVENT.H
 *
 *  The default values are DX = 8, DY = 8, SMOOTHX = 1, SMOOTHY = 1,
 *  SMOOTH = left Amiga, TRIGGER = 0xFFFF.
 */

int ParseArguments(argc,argv)
int argc;
char **argv;
{
   Argc = argc;
   Argv = argv;

   while (--Argc > 0)
   {
      ArgMatched = FALSE;
      Argv++;
      if (Argc > 1 && ARGMATCH("TO"))
      {
         JournalFile = *(++Argv);
         Argc--;
         ArgMatched = TRUE;
      }
      CheckNumber("DX",&xmove);
      CheckNumber("DY",&ymove);
      CheckNumber("SMOOTHX",&smoothxmove);
      CheckNumber("SMOOTHY",&smoothymove);
      CheckHexNum("SMOOTH",&SmoothMask);
      CheckHexNum("TRIGGER",&SmoothTrigger);
      if (ArgMatched == FALSE)
      {
         if (JournalFile == NULL)
            JournalFile = *Argv;
           else
            Action = SHOW_USAGE;
      }
   }
   if (JournalFile == NULL && Action == WRITE_JOURNAL) Action = SHOW_USAGE;
   return(Action);
}


/*
 *  OpenJournal()
 *
 *  Open the journal file and check for errors.  Write the version
 *  information to the file.
 */

void OpenJournal()
{
   OutFile = fopen(JournalFile,"w");
   if (OutFile == NULL)
      DoExit("Can't Open Journal File '%s', error %ld",JournalFile,_OSERR);
   if (fwrite(version,sizeof(version),1,OutFile) != 1)
      DoExit("Error writing to output file:  %ld",_OSERR);
}


/*
 *  GetSignal()
 *
 *  Allocate a signal (error if none available) and set the mask to
 *  the proper value.
 */

void GetSignal(theSignal,theMask)
LONG *theSignal, *theMask;
{
   LONG signal;

   if ((signal = AllocSignal(-ONE)) == -ONE) DoExit("Can't Get Signal");
   *theSignal = signal;
   *theMask = (ONE << signal);
}


/*
 *  SetupTask()
 *
 *  Find the task pointer for the main task (so the input handler can 
 *  signal it).  Clear the CTRL signal flags (so we don't get any left
 *  over from before JOURNAL was run) and allocate some signals for 
 *  new events and errors (so the input handler can signal them).
 */

void SetupTask()
{
   theTask = FindTask(NULL);
   SetSignal(0L,SIGBREAKF_ANY);
   GetSignal(&theSignal,&theMask);
   GetSignal(&ErrSignal,&ErrMask);
   #ifndef MANX
      onbreak(&Ctrl_C);
   #endif
}


/*
 *  SetupEvents()
 *
 *  Get a fake old-event to start off with, and mark it as saved (so we don't
 *  really try to use it).  Make it the end of the list (set its next pointer
 *  to NULL.  Tell the input handler where to start allocating new events
 *  by setting EventPtr to point the the next-pointer.  When the input
 *  handler allocates a new copy of an event, it will link it to this one
 *  so the main process can find it by following the next-pointer from the
 *  old event.
 */

void SetupEvents()
{
   if ((OldEvent = NEWEVENT) == NULL) DoExit("No Memory for OldEvent");
   SETSAVED(OldEvent);
   OldEvent->ie_NextEvent = NULL;
   EventPtr = &(OldEvent->ie_NextEvent);
}


/*
 *  AddHandler()
 *
 *  Add the input handler to the input.device handler chain.  Since the
 *  priority is 51, it will appear BEFORE intuition, so all it should
 *  see are raw key, raw mouse, timer, and disk insert/remove events.
 */
 
void AddHandler()
{
   long status;

   if ((InputPort = CreatePort(0,0)) == NULL)
      DoExit("Can't Create Port");
   if ((InputBlock = CreateStdIO(InputPort)) == NULL)
      DoExit("Can't Create Standard IO Block");
   InputDevice = (OpenDevice("input.device",0,InputBlock,0) == 0);
   if (InputDevice == 0) DoExit("Can't Open Input Device");
   
   InputBlock->io_Command = IND_ADDHANDLER;
   InputBlock->io_Data    = (APTR) &HandlerData;
   if (status = DoIO(InputBlock)) DoExit("Error from DoIO:  %ld",status);
   printf("%s - Press CTRL-AMIGA-E to End Journal\n",version);
}


/*
 *  RemoveHandler()
 *
 *  Remove the input handler from the input.device handler chain.
 */
 
void RemoveHandler()
{
   long status;

   if (InputDevice && InputBlock)
   {
      InputBlock->io_Command = IND_REMHANDLER;
      InputBlock->io_Data = (APTR) &HandlerData;
      if (status = DoIO(InputBlock)) DoExit("Error from DoIO:  %ld",status);
   }
   printf("Journal Complete\n");
}


/*
 *  SaveEvent()
 *
 *  Pack an InputEvent into a SmallEvent (by shifting bits around) so that
 *  it takes up less space in the output file.  Write the SmallEvent to the
 *  output file, and mark it as already-saved.
 */

void SaveEvent(theEvent)
struct InputEvent *theEvent;
{
   if (theEvent->my_Time > MILLION) theEvent->my_Time += MILLION;
   TinyEvent.se_XY        = 0;
   TinyEvent.se_Type      = theEvent->ie_Class;
   TinyEvent.se_Qualifier = theEvent->ie_Qualifier;
   TinyEvent.se_Long2     = (theEvent->my_Ticks << 20) |
                            (theEvent->my_Time & 0xFFFFF);

   if (theEvent->ie_Class == IECLASS_RAWKEY)
   {
      TinyEvent.se_Code  = theEvent->ie_Code;
      TinyEvent.se_Prev  = theEvent->my_Prev;
   } else {
      TinyEvent.se_Type |= (theEvent->ie_Code & IECODE_UP_PREFIX) |
                           ((theEvent->ie_Code & 0x03) << 5);
      TinyEvent.se_XY |= (theEvent->ie_X & 0xFFF) |
                         ((theEvent->ie_Y & 0xFFF) << 12);
   }

   if (fwrite((char *)&TinyEvent,sizeof(TinyEvent),1,OutFile) != 1)
      DoExit("Error writing to output file:  %ld",_OSERR);
   SETSAVED(theEvent);
   NotFirstEvent = TRUE;
}


/*
 *  SaveTime()
 *
 *  Save a fake mouse event that doesn't move anywhere but that includes a
 *  tick count.  That is, pause without moving the mouse.
 */

void SaveTime(theEvent)
struct InputEvent *theEvent;
{
   if (NotFirstEvent) TimeEvent.se_Ticks = (theEvent->my_Ticks << 20);
   if (fwrite((char *)&TimeEvent,sizeof(TimeEvent),1,OutFile) != 1)
      DoExit("Error writing to output file:  %ld",_OSERR);
   theEvent->my_Ticks = 0;
}


/*
 *  SaveEventList()
 *
 *  Write the events in the event list (built by the input handler) out
 *  to the output file, compressing multiple, small mouse moves into larger,
 *  single mouse moves (to save space in the output file) in the following 
 *  way:
 *
 *    if the current event is a mouse move (not a button press), then
 *      set its event count to 1 (the number of events compressed into it),
 *      if the user is requesting smooth movement, then use the smooth
 *        movement variables, otherwise use the course (normal) values. 
 *      if the event's x or y movement is big enough, or if there was a long
 *           pause before the movement occured, then
 *        if the old event was not saved, save it.
 *        if the pause was long enough, save a separate pause (so that the
 *           smoothing algorithm in PLAYBACK does not spread the pause over
 *           the entire mouse move).
 *        save the current event.
 *      otherwise, (we can compress the movement)
 *        if there was an old mouse event that was not saved,
 *          add it to the current event,
 *          if the new x or y movement is big enough to record, do so.
 *    otherwise, (this was not a mouse movement)
 *      if there was a previous mouse movement that was not saved, save it.
 *      finally, save the current event.
 *  At this point the OldEvent is either posted, or has been combined with the
 *  current event, so we can free the old event.  The current event then
 *  becomes the old event.
 */

void SaveEventList()
{
   struct InputEvent *theEvent;
   
   while ((theEvent = OldEvent->ie_NextEvent) != NULL)
   {
      if (MOUSEMOVE(theEvent))
      {
         theEvent->my_Count &= (~COUNTMASK);
         theEvent->my_Count++;
         if ((theEvent->ie_Qualifier & SmoothMask) == SmoothMask &&
             (theEvent->ie_Qualifier & SmoothTrigger))
         {
            xminmove = smoothxmove;
            yminmove = smoothymove;
         } else {
            xminmove = xmove;
            yminmove = ymove;
         }
         if (BIGX(theEvent) || BIGY(theEvent) || BIGTICKS(theEvent))
         {
            if (NOTSAVED(OldEvent)) SaveEvent(OldEvent);
            if (BIGTICKS(theEvent)) SaveTime(theEvent);
            SaveEvent(theEvent);
         } else {
            if (NOTSAVED(OldEvent))
            {
               theEvent->ie_X += OldEvent->ie_X;
               theEvent->ie_Y += OldEvent->ie_Y;
               theEvent->my_Ticks += OldEvent->my_Ticks;
               theEvent->my_Count += OldEvent->my_Count & COUNTMASK;
               if (BIGX(theEvent) || BIGY(theEvent)) SaveEvent(theEvent);
            }
         }
      } else {
         if (NOTSAVED(OldEvent)) SaveEvent(OldEvent);
         SaveEvent(theEvent);
      }
      FREEVENT(OldEvent);
      OldEvent = theEvent;
   }
}


/*
 *  RecordJournal()
 *
 *  Open the journal file, set up the task and signals, and set up the
 *  initial pointers for the event list.  Then add the input handler
 *  into the Input.Device handler chain.  
 *
 *  Wait for the input handler to signal us that an event is ready (or that
 *  an error occured), or that the user to press CTRL-AMIGA-E.  If it's the 
 *  latter, cancel the Wait loop, otherwise save the events that are in the
 *  list into the file.  If the error signal was sent, inform the user that
 *  some events were lost.
 *
 *  Once we are signaled to end the journal, remove the handler, and
 *  record any remaining, unsaved events to the file.
 */

void RecordJournal()
{
   LONG signals;
   LONG SigMask;
   
   OpenJournal();
   SetupTask();
   SetupEvents();
   SigMask = theMask | ErrMask | SIGBREAKF_CTRL_E;

   AddHandler(&myHandler);
   while (NotDone)
   {
      signals = Wait(SigMask);
      if (signals & SIGBREAKF_CTRL_E)
         NotDone = FALSE;
        else
         SaveEventList();
      if (signals & ErrMask)
         printf("[ Out of memory - some events not recorded ]\n");
   }
   RemoveHandler(&myHandler);
   SaveEventList();
}


/*
 *  main()
 *
 *  Parse the command-line arguments and perform the proper function
 *  (either show the usage, write a journal, or fall through and exit).
 */

void main(argc,argv)
int argc;
char **argv;
{
   switch(ParseArguments(argc,argv))
   {
      case SHOW_USAGE:
         printf("Usage:  JOURNAL [TO] file [DX x] [DY y]\n");
         printf("                [SMOOTHX x] [SMOOTHY y] [SMOOTH mask]");
         printf(               " [TRIGGER mask]\n");
         break;

      case WRITE_JOURNAL:
         RecordJournal();
         break;
   }
   DoExit(NULL);
}
SHAR_EOF
cat << \SHAR_EOF > playback.c
/*
 *  PLAYBACK.C  -  Plays back mouse and keyboard events that were recorded
 *                 by the JOURNAL program.
 *
 *             Copyright (c) 1987 by Davide P. Cervone
 *  You may use this code provided this copyright notice is kept intact.
 */

#include "journal.h"

/*
 *  Version number and author
 */
char *version = "Playback v1.0 (June 1987)";
char *author  = "Copyright (c) 1987 by Davide P. Cervone";

/*
 *  Usage string
 */
#define USAGE   "PLAYPACK [FROM] file [EVENTS n] [[NO]SMOOTH"

/*
 *  Macros to tell whether the user pressed CTRL-C
 */
#define CONTROL       IEQUALIFIER_CONTROL
#define KEY_C         0x33
#define CTRL_C(e)     (((e)->ie_Qualifier & CONTROL) && ((e)->ie_Code == KEY_C))

/*
 *  The packed code for a RAWMOUSE event with NOBUTTON pressed (i.e., one
 *  that probably contains more than one event compressed into a single 
 *  entry in the file).
 */
#define MOUSEMOVE     0xE2

/*
 *  Macro to check whether a command-line argument matches a given string
 */
#define ARGMATCH(s)   (stricmp(s,*argv) == 0)

/*
 *  The functions that PLAYBACK can perform
 */
#define SHOW_USAGE    0
#define READ_JOURNAL  1
#define JUST_EXIT     2


/*
 *  Global Variables
 */

struct MsgPort *InputPort = NULL;       /* Port for the Input.Device */
struct IOStdReq *InputBlock = NULL;     /* Request block for the Input.Device */
struct Task *theTask = NULL;            /* pointer to the main process */
int  HandlerActive = FALSE;             /* TRUE when handler has been added */
LONG InputDevice = FALSE;               /* TRUE when Input.Device is open */
LONG theSignal = 0;                     /* used when an event is freed */
LONG theMask;                           /* 1 << theSignal */

UWORD Ticks = 0;                        /* number of ticks between events */
LONG  TimerMics = 0;                    /* last timer event's micros field */
LONG  TimerSecs = 0;                    /* last timer event's seconds field */

struct InputEvent *Event = NULL;        /* pointer to array of input events */
struct SmallEvent TinyEvent;            /* a compressed event from the file */
long   MaxEvents  = 50;                 /* size of the Event array */
short  Smoothing  = TRUE;               /* TRUE if smoothing requested */
short  LastPosted = 0;                  /* Event index for last-posted event */
short  NextToPost = 0;                  /* Event index for next event to post */
short  NextFree   = 0;                  /* Event index for next event to use */

FILE *InFile = NULL;                    /* journal file pointer */
char *JournalFile = NULL;               /* name of journal file */


struct Interrupt HandlerData =          /* used to add an input handler */
{
   {NULL, NULL, 0, 51, NULL},             /* Node structure (nl_Pri = 51) */
   NULL,                                  /* data pointer */
   &myHandlerStub                         /* code pointer */
};


/*
 *  myHandler()
 *
 *  This is the input handler that posts the events read from the journal file.
 *
 *  First, free any events that were posted last time myHandler was
 *  called by the Input.Device.  Signal the main process when any are freed,
 *  in case it is waiting for an event to be freed.
 *
 *  Then, look through the list of events received from the Input.Device.
 *  Check whether a new event is ready to be posted (i.e., one is available
 *  and the proper number of ticks have been counted).  If so, then set its
 *  time fields to the proper time, add it into the event list, and look at
 *  the next event.  Set the tick count to zero again and check the next
 *  event in the array.
 *
 *  Once any new events have been added, check whether the current event
 *  from the Input.Device is a timer event.  If so, then increment the tick 
 *  count and record its time field.  If not, then check whether it is a
 *  raw mouse or raw key event.  If it is, then if it is a CTRL-C, signal the 
 *  main process that the user wants to abort the playback.  Remove the mouse
 *  or key event from the event list so that it will not interfere with the 
 *  playback events (i.e., the keyboard and mouse are disabled while PLAYBACK
 *  is running).
 *
 *  Finally, go on to the the next event in the chain and continue the loop.
 *  Once all the events have been processed, return the modified list
 *  (with new events added from the file and keyboard and mouse events removed)
 *  so that Intuition can act on them.
 */

struct InputEvent *myHandler(EventList,data)
struct InputEvent *EventList;
APTR data;
{
   struct InputEvent **EventPtr = &EventList;
   struct InputEvent *toPost = &Event[NextToPost];
   
   while (NextToPost != LastPosted)
   {
      Event[LastPosted].my_InUse = FALSE;
      Event[LastPosted].my_Ready = FALSE;
      LastPosted = (LastPosted + 1) % MaxEvents;
      Signal(theTask,theMask);
   }
   Forbid();
   while (*EventPtr)
   {
      while (toPost->my_Ready && Ticks >= toPost->my_Ticks)
      {
         toPost->ie_Secs = TimerSecs;
         toPost->ie_Mics += TimerMics;
         if (toPost->ie_Mics > MILLION)
         {
            toPost->ie_Secs++;
            toPost->ie_Mics -= MILLION;
         }
         toPost->ie_NextEvent = *EventPtr;
         *EventPtr = toPost;
         EventPtr = &(toPost->ie_NextEvent);
         NextToPost = (NextToPost + 1) % MaxEvents;
         toPost = &Event[NextToPost];
         Ticks = 0;
      }
      if ((*EventPtr)->ie_Class == IECLASS_TIMER)
      {
         Ticks++;
         TimerSecs = (*EventPtr)->ie_Secs;
         TimerMics = (*EventPtr)->ie_Mics;
      } else {
         if ((*EventPtr)->ie_Class == IECLASS_RAWMOUSE ||
             (*EventPtr)->ie_Class == IECLASS_RAWKEY)
         {
            if (CTRL_C(*EventPtr)) Signal(theTask,SIGBREAKF_CTRL_C);
            *EventPtr = (*EventPtr)->ie_NextEvent;
         }
      }
      EventPtr = &((*EventPtr)->ie_NextEvent);
   }
   Permit();
   return(EventList);
}


/*
 *  Ctrl_C()
 *
 *  Dummy routine to disable Lattice-C CTRL-C trapping.
 */

#ifndef MANX
int Ctrl_C()
{
   return(0);
}
#endif


/*
 *  DoExit()
 *
 *  General purpose exit routine.  If 's' is not NULL, then print an
 *  error message with up to three parameters.  Remove the handler (if
 *  it is active), free any memory, close any open files, delete any ports, 
 *  free any used signals, etc.
 */

void DoExit(s,x1,x2,x3)
char *s, *x1, *x2, *x3;
{
   long status = 0;
   
   if (s != NULL)
   {
      printf(s,x1,x2,x3);
      printf("\n");
      status = RETURN_ERROR;
   }
   if (HandlerActive) RemoveHandler();
   if (Event)         FreeMem(Event,IE_SIZE * MaxEvents);
   if (InFile)        fclose(InFile);
   if (InputDevice)   CloseDevice(InputBlock);
   if (InputBlock)    DeleteStdIO(InputBlock);
   if (InputPort)     DeletePort(InputPort);
   if (theSignal)     FreeSignal(theSignal);
   exit(status);
}

/*
 *  ParseArguements()
 *
 *  Check that all the command-line arguments are valid and set the 
 *  proper variables as requested by the user.  If no keyword is specified,
 *  assume "FROM".  If no file is specified, then show the usage.  EVENTS
 *  regulates the size of the Event array used for buffering event 
 *  communication between the main process and the handler.  SMOOTH and
 *  NOSMOOTH regulate the interpolation of mouse movements between recorded
 *  events.  The default is SMOOTH.
 */

int ParseArguments(argc,argv)
int argc;
char **argv;
{
   int function = READ_JOURNAL;

   while (--argc > 0)
   {
      argv++;
      if (argc > 1 && ARGMATCH("FROM"))
      {
         JournalFile = *(++argv);
         argc--;
      }
      else if (argc > 1 && ARGMATCH("EVENTS"))
      {
         argc--;
         if (sscanf(*(++argv),"%ld",&MaxEvents) != 1)
         {
            printf("Event count must be numeric:  '%s'\n",*argv);
            function = JUST_EXIT;
         }
         if (MaxEvents <= 1)
         {
            printf("Event count must be greater than 1:  '%d'\n",MaxEvents);
            function = JUST_EXIT;
         }
      }
      else if (ARGMATCH("NOSMOOTH")) Smoothing = FALSE;
      else if (ARGMATCH("SMOOTH"))   Smoothing = TRUE;
      else if (JournalFile == NULL) JournalFile = *argv;
      else function = SHOW_USAGE;
   }
   if (JournalFile == NULL && function == READ_JOURNAL) function = SHOW_USAGE;
   return(function);
}

/*
 *  OpenJournal()
 *
 *  Open the journal file and check for errors.  Read the version
 *  information to the file (someday we may need to check this).
 */

void OpenJournal()
{
   char fileversion[32];

   InFile = fopen(JournalFile,"r");
   if (InFile == NULL)
      DoExit("Can't Open Journal File '%s', error %ld",JournalFile,_OSERR);
   if (fread(fileversion,sizeof(fileversion),1,InFile) != 1)
      DoExit("Can't read version from '%s', error %ld",JournalFile,_OSERR);
}


/*
 *  GetEventMemory()
 *
 *  Allocate memory for the Event array (of size MaxEvents, specified by
 *  the EVENT option).
 */

void GetEventMemory()
{
   Event = AllocMem(IE_SIZE * MaxEvents, MEMF_CLEAR);
   if (Event == NULL) DoExit("Can't get memory for %d Events",MaxEvents);
}


/*
 *  GetSignal()
 *
 *  Allocate a signal (error if none available) and set the mask to
 *  the proper value.
 */

void GetSignal(theSignal,theMask)
LONG *theSignal, *theMask;
{
   LONG signal;

   if ((signal = AllocSignal(-ONE)) == -ONE) DoExit("Can't Allocate Signal");
   *theSignal = signal;
   *theMask = (ONE << signal);
}


/*
 *  SetupTask();
 *
 *  Find the task pointer for the main task (so the input handler can 
 *  signal it).  Clear the CTRL signal flags (so we don't get any left
 *  over from before PLAYBACK was run) and allocate a signal for 
 *  when the handler frees an event.
 */

void SetupTask()
{
   theTask = FindTask(NULL);
   SetSignal(0L,SIGBREAKF_ANY);
   GetSignal(&theSignal,&theMask);
   #ifndef MANX
      onbreak(&Ctrl_C);
   #endif
}


/*
 *  AddHandler()
 *
 *  Add the input handler to the Input.Device handler chain.  Since the
 *  priority is 51 it will appear BEFORE intuition, so when we insert
 *  new events into the chain, Intuition will process them just as though
 *  they came from the Input.Device.
 */
 
void AddHandler()
{
   long status;

   if ((InputPort = CreatePort(0,0)) == NULL)
      DoExit("Can't Create Port");
   if ((InputBlock = CreateStdIO(InputPort)) == NULL)
      DoExit("Can't Create Standard IO Block");
   InputDevice = (OpenDevice("input.device",0,InputBlock,0) == 0);
   if (InputDevice == 0) DoExit("Can't Open Input.Device");
   
   InputBlock->io_Command = IND_ADDHANDLER;
   InputBlock->io_Data    = (APTR) &HandlerData;
   if (status = DoIO(InputBlock)) DoExit("Error from DoIO:  %ld",status);
   printf("%s - Press CTRL-C to Cancel\n",version);
   HandlerActive = TRUE;
}

/*
 *  RemoveHandler()
 *
 *  Remove the input handler from the Input.Device handler chain.
 */
 
void RemoveHandler()
{
   long status;

   if (HandlerActive && InputDevice && InputBlock)
   {
      HandlerActive = FALSE;
      InputBlock->io_Command = IND_REMHANDLER;
      InputBlock->io_Data = (APTR) &HandlerData;
      if (status = DoIO(InputBlock)) DoExit("Error from DoIO:  %ld",status);
   }
   printf("Playback Complete\n");
}


/*
 *  Create an event that moves the pointer to the upper, left-hand corner
 *  of the screen so that the pointer is at a known position.  This is a
 *  large relative move (-1000,-1000).
 */

void PointerToHome()
{
   struct InputEvent *theEvent = &(Event[0]);
   
   theEvent->ie_Class     = IECLASS_RAWMOUSE;
   theEvent->ie_Code      = IECODE_NOBUTTON;
   theEvent->ie_Qualifier = IEQUALIFIER_RELATIVEMOUSE;
   theEvent->ie_X         = -1000;
   theEvent->ie_Y         = -1000;
   theEvent->my_Ticks     = 0;
   theEvent->my_Time      = 0;
   theEvent->my_Ready     = READY;
}


/*
 *  CheckForCTRLC()
 *
 *  Read the current task signals (without changing them) and check whether
 *  a CTRL-C has been signalled.  If so, abort the playback.
 */

void CheckForCTRLC()
{
   LONG signals = SetSignal(0,0);
   
   if (signals & SIGBREAKF_CTRL_C) DoExit("Playback Aborted");
}


/*
 *  GetNextFree()
 *
 *  Set NextFree to point to the next free event in the Event array.
 *  If there are no free events, Wait() for the handler to signal that it
 *  has freed one (or for CTRL-C to be pressed).
 */

void GetNextFree()
{
   LONG signals;

   NextFree = (NextFree + 1) % MaxEvents;
   while (Event[NextFree].my_InUse)
   {
      signals = Wait(theMask | SIGBREAKF_CTRL_C);
      if (signals & SIGBREAKF_CTRL_C) DoExit("Playback Aborted");
   }
}


#define ABS(x)  (((x)<0)?-(x):(x))


/*
 *  MovePointer()
 *
 *  Interpolate mouse move events that were compressed into one record in
 *  the journal file.  The se_Count field holds the number of events that
 *  were compressed into one.
 *
 *  First, unpack the X and Y movements.  Record their directions in dx and dy
 *  and their magnitudes in abs_x and abs_y.  Reduce 'count' if there would
 *  be events with offset of (0,0).  'x_move' specifies the x-offset for each
 *  event and 'x_add' specifies the fraction of a pixel correction that must
 *  be made (x_add/count is the fraction).  'x_count' counts the fraction of
 *  a pixel that has been added so far (when x_count/count >= 1 (i.e., when
 *  x_count >= count) we add another pixel to the x-offset).  Similarly for
 *  the y and t variables (t is for ticks).  Starting the counts at 'count/2' 
 *  makes for smoother movement.
 *
 *  Once these are set up, we create new mouse move events with the proper
 *  offsets, adding up the fractions of pixels and adding in addional 
 *  movements whenever the fractions add up to a whole pixel (or tick).
 *  When a new event is set up, we mark it as READY so that the handler will
 *  see it and post it.
 *
 *  Once we have sent all the events, the mouse should be in the proper
 *  position, so we set the tick count and XY-offset fields to 0.
 */

void MovePointer()
{
   WORD abs_x,abs_y, x_count,y_count, dx,dy, x_add,y_add, x_move,y_move;
   WORD t_count, t_add, t_move;
   WORD x = TinyEvent.se_XY & 0xFFF;
   WORD y = (TinyEvent.se_XY >> 12) & 0xFFF;
   WORD i, count = TinyEvent.se_Count & COUNTMASK;
   LONG Time = TinyEvent.se_Micros & 0xFFFFF;
   LONG Ticks = TinyEvent.se_Ticks >> 20;
   struct InputEvent *NewEvent;

   x_count = y_count = t_count = 0;
   if (x & 0x800) x |= 0xF000;
   if (x < 0) dx = -1; else dx = 1;
   if (y & 0x800) y |= 0xF000;
   if (y < 0) dy = -1; else dy = 1;
   abs_x = ABS(x); abs_y = ABS(y);
   if (abs_x > abs_y)
   {
      if (count > abs_x) count = abs_x;
   } else {
      if (count > abs_y) count = abs_y;
   }
   if (count)
   {
      x_move = x / count; y_move = y / count; t_move = Ticks / count;
      x_add = abs_x % count; y_add = abs_y % count; t_add = Ticks % count;
   } else {
      x_move = x; y_move = y; t_move = Ticks;
      x_add = y_add = t_add = -1; count = 1;
   }
   x_count = y_count = t_count = count / 2;
   for (i = count; i > 0; i--)
   {
      GetNextFree();
      NewEvent = &Event[NextFree];
      NewEvent->ie_Class     = IECLASS_RAWMOUSE;
      NewEvent->ie_Code      = IECODE_NOBUTTON;
      NewEvent->ie_Qualifier = TinyEvent.se_Qualifier;
      NewEvent->ie_X         = x_move;
      NewEvent->ie_Y         = y_move;
      NewEvent->my_Ticks     = t_move;
      NewEvent->my_Time      = Time;
      if ((x_count += x_add) >= count)
      {
         x_count -= count;
         NewEvent->ie_X += dx;
      }
      if ((y_count += y_add) >= count)
      {
         y_count -= count;
         NewEvent->ie_Y += dy;
      }
      if ((t_count += t_add) > count)
      {
         t_count -= count;
         NewEvent->my_Ticks++;
      }
      NewEvent->my_Ready = READY;
   }
   TinyEvent.se_XY    &= 0xFF000000;
   TinyEvent.se_Ticks &= 0xFFFFF;
}


/*
 *  PostNextEvent()
 *
 *  Read an event from the journal file.  If we are smoothing and the 
 *  event is a mouse movement, them interpolate the compressed mouse
 *  movements.  Get the next event in the Event array and unpack the
 *  proper values from the TinyEvent read from the file.  Mark the finished
 *  event as READY so the handler will see it and post it.
 */

void PostNextEvent()
{
   struct InputEvent *NewEvent = NULL;

   if (fread((char *)&TinyEvent,sizeof(TinyEvent),1,InFile) == 1)
   {
      if (Smoothing && TinyEvent.se_Type == MOUSEMOVE) MovePointer();

      GetNextFree();
      NewEvent = &Event[NextFree];
   
      NewEvent->ie_Class     = TinyEvent.se_Type & 0x1F;
      NewEvent->ie_Qualifier = TinyEvent.se_Qualifier;
      NewEvent->my_Ticks     = TinyEvent.se_Ticks >> 20;
      NewEvent->my_Time      = TinyEvent.se_Micros & 0xFFFFF;
      
      switch(NewEvent->ie_Class)
      {
         case IECLASS_RAWKEY:
            NewEvent->ie_Code = TinyEvent.se_Code;
            NewEvent->ie_X = NewEvent->ie_Y = TinyEvent.se_Prev;
            break;

         case IECLASS_RAWMOUSE:
            NewEvent->ie_Code = (TinyEvent.se_Type >> 5) & 0x03;
            if (NewEvent->ie_Code == 0x03)
               NewEvent->ie_Code = IECODE_NOBUTTON;
              else
               NewEvent->ie_Code |= (TinyEvent.se_Type & IECODE_UP_PREFIX) |
                  (IECODE_LBUTTON & ~(0x03 | IECODE_UP_PREFIX));
            NewEvent->ie_X = TinyEvent.se_XY & 0xFFF;
            NewEvent->ie_Y = (TinyEvent.se_XY >> 12) & 0xFFF;
            NewEvent->my_Ticks = TinyEvent.se_Ticks >> 20;
            if (NewEvent->ie_X & 0x800) NewEvent->ie_X |= 0xF000;
            if (NewEvent->ie_Y & 0x800) NewEvent->ie_Y |= 0xF000;
            break;

         default:
            printf("[ Unknown Event Class:  %02X]\n",NewEvent->ie_Class);
            break;
      }
      NewEvent->my_Ready = READY;
   }
}


/*
 *  WaitForEvents()
 *
 *  Wait for the handler to finish posting all the events in the Event
 *  array (so we don't remove the handler before it is done).
 */

void WaitForEvents()
{
   short LastFree = NextFree;
   
   do GetNextFree(); while (NextFree != LastFree);
}


/*
 *  PlayJournal()
 *
 *  Open the journal file, set up the task and signals, and allocate the
 *  Event array.  Add the input handler and send the pointer to the upper,
 *  left-hand corner of the screen.  While there are still events in the
 *  journal file, check whether the user wants to cancel the playback and
 *  if not, post the next event in the file.  When the end-of-file is reached
 *  wait for the handler to finish posting all the events in the array, and 
 *  then remove the handler.
 */

void PlayJournal()
{
   OpenJournal();
   SetupTask();
   GetEventMemory();

   AddHandler(&myHandler);
   PointerToHome();

   while (feof(InFile) == 0)
   {
      CheckForCTRLC();
      PostNextEvent();
   }
   
   WaitForEvents();
   RemoveHandler();
}


/*
 *  main()
 *
 *  Parse the command-line arguments, and perform the proper function
 *  (either show the usage, read a journal, or fall through and exit).
 */

void main(argc,argv)
int argc;
char **argv;
{
   switch(ParseArguments(argc,argv))
   {
      case SHOW_USAGE:
         printf("Usage:  %s\n",USAGE);
         break;

      case READ_JOURNAL:
         PlayJournal();
         break;
   }
   DoExit(NULL);
}
SHAR_EOF
cat << \SHAR_EOF > journal.h
/*
 *  JOURNAL.H  -  Common header file for JOURNAL.C and PLAYBACK.C
 *
 *             Copyright (c) 1987 by Davide P. Cervone
 *  You may use this code provided this copyright notice is kept intact.
 */

#include <libraries/dos.h>
#include <exec/io.h>
#include <exec/interrupts.h>
#include <exec/memory.h>
#include <devices/input.h>
#include <devices/inputevent.h>
#include <stdio.h>

#define ONE         1L

extern struct MsgPort *CreatePort();
extern struct IOStdReq *CreateStdIO();
extern LONG AllocSignal(), Wait(), SetSignal();
extern struct Task *FindTask();
extern struct InputEvent *AllocMem();
extern FILE *fopen();
extern long errno, _OSERR;              /* Lattice and DOS error numbers */

extern void RemoveHandler();            /* defined later on */

/*
 *  assembly routine that gets called by the Input.Device which sets up
 *  the stack and calls our input handler
 */
extern void myHandlerStub();


/*
 *  Structure used to pack event data into a small space.  This is the 
 *  format used to record that data in the journal file */

struct SmallEvent
{
   union
   {
      struct
      {
         UBYTE se_IDType;   /* ie_Class and ie_Code combined */
         UBYTE se_Raw;      /* RawKey Code */
         UWORD se_PrevChar; /* previous key and qualifier */
      } se_ID;
      ULONG se_XYpos;       /* X in bits 0-11, Y in 12-23 (ie_Type in 24-31) */
   } se_Long1;
   UWORD se_Qualifier;
   ULONG se_Long2;          /* Micros in 0-19, Ticks in 20-32 */
};
#define se_Type   se_Long1.se_ID.se_IDType
#define se_Code   se_Long1.se_ID.se_Raw
#define se_Prev   se_Long1.se_ID.se_PrevChar
#define se_XY     se_Long1.se_XYpos
#define se_Ticks  se_Long2
#define se_Micros se_Long2
#define se_Count  se_Long2

/*
 *  Some shorthands for InputEvent fields
 */
#define my_Prev   ie_X
#define my_Time   ie_Secs                   /* micros since last event */
#define my_Ticks  ie_Mics                   /* ticks since last event */
#define my_Ready  ie_NextEvent              /* TRUE when it can be posted */
#define my_InUse  ie_Class                  /* TRUE when it is in use */
#define my_Saved  ie_SubClass               /* TRUE if is has been recorded */
#define my_Count  my_Time                   /* number of compressed events */
#define ie_Secs   ie_TimeStamp.tv_secs
#define ie_Mics   ie_TimeStamp.tv_micro

#define COUNTMASK 0x3F                      /* how much of my_Count is count */
#define READY     ((struct InputEvent *) TRUE)
#define TIME      (theEvent->ie_Mics-TimerMics)
#define MILLION   1000000

#define XMINMOUSE      xminmove
#define YMINMOUSE      yminmove
#define XDEFMIN        8                    /* default DX */
#define YDEFMIN        8                    /* default DY */
#define LONGTIME       8                    /* if this many ticks occur, we */
                                            /*   write out a dummy move to */
                                            /*   record the pause */    

#define IE_SIZE        sizeof(struct InputEvent)
#define NEWEVENT       AllocMem(IE_SIZE,0)
#define FREEVENT(ev)   FreeMem(ev,IE_SIZE)

#define SIGBREAKF_ANY  (SIGBREAKF_CTRL_C | SIGBREAKF_CTRL_D |\
                        SIGBREAKF_CTRL_E | SIGBREAKF_CTRL_F)
SHAR_EOF
cat << \SHAR_EOF > handlerstub.a
        CSECT   text

        XREF    _myHandler
        XDEF    _myHandlerStub
        
_myHandlerStub:
        MOVEM.L A0/A1,-(A7)
        JSR     _myHandler
        ADDQ.L  #8,A7
        RTS
        
        END
SHAR_EOF
cat << \SHAR_EOF > journal.lnk
FROM LIB:c.o+journal.o+handlerstub.o
TO journal
LIB LIB:lc.lib+LIB:amiga.lib
NODEBUG
SHAR_EOF
cat << \SHAR_EOF > playback.lnk
FROM LIB:c.o+playback.o+handlerstub.o
TO playback
LIB LIB:lc.lib+LIB:amiga.lib
NODEBUG
SHAR_EOF
#	End of shell archive
exit 0