This is HighScoreServer.m in view mode; [Download] [Up]
// 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
These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.