GameSource/GameAEvents.c

#include "GameAEvents.h"
#include "ZAM.h"
#include "WindowDispatch.h"
#include "TankSprite.h"
#include "MissileSprite.h"
#include "CoreGlobals.h"
#include "ZAMProtos.h"
 
#define rNotifySICN 128
#define rWakeUpSND  128
#define kDiamondMark 1
#define rReqGameAlert 130
#define AcceptBtnItem 1
 
 
 
 
pascal OSErr AERequestGame ( AppleEvent *theAE, AppleEvent *reply, long rfCon);
pascal OSErr AEAcceptGame ( AppleEvent *theAE, AppleEvent *reply, long rfCon);
pascal OSErr AERefuseGame ( AppleEvent *theAE, AppleEvent *reply, long rfCon);
pascal OSErr AEAnswer (AppleEvent *theAE, AppleEvent *reply, long rfCon);
pascal OSErr AEGoodbye (AppleEvent *theAE, AppleEvent *reply, long rfCon);
pascal OSErr AEAckTime(AppleEvent *theAE, AppleEvent *reply, long rfCon);
pascal OSErr AESynchTank ( AppleEvent *theAE, AppleEvent *reply, long rfCon);
 
Boolean gByeNeeded;
long    gLastSynchTime;
long    gLocalTime;
long    gLastReturnTime;
 
 
void InstallCustomEvents(gamePtr game)
/*
    This installs the apple event handlers used by the game.
    
*/
{
    OSErr           err = noErr;
 
    gByeNeeded = false;
    gLastSynchTime = 0;
    gLastReturnTime = 0;
    gLocalTime = 0;
 
    err = AEInstallEventHandler (kCoreEventClass, kAEAnswer, AEAnswer, (long)game, false);
    if(err) {
        ErrMsgCode("\pCould not install AE handler.",err);
        ExitToShell();
    }
 
    err = AEInstallEventHandler (kZAMEventClass, kRequestGameID, AERequestGame,(long)game, false);
    if(err) {
        ErrMsgCode("\pCould not install AE handler.",err);
        ExitToShell();
    }
 
    err = AEInstallEventHandler (kZAMEventClass, kGoodByeID, AEGoodbye,(long)game, false);
    if(err) {
        ErrMsgCode("\pCould not install AE handler.",err);
        ExitToShell();
    }
 
    err = AEInstallEventHandler (kZAMEventClass, kTankSynchID, AESynchTank, (long)game, false);
    if(err) {
        ErrMsgCode("\pCould not install AE handler.",err);
        ExitToShell();
    }
}
 
 
void SendGoodBye(void)
/*
    When quit is selected it calls this function, which sends a message to the other player,
    letting them know you are no longer around.
    
    It would be nice if the quit handler displayed even a simple dialog,
    but this was a pain for my testing, so I made it just quit.  Of course a real
    application would not behave this way.
*/
{
    AppleEvent      goodByeEvt;
    AppleEvent      reply;
    OSErr           err = noErr;
    Boolean         disposeNeeded = false;
 
#ifdef NO_NET
    return;
#endif
    
    if(gByeNeeded) {        
        err = AECreateAppleEvent(kZAMEventClass, kGoodByeID, &gGame->oppAddr,
                     kAnyTransactionID, gGame->gameID, &goodByeEvt);
        if(err != noErr) {
            ErrMsgCode("\p Failure: SendGoodBye AECreateAppleEvent",err);
        }
            
        if(err == noErr) {
            disposeNeeded = true;
            err = AESend(&goodByeEvt, &reply, kAENoReply + kAECanInteract,
                     kAEHighPriority, 60 * 60, nil, nil);
            if(err != noErr) {
                ErrMsgCode("\p SendGoodBye: AESend goodByeEvt",err);
            }
        }
        
        
        if(disposeNeeded) {
            AEDisposeDesc(&goodByeEvt);
        }
    }
}
 
pascal OSErr AEGoodbye (AppleEvent *theAE, AppleEvent *reply, long rfCon)
/*
    This is the handler for when the remote mac has quit.
    It does not display any warning dialog box, because as I mentioned before,
    in a code-build-test cycle, it was too much to be navigating dialogs and stuff.
    So, it just causes the program to quit without warning.
*/
{
    gDone = true;
    gByeNeeded = false;
}
 
pascal OSErr AEAnswer(AppleEvent *theAE, AppleEvent *reply, long rfCon)
/*
    This handler is used for the apple events that return a result asynchronously.
    The type of reply is stored in the keyReturnIDAttr.  The only events that require
    a reply are the request game event, and the synch event.  Another synch event will
    not be sent until an ack is received.
*/
{
    long            actType,actSize;
    short           retID;
    OSErr           err= noErr;
    
    
    err = AEGetAttributePtr(theAE, keyReturnIDAttr,  typeShortInteger, &actType,
                            &retID, sizeof(short), &actSize);
    if(err != noErr) {
        ErrMsgCode("\pAEAnswer: AEGetAttr keyReturnIDAttr",err);
    }
 
    if(err == noErr) {
        switch(retID) {
            case    kAcceptID:  err = AEAcceptGame(theAE, reply, rfCon);
            break;
            
            case    kTimeID:    err = AEAckTime(theAE, reply, rfCon);
            break;
            
            default:    ErrMsgCode("\pUnknown Return ID.",retID);
            break;
        }       
    }
    
    return err;
}
 
 
pascal OSErr AEAckTime(AppleEvent *theAE, AppleEvent *reply, long rfCon)
/*
    just grab the time so the synch event will not flood the wire with
    unhandled events.  This is not a REAL appleevent handler, it just
    looks like one, because it was easier that way.  This is actually called
    by the return event handler, above.
*/
{
    OSErr       err = 0;
    long        len;
    DescType    actualType;
    long        synchTime;
 
    err = AEGetParamPtr(theAE, keySynchTime, typeLongInteger, &actualType, 
            &gLastReturnTime, sizeof(long), &len);
            
    if(err != noErr) {
        ErrMsgCode("\p Failed: AEAckTime keySynchTime, typeLongInteger",err);
    }
 
    return err;
}
 
pascal OSErr AEAcceptGame(AppleEvent *theAE, AppleEvent *reply, long rfCon)
/*
    This event comes back when the other player accepts the game.  Any ZAM game will
    accept if it is not already playing.  I used to have code to allow you to type in
    a name, and request a game, and the other player would be able to decide if they
    wanted to or not.  Again, this really hampered my code-compile-test cycle, so I stripped
    or commented it out.  You could probably revive it if you wanted to.
    
    This is not a REAL appleevent handler, it just
    looks like one, because it was easier that way.  This is actually called
    by the return event handler, above.
*/
{
    OSErr           err = noErr;
    long            actualType,longSize;
    Boolean         acceptFlag;
 
    
    if(gGame == nil) {
        ErrMsg("\pReceived accept when never made window");
        err = paramErr;
    }
        
    if(gGame->gameState == kWaitingForAccept) {
        err = AEGetParamPtr(theAE, keyAnswer, typeBoolean, &actualType, 
                &acceptFlag, sizeof(Boolean), &longSize);
        if(err != noErr) {
            ErrMsgCode("\p Failed: AEAcceptGame AEGetParamPtr typeBoolean",err);
        }
        
        if(err == noErr)
            if(acceptFlag) {
                gGame->gameState = kGameInProgress;
                /* Set up tank indexes here */
                gGame->localTankIndex = 0;
                gGame->remoteTankIndex = 1;
                PlaceTankSprites(gGame);
                gByeNeeded = true;
            } else {
                /* display rejection notice */
                ParamText("\pThat person has refused your challenge. Try another.",nil,nil,nil);
                (void) Alert(131,nil);      // evil numbers must go
                gGame->gameState = kWaitingForRequest;
            }
    }
        
    return err;
}
 
 
pascal OSErr AERequestGame( AppleEvent *theAE, AppleEvent *reply, long rfCon)
/*
    This BIG ugly function handles requests for a game from another player
    and starts things rolling.  It changes the game state flag, but
    this flag is mostly ignored by the rest of the program.  At one time
    I was using it, but I was not setting it properly all the time, and there
    were more important bugs to fix so I just crippled it.
    
    
*/
{
    OSErr           err = noErr;
    short           itemHit;
    DescType        returnedType;
    Size            length;
    long            replyID;
    Str255          nameStr;
    AEAddressDesc   targetAddress;
    AppleEvent      replyEvent;
    Boolean         acceptFlag = true;
    ulong           tranID;
 
    if(err == noErr) {
        if(gGame->gameState != kWaitingForRequest) {
            /* just automagically refuse the game cause we are busy */
            acceptFlag = false;
            err = AEPutParamPtr(reply, keyAnswer, typeBoolean, &acceptFlag, sizeof(Boolean));
            if(err != noErr)  {
                ErrMsgCode("\p AERequestGame: AEPutParamPtr keyAnswer FALSE",err);
            }
        } else {
        
            /* get the address of the person requesting we play */
            if(err == noErr) {
                err = AEGetAttributeDesc ( theAE, keyAddressAttr, typeWildCard, &targetAddress);
                if(err != noErr) {
                    ErrMsgCode("\p AERequestGame: AEGetAttr keyAddressAttr",err);
                }
            }
            
            if(err == noErr) {
                err = AEGetAttributePtr(theAE, keyTransactionIDAttr, typeLongInteger, &returnedType,
                                    &tranID, sizeof(long), &length);
                if(err != noErr) {
                    ErrMsgCode("\p AEGetAttr keyTransactionIDAttr",err);
                }           
            }
            
            if(err == noErr) {
                /* ask user if they want to play */
                /* NOTE:  this is where I used to display the dialog asking */
                /* if a game was wanted or not */
                /* now it always accepts */
                    acceptFlag = true;
                    if(err == noErr) {
                        gGame->oppAddr = targetAddress;
                        gGame->gameID = tranID;
                        gGame->localTankIndex = 1;
                        gGame->remoteTankIndex = 0;
                        gGame->gameState = kGameInProgress;
                        PlaceTankSprites(gGame);
                        gByeNeeded = true;
                    }
                    else if(err == paramErr) {
                        acceptFlag = false;
                        err = noErr;
                    }
    
                if(err == noErr) {
                    err = AEPutParamPtr(reply, keyAnswer, typeBoolean, &acceptFlag, sizeof(Boolean));
                    if(err != noErr)  {
                        ErrMsgCode("\p AERequestGame: AEPutParamPtr keyAnswer",err);
                    }                   
                }
            }
        }
    }                   
    return err;
}
 
 
pascal OSErr AESynchTank ( AppleEvent *theAE, AppleEvent *reply, long rfCon)
/*
    This is the network heartbeat of the game.  It recieves the state of the remote machine.
    
    It does some things with AppleEvents for speed
    reasons that a normal application would not want to do.  I send large structures
    and arrays, instead of building platform independant AEDescLists and that sort of thing.
    I send the current state of the tank, and the current state of all the missiles (from
    a subroutine call, see the code).
*/
{
 
    OSErr       err = 0;
    long        len;
    DescType    actualType;
    long        synchTime;
    TankStatus  tStatus;
 
    /* first thing is to get the synch time to see if this one 
        should be ignored.  The synch time is saved off each time
        so the network is not flooded with synch events.
        It is very easy to flood a network with these things.
        Sometimes a mac may be busy and miss a few events, and then when it gets back
        to it, it caused a hyper-spurt of animation while the mac caught up
        and processed the apple events.  This was undesireable for my sample, so
        I implemented this strategy of synching */
         
    if(err == noErr) {
        /* get the synch time to see if we should ignore this or not*/
        err = AEGetParamPtr(theAE, keySynchTime, typeLongInteger, &actualType, 
                &synchTime, sizeof(long), &len);
        if(err != noErr) {
            ErrMsgCode("\p Failed: AESynchTank keyTankPosition, typefixPt",err);
        }
    }
    
    if(err == noErr) {
        /* send the synch time back */
        err = AEPutParamPtr(reply, keySynchTime, typeLongInteger, 
                            &synchTime, sizeof(long));
        if(err != noErr) {
            ErrMsgCode("\p Failure: AESynchTank AEPutParamPtr reply",err);
        }
    }
    
    if(err == noErr) {
        if(gLastSynchTime > synchTime)
            err = 1;
        else
            gLastSynchTime = synchTime;
    }
 
    /*
        this gets the status of the tank from the event */
    if(err == noErr) {
        err = AEGetParamPtr(theAE, keyTankStatus, typeTankStatus, &actualType, 
                &tStatus, sizeof(TankStatus), &len);
        if(err != noErr) {
            ErrMsgCode("\p Failed: AESynchTank keyTankStatus typeTankStatus",err);
        }
    }
    
    /* if we got the status of the tank ok, then we call the Synch. Tank function
        (See TankSprites.c) to make sure the tank is in the right place doin the right
        thing.
        
        After that, the remote missiles are updated by passing the whole applevent
        to  MissileSprites.c via ProcessMissilePositions.
    */
    if(err == noErr) {
        SynchronizeTank(gGame, &tStatus.position, tStatus.direction, tStatus.speed);
        err = ProcessMissilePositions(theAE);
    }
 
    /* eat the synch time errors */
    if(err == 1) err = noErr;
 
    return err;
}
 
 
 
 
Boolean TankSynchTask(xthing *xtp, spritePtr spr)
/*
    This is a timed task managed by the xthing time manager.
    It sends the state of the tank and the state of the missiles
    to the remote mac.
    
    See the notes above about abusing and sending structures using AppleEvents.
    
    Good.  Now that you have read that, I'll elaborate more.
    One of the key concepts with scriptable AppleEvent programs is to
    use tagged data so that any application can determine the format of
    parameters and send them along in a happy manner.  This may require many
    calls to AEPutParamPtr and such, which may also cause the AppleEvent record
    to grow multiple times, and can cause a slow down for a real time application
    like this.  So, to minimize this, I kind go against the grain of AppleEvents and
    just send a big block of data.  The good design thing for me to have done, which I
    didn't, would have been to isolate the network code a little more, so that any layer could
    be plugged in.
*/
{
    OSErr           err = noErr;
    AppleEvent      tankSynchEvent;
    AppleEvent      reply;
    tankInfoRec     *tInfo;
    Boolean         disposeNeeded = false;
    long            tickTime;
    TankStatus      tStatus;
    
    if(gLastReturnTime != gLocalTime)  return true;
 
    tInfo = (tankInfoRec*)spr->refCon;
    
    err = AECreateAppleEvent(kZAMEventClass, kTankSynchID, &gGame->oppAddr,
                 kTimeID, gGame->gameID, &tankSynchEvent);
    if(err != noErr) {
        ErrMsgCode("\p Failure: FireNetworkMissile AECreateAppleEvent",err);
    }
    
    
    /*
        This is where the time stamp is incremented, and placed into the appleevnet.
        This is sent back in the reply event */
        
    if(err == noErr) {
        disposeNeeded = true;
        gLocalTime++;
        err = AEPutParamPtr(&tankSynchEvent, keySynchTime, typeLongInteger, 
                            &gLocalTime, sizeof(long));
        if(err != noErr) {
            ErrMsgCode("\p Failure: AEPutParamPtr",err);
        }
    }
 
    /* load up the tank info and stuff it into the event */
    tStatus.position = spr->loc;
    tStatus.direction = tInfo->dir;
    tStatus.speed = tInfo->speed;
    
    if(err == noErr) {
        err = AEPutParamPtr(&tankSynchEvent, keyTankStatus, typeTankStatus, 
                            &tStatus, sizeof(TankStatus));
        if(err != noErr) {
            ErrMsgCode("\p Failure: AEPutParamPtr keyTankStatus, typeTankStatus",err);
        }
    }
    
    /* getting a little more structured here, I call out to add the missile information */
    /* to the event.  CAn you tell that I did the tank stuff first, and missile stuff
       later? */
    if(err == noErr) {
        err = AppendMissilePositions(&tankSynchEvent);
        if(err != noErr) {
            ErrMsgCode("\pError appending missile positions.",err);
        }
    }
 
    if(err == noErr) {
        err = AESend(&tankSynchEvent, &reply, kAEQueueReply + kAECanInteract,
                 kAEHighPriority, kNoTimeOut, nil, nil);
        if(err != noErr) {
            ErrMsgCode("\p Failure: AESend",err);
        }
    }
 
    if(disposeNeeded) {
        AEDisposeDesc(&tankSynchEvent);
    }
 
    return true;
}