Title: Interface to a high-score fileAuthor: Kevin Turner (acapnotic at users.sourceforge.net) Description: This provides an interface for the loading and saving the game's high score file. High score files are generally placed in a location shared between all the players on that machine, but not writable by them (on filesystems which support file permissions). For the game to be able to write to the score file, it must be run from a setgid wrapper. Download: scorefile.py pygame version required: Any Comments: This code pickles a machine-wide high-score listing to (by default) /var/games. The high-score file contains username, realname, date, score/level, version and custom info fields, and should be created by your installation script. In order to get around permissions issues under unix, scorefile.py will create a C source file for you, which you must then compile and install properly yourself - read the write_wrapper_source docstring carefully. This code should work with little modification on non-unix filesystems. |
#!/usr/bin/env python2 # $Id: scorefile.py,v 1.2 2001/04/13 20:15:09 kevint Exp $ # File authored by Kevin Turner (acapnotic at users.sourceforge.net) # Created April 11, 2001 # This file is released into the Public Domain, # but we always appreciate it if you send notice to myself or the pygame # team (http://pygame.seul.org/) if you use it in your project or if # you have questions, comments, or modifications. And of course, if it # brings you fantastic good fortune, we'd be happy to accept a token of # your gratitude. On the other hand, we make no guarantees (none at # all), so if it ruins your life, don't blame us. """Interface to the high score file. This provides an interface for the loading and saving the game's high score file. High score files are generally placed in a location shared between all the players on that machine, but not writable by them (on filesystems which support file permissions). For the game to be able to write to the score file, it must be run from a setgid wrapper. All the fun is in the ScoreFile class. There's also a write_wrapper_source subroutine, see its documentation for information about the need for a set-group-id (SGID) wrapper. NOTE: You should be warned that if run SGID, this module will change the process EGID upon import.""" TRUE = 1 == 1 FALSE = not TRUE import os import time import pickle import sys from types import * # Try importing pygame, so we can define some rendering methods. try: import pygame except ImportError: pass # According to the Filesystem Hierarchy Standard (2.2 beta), # /var/games is the place to put score files. # XXX - What's the default path for non-Unix systems? default_dir = os.path.normpath('/var/games') __methods__ = ['write_wrapper_source'] class ScoreFileError: def __init__(self, errorstring): self.value = errorstring def __str__(self): return `self.value` class ScoreRecord: """An individual record in the score file. score: A numeric value indicating the player's score. level: The number of the highest level or wave reached by the player. May be "None" if the game is not structured by level. login: The identity of this user, according to the OS, e.g. "agreensp". name: The name by which the player wishes to be known, e.g. "Alan Greenspan" or "MoneyMan". version: A string describing the version, or release, of the game software. date: The date and time when the score was recorded. Stored in UTC, as a tuple. extradata: A dictionary object containing game-specific stats, e.g. board size, difficulty level, character race/class, etc. You may put anything in here you like, as long as it's pickleable. """ __members__ = ['date','login','name', 'score','level', 'extradata'] __methods__ = ['get_date'] def __init__(self, score, name=None, level=None, extradata=None): self.date = time.gmtime(time.time()) # XXX - If we're a network game server, os.getlogin() probably # doesn't return what we want. try: self.login = os.getlogin() except: self.login = None self.score = score self.name = name self.level = level self.extradata = extradata def get_date(self, formatstring=None): """Returns a string describing the time of the entry, in local time. formatstring is the same format as used by time.strftime().""" if not formatstring: formatstring = '%c' formatstring = str(formatstring) # self.date is a UTC time in a tuple. utc_seconds = time.mktime(self.date) # I thought I would have to test for time.daylight here, # and then use either time.altzone or time.timezone accordingly, # but apparently one of the other time functions already handles it, # because if I use altzone, I'm over-correcting by an hour. local_offset = -time.timezone local_seconds = utc_seconds + local_offset local_time = time.localtime(local_seconds) time_string = time.strftime(formatstring, local_time) return time_string def __getattr__(self,name): "Check extradata to see if has this attribute." if name != 'extradata' and self.extradata \ and self.extradata.has_key(name): return self.extradata[name] raise AttributeError def __str__(self): ds = {'score':self.score, 'name':self.name, 'level':self.level, 'login':self.login, 'date':self.get_date()} if self.extradata: ds.update(self.extradata) return str(ds) # ScoreRecord class ScoreFile: """A high score file which may be read from and added to. Entries are stored sorted by score.""" __members__ = ['filename', 'game_name', 'game_version', 'records'] __methods__ = ['add', 'write', 'crop', 'top', 'latest'] # TODO: Provide a default DRAW method to render some scores to a SDL surface. def __init__(self, game_name, game_version, filename=None, dirname=None): """Load or create a score file. game_name: this name is stored in the score file, and possibly used to generate a filename if none is supplied. game_version: this is the current version of the game, stored with each entry. filename: the name of the high score file. If none is given, an attempt is made to construct one from game_name. dirname: the directory where the high score file should be placed, if the filename is not specified as an absolute pathname. The Filesystem Hierarchy Standard dictates that this be /var/games. """ self.game_name = str(game_name) self.game_version = str(game_version) if dirname: dirname = os.path.normpath(dirname) else: dirname = default_dir if not os.path.abspath(dirname): dirname = os.path.join(default_dir,dirname) if filename: filename = os.path.normpath(filename) else: # find a score file, or make a filename up (based on # game_name), or get one from the configuration module # XXX - How should this work on non-Unix platforms? # XXX - configuration module does not exist # XXX - filter game_name so it contains only # filesystem-friendly characters. filename = self.game_name + '.hiscore' if not os.path.isabs(filename): filename = os.path.join(dirname, filename) # This sort of thing *should* be done by the game's installer. # We'll make a bumbling attempt to try and get something working, # though. if not os.path.exists(os.path.dirname(filename)): if _obtain_permission(): os.makedirs(os.path.dirname(filename),0775) else: os.makedirs(os.path.dirname(filename)) if not os.path.exists(filename): _obtain_permission() if os.access(os.path.dirname(filename), os.W_OK): # we can create a scorefile, but for now it's empty. self.records = [] else: raise ScoreFileError, \ "I can't write to directory %s to create the "\ "score file." % os.path.dirname(filename) _drop_permission() else: _obtain_permission() fobj = open(filename, 'r') try: loaded_scorefile = pickle.load(fobj) self.records = loaded_scorefile.records del loaded_scorefile except EOFError: # We get this if the file is zero-sized. self.records = [] fobj.close() _drop_permission() self.filename = filename # ScoreFile.__init__() def add(self, score, name=None, level=None, extradata=None, write=TRUE): """Make a new entry in the high score file. See ScoreRecord for a description of the arguments. Returns the entry's place in the high score list, with 1 being the highest. write: Should the score file be written to disk after this add? If 'must', then raise an exception if the write fails. Otherwise, if true, attempt the write (but only print a warning if it fails), if false, add the entry to the score file in memory but make no attempt to save to disk. """ # XXX - doc string should do necessary translcusion of ScoreRecord # argument docs, don't make the reader jump around to do our work. score = float(score) name = str(name) score_rec = ScoreRecord(score, name, level, extradata) score_rec.game_version = self.game_version pos = self.place(score) self.records.insert(pos-1, score_rec) if(write): if write=='must': self.write() else: try: self.write() except IOError: print "WARNING: failed to save scorefile, got %s: %s"\ % sys.exc_info()[:2] # endif(write) return pos # ScoreFile.add() def place(self,score): """Returns the place that this score would earn in the list.""" if (not self.records) or score >= self.records[0].score: # New High Score! pos = 0 elif score < self.records[-1].score: # Congratz -- Yours is the lowest score yet. pos = len(self.records) else: # binary search to find the insertion location high_end = len(self.records) - 1 low_end = 0 pos = 0 while abs(high_end - low_end) > 1: pos = int(round((high_end + low_end) / 2)) # Reminder: A HIGHER score means a LOWER index. if(score >= self.records[pos].score): high_end = pos else: low_end = pos # whend # fi return pos + 1 # ScoreFile.place() def write(self): """Save the score file to disk. Note: ScoreFile.add() calls this method, so you should rarely need to do so yourself.""" assert(self.filename) if _obtain_permission(): # Err. We can't mask the write bit for the file's owner, # otherwise this user won't be able to write to it in the future! # Ideally, the file has already been created (by the install process), # and so is already owned by someone else. os.umask(02) fobj = open(self.filename, 'w') pickle.dump(self, fobj) fobj.close() _drop_permission() def crop(self, bytes): # XXX - Untested! """Limits the size of the scorefile. This method may be used to limit the size of the scorefile. It will remove as many entries from the scorefile as are required to make it fit in the specified number of bytes. The entries removed are those with the lowest scores. Returns the number of entries trimmed.""" bytes = long(bytes) assert(bytes > 0) trimmed_entries = 0 while 1: pickledfile = pickle.dumps(self) fat = len(pickledfile) - bytes if fat <= 0: return trimmed_entries else: if len(self.records) == 0: raise ScoreFileError, "Failed to make the score "\ "file small enough for you." del self.records[-1] trimmed_entries = trimmed_entries + 1 # whend # ScoreFile.crop() def __get_item__(self, key): """Scores are returned sorted highest to lowest.""" return self.records[key] def get_by_time(self, key): """Like get_item, but sorted by time, not score.""" time_list = map(None, self.records[:].date, range(len(self.records))) time_list.sort() time_sorted = [ self.records[tl[1]] for tl in time_list ] return time_sorted[key] # ScoreFile.get_by_time() def top(self, n=1): """Returns a list of the top n scores (defaulting to 1). Returns a list of ScoreRecords. Note: This object supports the standard slice syntax, so this method is equivalent to my_scorefile[:n].""" return self[:n] def latest(self, n=1): """Returns the n most recent entries in the score file. If n is omitted, it defaults to 1. Returns a list of ScoreRecords, starting with the most recent.""" results = self.get_by_time(-n) results.reverse() return results def __len__(self): """The number of entries in the high score file.""" return len(self.records) def __str__(self): s = "%s: Score File for %s, contains %d entries.\n" %\ (self.filename, self.game_name, len(self.records)) for i in self.records: sr = "%d\t%s\t%s\t%s\t%s" \ % (i.score, i.level, i.name, i.login, i.get_date()) if i.extradata: sr = sr + str(i.extradata) s = s + sr + "\n" return s # ScoreFile.__str__() # ScoreFile def _obtain_permission(): """Re-gain our SGID group identity.""" if _SGID_WRAPPER: os.setregid(_user_gid,_privledged_gid) return TRUE return FALSE def _drop_permission(): """Drop our SGID group identity for the time being.""" if _SGID_WRAPPER: os.setregid(_privledged_gid,_user_gid) return TRUE return FALSE # top-level code if os.environ.has_key('RUN_BY_WRAPPER'): _privledged_gid = os.getegid() _user_gid = os.getgid() if _privledged_gid != _user_gid: _SGID_WRAPPER = TRUE else: _SGID_WRAPPER = FALSE print "Apparently run from a wrapper, but it doesn't seem to be SGID." # XXX: This causes us to drop our SGID permissions upon importing # the module! If our parent module was not expecting those to # change, it may not be happy with us. _drop_permission() else: _SGID_WRAPPER = FALSE print "Warning: Not run from a SGID wrapper, I may use inappropriate" print " ownership and/or permissions for the high score file." # TODO: At this point, if we believe there *should* be a wrapper installed, # but we just weren't run by it, we could exec it here, if we wanted to. # top-level code def write_wrapper_source(path,source_file): """Writes C source code that exec's path to source_file. On some platforms (e.g. unix) it's desirable to have a shared score file between users. This requires that the game be able to write to the score file no matter who's playing, but we'd rather not make the file world-writable. If the score file is installed as owned by a "games" group, then the game could write to that file if it were owned by the games group and had its sgid bit set (chmod g+s). Unfortunately for us, most systems don't use sgid for interpreted files (e.g. your python script), so we need to make a "real executable" to put the sgid bit on. This, having gained the "games" permissions, will in turn run our python script. This function writes the source for such a wrapper program to a file. You must then see to it that the file is compiled and properly installed.""" path = os.path.normpath(path) source_file = os.path.normpath(source_file) if not os.path.exists(path): print "Warning: path",path,"doesn't seem to exist." fobj = open(source_file, 'w') s = '#include <unistd.h>\n#include <stdlib.h>\n' s = s + "/* Compile like: gcc -o my_wrapper %s */\n" % source_file s = s + """ int main(int argc, char* argv[]) { putenv("RUN_BY_WRAPPER=1"); """ fobj.write(s) fobj.write("return execv(\"%s\",argv);\n}\n" % path) fobj.close() # write_wrapper_source()
Main - Repository - Submit - News |