home *** CD-ROM | disk | FTP | other *** search
- // HighScoreServer.m
- // under development; anything with ***** is yet to be completed
-
- // This class is the actual high score server for any given game.
- // You shouldn't need to subclass it: Just change the class of
- // HighScoreSlot used and change the GameInfo object's parameters.
-
- // I need to do exception handling in here to catch any DO problems,
- // but haven't got around to it yet, so exceptions will currently crash
- // the server. :-< *****
-
- // Note that a game with multiple tables simply uses multiple servers,
- // each with a different name. (For example: NX_Invaders.easy,
- // NX_Invaders.hard would be a possibility. The HighScoreController in
- // the game itself handles all the coordination between tables; by
- // default they are numbered.)
-
- // In the future it would be nice to allow the game to send a .o file
- // over the connection for any high score slots the server can't deal
- // with. This would allow the server to truly dynamically update itself!
- // Right now, though, there's not an easy way to do this and I don't have
- // the time to take care of it, since other parts of the GameKit need more
- // attention. The method frameworks are in the protocol, though.
-
- #import <appkit/appkit.h>
- #import <daymisckit/daymisckit.h>
- #import <gamekit/gamekit.h>
- #import <remote/NXProxy.h>
- #import <objc/objc-runtime.h>
- #import <objc/objc-load.h>
-
- // this is where the files are stored. Each file is named
- // using the convention "<game's name>.highscores" The files
- // themselves are all typed streams with archived GameInfo and
- // HighScoreTable objects in them. Override the -pathToTables
- // method to change this.
- #define PATH_TO_TABLES "/usr/local/games/highscores/"
-
- static BOOL loggingIsOn; // a flag to turn all logging on or
- // off for all servers in an application
- static id logFile;
- // define DEBUGLOG for even more logging...
-
- @implementation HighScoreServer
-
- + turnLoggingOn:(BOOL)flag
- {
- loggingIsOn = flag;
- if (loggingIsOn) { // make new log file when turning on logging
- // (This allows us to change the name of the file by toggling
- // the on and off. A subclass would have to implement +makeLogFile
- // in order to actually do something like this.)
- if (logFile) [logFile free];
- logFile = [self makeLogFile];
- }
- return self;
- }
-
- + makeLogFile // override to change where loggin goes to.
- { // Just return a DAYLogFile instance!
- id fileName = [[DAYString newWithString:PATH_TO_TABLES] cat:"log"];
- // I don't use an attendant lock file since there should only
- // ever be one server after the file -- note that I'm intending
- // this facility for remote servers, not local. If you are going
- // to do logging from a local game, you _better_ give it a lock file!!!
- id newLog = [[[DAYLogFile alloc] init] setFileName:fileName];
- return newLog;
- }
-
- - init { return [self error:"Use -initForGame: (not -init)"]; }
- - initForGame:(const char *)name // designated initializer
- {
- [super init];
- if (!name) { // must give us a name or we'll barf.
- #ifdef DEBUGLOG
- if (loggingIsOn) {
- id tempString = [DAYString
- newWithString:"An attempt was made to start a NULL server."];
- [logFile addLineToLogFile:tempString];
- [tempString free];
- }
- #endif
- fprintf(stderr, "HighScoreServer error: can't init for null game.\n");
- [self free];
- return nil; // note that nothing is returned...
- }
- // find file that holds the high score file
- // if non-existent, create an empty table, otherwise we load it in
- gameName = [[DAYString alloc] initString:name];
- if (loggingIsOn) {
- id tempString = [[gameName copy] cat:": Starting server.\n"];
- [logFile addLineToLogFile:tempString];
- [tempString free];
- }
- clientList = [[List alloc] initCount:0];
- connList = [[List alloc] initCount:0];
- clientAuth = [[Storage alloc]
- initCount:0 elementSize:sizeof(char) description:"c"];
- [[[GameInfo alloc] init] free]; // forces class to be linked into serverd
- // without the need for the ld flag that links in the whole library...
- gameInfo = nil; // we'll force the first client to check in to send it
- // build the name of the file where we store the highscores
- scoreFile = [[DAYString alloc] initString:[self pathToTables]];
- [scoreFile cat:"/"];
- [scoreFile concatenate:gameName];
- [scoreFile cat:".highscores"];
- [self load]; // load in a highscore table
- return self;
- }
-
- - (const char *)pathToTables
- { // subclass can override this to customize.
- return PATH_TO_TABLES;
- }
-
- - (oneway)addSlotCode:(bycopy in id)code // not yet implemented
- { // *****
- if (loggingIsOn) {
- id tempString = [[gameName copy]
- cat:": Slot code sent to server.\n"];
- [logFile addLineToLogFile:tempString];
- [tempString free];
- }
- return self;
- }
-
- - (oneway)setGameInfo:(bycopy in id)info
- {
- id emptySlotClass;
- const char *slotClass = [[info slotType] stringValue];
-
- // avoid freeing GameInfo if in a game and not a remote server
- if (![NXApp delegate]) if (gameInfo) [gameInfo free];
- gameInfo = info; // we now have a gameInfo object that tells us
- // how to do things. Without this, we can only function according
- // to the gamekit defaults.
- emptySlotClass = objc_lookUpClass(slotClass);
- if (!emptySlotClass) { // can't find the class, so try and load it
- long ret;
- char *fileName = (char *)malloc(strlen(slotClass) +
- strlen([self pathToTables]) + 3);
- char *fileNames[2] = {fileName, NULL};
- sprintf(fileName, "%s%s.o", [self pathToTables], slotClass);
- ret = objc_loadModules(fileNames, NULL, NULL, NULL, NULL);
- emptySlotClass = objc_lookUpClass(slotClass);
- if (ret || !emptySlotClass) {
- // couldn't load the class so tell the clients we can't help.
- [clientList makeObjectsPerform:@selector(cantBeServed:)
- with:gameName];
- if (![NXApp delegate]) [gameInfo free];
- gameInfo = nil; // assume we're still uninitted.
- return self;
- }
- }
- [table setEmptySlotClass:emptySlotClass];
- [table setMaxHighScores:[gameInfo maxHighScores]];
- // ***** need to figure out which table we are so that this works right
- [table setMaxScoresPerPlayer:[gameInfo maxScoresPerPlayerTable:0 net:YES]];
- #ifdef NOISYDEBUG
- fprintf(stderr, "HighScoreServer: maxScorePerPlayer is %d.\n",
- [gameInfo maxScoresPerPlayerTable:0 net:YES]);
- #endif
- if (loggingIsOn) {
- id tempString = [[gameName copy] cat:": Got GameInfo object.\n"];
- [logFile addLineToLogFile:tempString];
- [gameInfo dumpToLog:logFile];
- [tempString free];
- }
- [self save]; // so even if no slot is sent we at least have GameInfo saved.
- return self;
- }
-
- - (oneway)setTemplate:(bycopy in id)newTemplate
- {
- if (template) [template free];
- template = newTemplate;
- if (haveNonTemplateTable) {
- int i;
- haveNonTemplateTable = NO;
- [self _makeTableRatherThanLoad];
- for (i=0; i<[clientList count]; i++)
- [[clientList objectAt:i] acceptTable:table name:gameName];
- }
- if (loggingIsOn) {
- id tempString = [[gameName copy]
- cat:": Got table template object.\n"];
- [logFile addLineToLogFile:tempString];
- [tempString free];
- }
- return self;
- }
-
- - free
- {
- // free our private strings
- [scoreFile free];
- [gameName free];
- // free all high score slots and tables
- [[table freeObjects] free];
- // free other items (internal params)
- [clientList free];
- [clientAuth free];
- [connList free];
- if (![NXApp delegate]) [gameInfo free];
- return [super free];
- }
-
- // methods that the client can call
- - (oneway)clientDying:(in id <HighScoreClient>)client
- // alerts server that a client is going away
- {
- // remove client from the list
- unsigned num = [clientList indexOf:client];
- while (num != NX_NOT_IN_LIST) {
- [clientList removeObject:client];
- [connList removeObjectAt:num];
- [clientAuth removeElementAt:num];
- num = [clientList indexOf:client];
- }
- #ifdef DEBUGLOG
- if (loggingIsOn) {
- id tempString = [[gameName copy] cat:": A client left.\n"];
- [logFile addLineToLogFile:tempString];
- [gameInfo dumpToLog:logFile];
- [tempString free];
- }
- #endif
- return self;
- }
-
- - senderIsInvalid:sender
- {
- int i;
- BOOL changed = YES;
- while (changed) {
- changed = NO;
- for (i=0; i<[connList count]; i++) {
- if (sender == [connList objectAt:i]) {
- [clientList removeObjectAt:i];
- [connList removeObjectAt:i];
- [clientAuth removeElementAt:i];
- changed = YES;
- break; // for loop -- have to restart iteration, since
- // list object has been changed now, but we also
- // want to be sure that we remove multiple pointers
- // to dead clients if they exist, hence the while loop
- } } }
- #ifdef DEBUGLOG
- if (loggingIsOn) {
- id tempString = [[gameName copy] cat:": A client died.\n"];
- [logFile addLineToLogFile:tempString];
- [gameInfo dumpToLog:logFile];
- [tempString free];
- }
- #endif
- return self;
- }
-
- - (oneway)clientCheckIn:(in id <HighScoreClient>)client
- // new client alerts of presence so that server
- // can notify client of changes in the table
- {
- BOOL *auth = (BOOL *)malloc(sizeof(BOOL));
- NXConnection *conn;
- *auth = NO;
- if ([(NXProxy *)client isProxy]) // could be Object subclass, too.
- conn = [(NXProxy *)client connectionForProxy];
- else { // client is local, so no connection, and it's authorized.
- conn = nil;
- *auth = YES;
- }
- // add the client to the list
- [clientList addObject:client];
- [connList addObject:conn];
- [clientAuth addElement:auth];
- [conn registerForInvalidationNotification:self];
- #ifdef DEBUGLOG
- if (loggingIsOn) {
- id tempString = [[gameName copy] cat:": A client checked in.\n"];
- [logFile addLineToLogFile:tempString];
- [gameInfo dumpToLog:logFile];
- [tempString free];
- }
- #endif
- // ask for gameInfo object if we don't have it yet
- if (!gameInfo) [client sendGameInfoTo:gameName];
- else [client acceptTable:table name:gameName];
- return self;
- }
-
- - (oneway)addSlot:newSlot // new high scores come in here
- fromClient:(in id <HighScoreClient>)client // and go to all clients
- {
- int c;
- id <HighScoreClient> tempClient; // gets rid of protocol warnings
-
- if (loggingIsOn) {
- id tempString = [[gameName copy] cat:": A new slot was submitted.\n"];
- [logFile addLineToLogFile:tempString];
- [newSlot dumpToLog:logFile];
- [tempString free];
- }
- // insert the new score into the table; return if it doesn't fit
- if (![table addSlot:newSlot]) return self;
- // The server will send each client -addSlot:tableName: messages for every
- // slot which changes while the client is connected. That way, the client
- // can update panels, etc. when someone else gets a new highscore.
- for (c=0; c<[clientList count]; c++) {
- tempClient = [clientList objectAt:c];
- if (client != tempClient) {
- [tempClient addSlot:newSlot tableName:gameName];
- }
- }
- // save the table to a file. Done every time there's a change
- // so that nothing is lost if we crash, die, or get killed.
- [self save];
- return self;
- }
-
- - (BOOL)authorize:(id <HighScoreClient>)client
- { // get password from client and compare to gameInfo password via crypt()
- id password = [client password]; // returns a DAYString
- id encr = [password encrypt:[[gameInfo encryptedPassword] left:2]];
- BOOL ret = YES;
- if ([encr compareTo:[gameInfo encryptedPassword]]) ret = NO;
- if (loggingIsOn) {
- id tempString = [[gameName copy] cat:": Client sent password "];
- if (ret) [tempString cat:"successfully.\n"];
- else [tempString cat:"but it was wrong.\n"];
- [logFile addLineToLogFile:tempString];
- [tempString free];
- }
- return ret;
- }
-
- - (BOOL)validateClient:(id <HighScoreClient>)client
- {
- unsigned num = [clientList indexOf:client];
- BOOL *valid = [clientAuth elementAt:num];
- if (*valid != YES) return [self authorize:client];
- return YES;
- }
-
- - (oneway)clearTable:(in id <HighScoreClient>)sender
- // zero out the table. Asks sender for
- // proper authentication first. See the table
- // editing app for an example of how to do this.
- { // need to validate sender first off
- int c; id <HighScoreClient> tempClient; // gets rid of protocol warnings
- if (![self validateClient:sender]) return self;
- if (loggingIsOn) {
- id tempString = [[gameName copy] cat:": Client cleared the table.\n"];
- [logFile addLineToLogFile:tempString];
- [tempString free];
- }
- // clear the table
- [table freeObjects];
- if (template) { // if available, use the empty "template"
- [table free];
- table = [template copy];
- }
- [self save];
- // now send it off to the clients
- for (c=0; c<[clientList count]; c++) {
- tempClient = [clientList objectAt:c];
- // ***** commented out since we want to update _all_ clients!
- //if (sender != tempClient) {
- [tempClient acceptTable:table name:gameName];
- //}
- }
- return self;
- }
-
- - (oneway)deleteSlot:(in int)i client:(in id <HighScoreClient>)sender
- {
- id <HighScoreClient> tempClient; // gets rid of protocol warnings
- int c;
- if (![self validateClient:sender]) return self;
- if (loggingIsOn) {
- char *string = (char *)malloc(16);
- id tempString = [[gameName copy] cat:": Client removed slot #"];
- sprintf(string, "%d\n", i);
- [tempString cat:string];
- [logFile addLineToLogFile:tempString];
- [tempString free];
- free(string);
- }
- [table removeObjectAt:i];
- for (c=0; c<[clientList count]; c++) {
- tempClient = [clientList objectAt:c];
- if (sender != [clientList objectAt:c]) {
- [tempClient removeSlotAt:i tableName:gameName];
- }
- }
- [self save];
- return self;
- }
-
- - (oneway)replaceSlot:(in int)i with:(bycopy in id)aSlot
- client:(in id <HighScoreClient>)sender
- {
- int c; id <HighScoreClient> tempClient; // gets rid of protocol warnings
- if (![self validateClient:sender]) return self;
- if (loggingIsOn) {
- char *string = (char *)malloc(16);
- id tempString = [[gameName copy] cat:": Client replaced slot #"];
- sprintf(string, "%d\n", i);
- [tempString cat:string];
- [logFile addLineToLogFile:tempString];
- [tempString free];
- free(string);
- }
- [table replaceObjectAt:i with:aSlot];
- for (c=0; c<[clientList count]; c++) {
- tempClient = [clientList objectAt:c];
- if (sender != tempClient) {
- [tempClient replaceSlotAt:i with:aSlot tableName:gameName];
- }
- }
- [self save];
- return self;
- }
-
- // ***** not yet implemented. Will be used mostly by editors to guarantee
- // edits properly sent to all clients. For locking all other clients out
- // of a table temporarily...
- - (oneway)lockTable { return self; }
- - (oneway)unlockTable { return self; }
-
- - (const char *)gameName // Name of the game we are serving
- { return [gameName stringValue]; }
-
- - save // flushes the table to the appropriate file.
- {
- NXTypedStream *typedStream;
- haveNonTemplateTable = NO;
- NX_DURING
- typedStream = NXOpenTypedStreamForFile([scoreFile stringValue],
- NX_WRITEONLY);
- NXWriteObject(typedStream, gameInfo);
- NXWriteObject(typedStream, table);
- NXWriteObject(typedStream, template);
- NXCloseTypedStream(typedStream);
- NX_HANDLER
- // deal with typed stream errors here *****
- fprintf(stderr, "Exception %d raised in -save.\n",
- NXLocalHandler.code);
- NX_ENDHANDLER
- #ifdef DEBUGLOG
- if (loggingIsOn) {
- id tempString = [[gameName copy] cat:": Saved table.\n"];
- [logFile addLineToLogFile:tempString];
- [tempString free];
- }
- #endif
- return self;
- }
-
- - _makeTableRatherThanLoad
- {
- table = [template copy];
- if (!table) {
- haveNonTemplateTable = YES;
- table = [[HighScoreTable alloc] init]; // ***** should follow gameInfo params...
- }
- #ifdef DEBUGLOG
- if (loggingIsOn) {
- id tempString = [[gameName copy]
- cat:": Built table from template.\n"];
- [logFile addLineToLogFile:tempString];
- [tempString free];
- }
- #endif
- return self;
- }
-
- - load // load the table from a file, if it exists.
- {
- NXTypedStream *typedStream;
- FILE *testFile;
-
- haveNonTemplateTable = NO;
- // for some reason, NXOpenTypedStreamForFile() isn't returning
- // NULL for me when the file doesn't exist, so I check for the
- // file's existence first.
- testFile = fopen([scoreFile stringValue], "r");
- if (!testFile) return [self _makeTableRatherThanLoad];
- fclose(testFile);
- NX_DURING
- typedStream = NXOpenTypedStreamForFile([scoreFile stringValue],
- NX_READONLY);
- if (!typedStream) return [self _makeTableRatherThanLoad];
-
- if (table) [[table freeObjects] free];
- if (template) [[template freeObjects] free];
- gameInfo = NXReadObject(typedStream);
- // ***** should load dynamic classes here; GameInfo might require it!
- table = NXReadObject(typedStream);
- template = NXReadObject(typedStream);
- NXCloseTypedStream(typedStream);
- NX_HANDLER
- // deal with typed stream errors here *****
- [self _makeTableRatherThanLoad];
- fprintf(stderr, "Exception %d raised in -load.\n",
- NXLocalHandler.code);
- NX_ENDHANDLER
- #ifdef DEBUGLOG
- if (loggingIsOn) {
- id tempString = [[gameName copy] cat:": Loaded table.\n"];
- [logFile addLineToLogFile:tempString];
- [tempString free];
- }
- #endif
- return self;
- }
-
-
- @end