/* * 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); }