Relay-Version: version B 2.10 5/3/83; site utzoo.UUCP
Path: utzoo!mnetor!uunet!seismo!husc6!mit-eddie!ll-xn!cit-vax!ucla-cs!zen!ucbvax!s.cc.purdue.edu!dpvc
From: dpvc@ur-tut.UUCP (Davide P. Cervone)
Newsgroups: comp.sources.amiga
Subject: Journal and Playback (sources)
Message-ID: <486@s.cc.purdue.edu>
Date: Fri, 10-Jul-87 14:58:56 EDT
Article-I.D.: s.486
Posted: Fri Jul 10 14:58:56 1987
Date-Received: Sun, 12-Jul-87 12:56:38 EDT
Expires: Fri, 7-Aug-87 09:34:41 EDT
Sender: doc@s.cc.purdue.edu
Reply-To: doc@s.cc.purdue.edu (Craig Norborg)
Distribution: world
Organization: Purdue University Computing Center
Lines: 1498
Approved: doc@s.cc.purdue.edu


    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 
#include 
#include 
#include 
#include 
#include 
#include 

#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