#MUDPyE - (M)ulti-(U)ser (D)imension (Py)thon (E)ngine
#Copyright (C) 2005 Corey Staten
#This program is free software; you can redistribute it and/or
#modify it under the terms of the GNU General Public License
#as published by the Free Software Foundation; either version 2
#of the License, or (at your option) any later version.
#This program is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#GNU General Public License for more details.
#You should have received a copy of the GNU General Public License
#along with this program; if not, write to the Free Software
#Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#Send feedback/questions to MUDPyE@gmail.com
import copy
import gzip
import mudPickle
import operator
import os
import re
import time
import types
import mpmudobject
import mptraceback
import whichdb
import zlib
from mpmudobject import *
def ExtractStringList(listString):
stringList = listString.split(",")
stringList = [element.strip().replace("'", "").replace("\"", "") for element in stringList]
stringList = [element for element in stringList if (len(element) > 0)]
return stringList
class PseudoModule(object):
"""Simple class which routes calls to __getattribute__ and __setattribute__ to a dict."""
def __init__(self, modType, sourceDB):
object.__setattr__(self, "modDict", None)
object.__setattr__(self, "modType", modType)
object.__setattr__(self, "sourceDB", sourceDB)
def __getattribute__(self, key):
modDict = object.__getattribute__(self, "modDict")
modType = object.__getattribute__(self, "modType")
#Make sure the object is loaded.
if modDict is None:
modDict = object.__getattribute__(self, "sourceDB").GetContext(modType)
if modDict is None:
object.__getattribute__(self, "sourceDB").mudWorld.loggers["mud.scripterror"].error("Invalid existant pseudo-module [%s]." % modType)
return None
object.__setattr__(self, "modDict", modDict)
return modDict[key]
def __setattr__(self, key, value):
raise AttributeError, "PseudoModule's are read-only."
def __delattr__(self, key):
raise AttributeError, "PseudoModule's are read-only."
def __getitem__(self, key):
modDict = object.__getattribute__(self, "modDict")
modType = object.__getattribute__(self, "modType")
#Make sure the object is loaded.
if modDict is None:
modDict = object.__getattribute__(self, "sourceDB").GetContext(modType)
if modDict is None:
MudObjectRef.mudWorld.loggers["mud.scripterror"].error("Invalid existant pseudo-module [%s]." % modType)
return None
object.__setattr__(self, "modDict", modDict)
return modDict[key]
def __setitem__(self, key, value):
raise AttributeError, "PseudoModule's are read-only."
def __delitem__(self, key):
raise AttributeError, "PseudoModule's are read-only."
class SourceDatabase(object):
"""A database which keeps track of compressed string representations of source files."""
importRe = re.compile("^\\s*_imports\\s*=\\s*\\[\\s*((?:[\"']\\w*[\"']\\s*\\,?\\s*)*)\\s*\\]\\s*$", re.MULTILINE)
parentRe = re.compile("^\\s*_parents\\s*=\\s*\\[\\s*((?:[\"']\\w*[\"']\\s*\\,?\\s*)*)\\s*\\]\\s*$", re.MULTILINE)
def __init__(self, mudWorld, fileName, globalsFileName):
self.mudWorld = mudWorld
self.contextCache = {}
self.fileName = fileName
self.isOpen = False
self.OpenDB()
self.scriptGlobals = {"mudWorld":mudWorld}
self.pseudoModules = {}
try:
globalsFile = file(globalsFileName, "rb")
except IOError, excInst:
self.mudWorld.loggers["engine.db"].warning("Unable to open script globals file [%s]: %s" % (globalsFileName, str(excInst)))
else:
try:
globalsText = globalsFile.read()
exec globalsText in self.scriptGlobals
except IOError, excInst:
self.mudWorld.loggers["engine.db"].warning("Unable to read from script globals file: %s" % str(excInst))
except:
self.mudWorld.loggers["mud.scripterror"].error("Error executing script globals:\r\n%s" % mptraceback.format_exc())
else:
globalsFile.close()
def OpenDB(self):
"""Opens the database from the file in self.fileName. Attempts to import whatever dbm is used by the file."""
dbModuleName = whichdb.whichdb(self.fileName)
if not((dbModuleName is None) or (len(dbModuleName) == 0)):
try:
dbModule = __import__(dbModuleName)
except ImportError:
self.mudWorld.loggers["engine.db"].critical("Module [%s] not available to open SourceDatabase file [%s]." % (dbModuleName, self.fileName))
raise
else:
dbModule = __import__("anydbm")
try:
self.db = dbModule.open(self.fileName, "c")
except IOError, excInst:
self.mudWorld.loggers["engine.db"].critical("Unable to open SourceDatabase file [%s]: %s" % (self.fileName, str(excInst)))
raise
self.isOpen = True
def CloseDB(self):
"""Internal function for closing the database. Does not wipe the context cache!
Note that calling most database functions after closing will cause an exception."""
self.db.close()
self.isOpen = False
def EmptyCache(self):
"""Empties the cache of source context's."""
for objType in self.contextCache.keys():
self.UnloadContext(objType)
def BackupDB(self, backupPath):
"""Backs up a zipped version of the database to backupPath. Names the file based on the current time."""
self.CloseDB()
backupFileName = os.path.join(backupPath, "%s %s.gz" % (time.asctime(), os.path.basename(self.fileName))).replace(":", "-")
try:
backupFile = gzip.GzipFile(backupFileName, "wb", 9)
except IOError, excInst:
self.OpenDB()
self.mudWorld.loggers["engine.db"].error("Unable to open SourceDatabase backup file [%s]: %s" % (backupFileName, str(excInst)))
else:
try:
dbFile = file(self.fileName, "rb")
except IOError, excInst:
self.OpenDB()
self.mudWorld.loggers["engine.db"].error("Unable to open SourceDatabase file [%s] for backup reading: %s" % (self.fileName, str(excInst)))
else:
try:
buffer = dbFile.read(8192)
while len(buffer):
backupFile.write(buffer)
buffer = dbFile.read(8192)
except IOError, excInst:
self.OpenDB()
self.mudWorld.loggers["engine.db"].error("Error transferring SourceDatabase [%s] to backup [%s]: %s" % (self.fileName, backupFileName, str(excInst)))
dbFile.close()
backupFile.close()
if not(self.isOpen):
self.OpenDB()
def GetModule(self, objType):
try:
return self.pseudoModules[objType]
except KeyError:
pseudoModule = PseudoModule(objType, self)
self.pseudoModules[objType] = pseudoModule
return pseudoModule
def GetContext(self, objType):
"""Returns a source context dictionary for objType, loading it if necessary.
Context dictionaries are used for executing code on Mud Objects."""
try:
return self.contextCache[objType]
except KeyError:
return self.LoadContext(objType)
def LoadContext(self, objType):
"""Loads the object context for objType, either from its source file or the database."""
#We set this so that we don't have to continually pass it to the recursive functions.
try:
srcFileName = self.mudWorld.objIndex[objType]
srcFile = file(srcFileName, "rb")
except (KeyError, IOError), excInst:
self.mudWorld.loggers["engine.db"].warning("Cannot get source file for for objType [%s]: %s" % (objType, str(excInst)))
try:
zipText = self.db[objType]
srcFileText = zlib.decompress(zipText)
except KeyError:
self.mudWorld.loggers["engine.db"].error("No source text available for type [%s]." % objType)
return None
else:
srcFileText = srcFile.read()
srcFileText = srcFileText.replace("\r", "").strip()
srcFile.close()
srcFileStats = os.stat(srcFileName)
objStampName = "__mtime__%s" % (objType,)
if not(self.db.has_key(objStampName) and (self.db[objStampName] == str(srcFileStats.st_mtime))):
self.db[objStampName] = str(srcFileStats.st_mtime)
self.db[objType] = zlib.compress(srcFileText,9)
contextDict = {}
self.contextCache[objType] = contextDict
result = self._ConstructContextDict(contextDict, objType, srcFileText)
if result == False:
self.UnloadContext(objType)
return None
else:
return contextDict
def UnloadContext(self, objType):
"""Unloads the given type's source context. Useful for making sure the program reloads a revision to the source."""
if self.pseudoModules.has_key(objType):
object.__setattr__(self.pseudoModules[objType], "modDict", None)
try:
del self.contextCache[objType]
except KeyError:
self.mudWorld.loggers["engine.db"].warning("Attempt to unload source context [%s]; not in database." % objType)
def _ConstructContextDict(self, contextDict, objType, srcFileText):
"""Constructs a new context dictionary given the text of a source file."""
contextDict.update(self.scriptGlobals)
#First, we have to search for the _imports and _parents lines.
contextDict["_srcType"] = objType
contextDict["_imports"] = []
contextDict["_parents"] = []
contextDict["_version"] = 1
importMatch = SourceDatabase.importRe.search(srcFileText)
parentMatch = SourceDatabase.parentRe.search(srcFileText)
if not(importMatch is None):
importList = ExtractStringList(importMatch.group(1))
for importName in importList:
self._Import(contextDict, importName)
if not(parentMatch is None):
parentList = ExtractStringList(parentMatch.group(1))
for parentName in parentList:
self._Derive(contextDict, parentName)
contextDict["_srcType"] = objType
try:
exec srcFileText in contextDict
except:
self.mudWorld.loggers["mud.scripterror"].error("Error constructing context for objtype [%s]:\r\n%s" % (objType, mptraceback.format_exc()))
return False
#Wrap all the functions.
wrapKeys = [key for key,val in contextDict.items() if type(val) is types.FunctionType]
for wrapKey in wrapKeys:
contextDict[wrapKey] = mpmudobject.MudFunctionWrapper(contextDict[wrapKey], contextDict)
return True
def _Import(self, objDict, importType):
"""Imports the context of an objType as a PseudoModule(acts as a module in the context's global space)."""
if self.mudWorld.transitions.has_key(importType):
self.mudWorld.loggers["engine.db"].warning("Deprecated import type [%s] encountered in [%s]." % (importType, objDict["_srcType"]))
newImportType = self.mudWorld.transitions[importType]
objDict["_imports"][objDict["_imports"].index(importType)] = newImportType
importType = newImportType
if self.GetContext(importType) is None:
self.mudWorld.loggers["mud.scripterror"].error("Attempt to import non-existant PseudoModule [%s].in [%s]" % (importType, objDict["_srcType"]))
objDict[importType] = None
return None
pseudoModule = self.GetModule(importType)
objDict[importType] = pseudoModule
def _Derive(self, childDict, parentType):
"""Derives an object from the context of a given parentType."""
if self.mudWorld.transitions.has_key(parentType):
self.mudWorld.loggers["engine.db"].warning("Deprecated parent type [%s] encountered in [%s]." % (parentType, childDict["_srcType"]))
newParentType = self.mudWorld.transitions[parentType]
childDict["_parents"][childDict["_parents"].index(parentType)] = newParentType
parentType = newParentType
parentDict = self.GetContext(parentType)
if parentDict is None:
self.mudWorld.loggers["mud.scripterror"].error("Attempt to derive from non-existant objType [%s] in [%s]" % (parentType, childDict["_srcType"]))
return None
tempDict = {}
tempDict.update(parentDict)
tempDict.update(childDict)
childDict.update(tempDict)
class ObjectDatabase:
"""Database used to store MudObject data."""
def __init__(self, mudWorld, fileName):
self.mudWorld = mudWorld
self.fileName = fileName
self.objCache = {}
self.dirtyObjs = []
self.refDict = {}
self.isOpen = False
self.mudObjectRefType = GenMudObjectRefType(mudWorld)
self.OpenDB()
def OpenDB(self):
"""Opens the database from the file in self.fileName. Attempts to import whatever dbm is used by the file."""
dbModuleName = whichdb.whichdb(self.fileName)
if not((dbModuleName is None) or (len(dbModuleName) == 0)):
try:
dbModule = __import__(dbModuleName)
except ImportError:
mudWorld.loggers["engine.db"].critical("Module [%s] not available to open ObjectDatabase file [%s]." % (dbModuleName, self.fileName))
raise
else:
dbModule = __import__("anydbm")
try:
self.db = dbModule.open(self.fileName, "c")
except IOError, excInst:
mudWorld.loggers["engine.db"].critical("Unable to open ObjectDatabase file [%s]: %s" % (self.fileName, str(excInst)))
raise
self.isOpen = True
def CloseDB(self):
"""Internal function for closing the database. Does not wipe the context cache!
Note that calling most database functions after closing will cause an exception."""
self.db.sync()
self.db.close()
self.isOpen = False
def EmptyCache(self):
"""Saves dirtied objects and then clears the memory cache."""
self._SaveDirtyObjs()
self.objCache = {}
self.dirtyObjs = []
def BackupDB(self, backupPath):
"""Backs up a zipped version of the database to backupPath. Names the file based on the current time."""
self._SaveDirtyObjs()
self.CloseDB()
backupFileName = os.path.join(backupPath, "%s %s.gz" % (time.asctime(), os.path.basename(self.fileName))).replace(":", "-")
try:
backupFile = gzip.GzipFile(backupFileName, "wb", 9)
except IOError, excInst:
self.OpenDB()
self.mudWorld.loggers["engine.db"].error("Unable to open ObjectDatabase backup file [%s]: %s" % (backupFileName, str(excInst)))
else:
try:
dbFile = file(self.fileName, "rb")
except IOError, excInst:
self.OpenDB()
self.mudWorld.loggers["engine.db"].error("Unable to open ObjectDatabase file [%s] for backup reading: %s" % (self.fileName, str(excInst)))
else:
try:
buffer = dbFile.read(8192)
while len(buffer):
backupFile.write(buffer)
buffer = dbFile.read(8192)
except IOError, excInst:
self.OpenDB()
self.mudWorld.loggers["engine.db"].error("Error transferring ObjectDatabase [%s] to backup [%s]: %s" % (self.fileName, backupFileName, str(excInst)))
dbFile.close()
backupFile.close()
if not(self.isOpen):
self.OpenDB()
def Update(self):
"""Generalized update function which should be called periodically.
Currently, all it does is save dirtied objects."""
self._SaveDirtyObjs()
def DirtyObj(self, ID):
"""Dirties an object if it is not dirty already. Note, MudObject's inline this, so this should only be
used under special circumstances."""
if ID not in self.dirtyObjs:
self.dirtyObjs.append(ID)
def HasObj(self, ID):
return self.db.has_key(ID)
def GetObjRef(self, ID):
"""Should be 'GetObj' for scripts, used to get a proxy object for MudObject's."""
if not(self.refDict.has_key(ID)):
self.refDict[ID] = self.mudObjectRefType(ID)
return self.refDict[ID]
def GetObjDict(self, ID):
"""Finds the object with ID in the cache or loads it from disk. This is where cache-ing is
handled."""
if not(self.objCache.has_key(ID)):
if self.db.has_key(ID):
loadDict = self.LoadObjDict(ID)
if loadDict is None:
self.mudWorld.loggers["engine.db"].debug("Attempt to access non-existant object [%s]." % ID)
return None
self.objCache[ID] = loadDict
else:
self.mudWorld.loggers["engine.db"].debug("Attempt to access non-existant object [%s]." % ID)
return None
return self.objCache[ID]
def LoadObjDict(self, ID):
"""Loads an object from the hard-disk database. Will throw an exception if object isn't present."""
loadDict = mudPickle.loads(self.db[ID], self.GetObjRef)
objType = loadDict["_objType"]
if self.mudWorld.transitions.has_key(objType):
newObjType = self.mudWorld.transitions[objType]
loadDict["_objType"] = newObjType
self.mudWorld.loggers["engine.db"].info("Transitioned object [%s] from [%s] to [%s]" % (ID, objType, newObjType))
objRef = self.GetObjRef(ID)
object.__setattr__(objRef, "objDict", loadDict)
ExParentToChild(objRef, "_Sys_Load")
return loadDict
def UnloadObjDict(self, ID):
"""Unloads the object with the given ID, even if it is persistant."""
if ID in self.dirtyObjs:
ExChildToParent(self.GetObjRef(ID), "_Sys_Unload")
self.db[ID] = mudPickle.dumps(self.objCache[ID], 2)
self.dirtyObjs.remove(ID)
object.__setattr__(self.refDict[ID], "objDict", None)
del self.objCache[ID]
def _SaveDirtyObjs(self):
"""Saves dirtied objects to the database, and then synchronizes."""
for dirtyObj in self.dirtyObjs:
if not(self.objCache.has_key(dirtyObj)):
self.mudWorld.loggers["engine.db"].error("Dirtied object ID [%s] not in database." % dirtyObj)
continue
self.db[dirtyObj] = mudPickle.dumps(self.objCache[dirtyObj], 2)
self.dirtyObjs = []
self.db.sync()
def CreateObj(self, objType, **createArgs):
"""Creates an object with the next ID in line."""
nextIDStr = "__db__nextid_%s" % objType
try:
IDNum = int(self.db[nextIDStr])
self.db[nextIDStr] = str(IDNum + 1)
except KeyError:
IDNum = 1
self.db[nextIDStr] = "2"
ID = "%s%d" % (objType, IDNum)
return self.CreateObjAsID(ID, objType, **createArgs)
def CreateObjAsID(self, ID, objType, **createArgs):
"""Does the actual work of creating an object. Returns a reference to the object. If an object
currently has this ID, it will be deleted."""
if objType not in self.mudWorld.objIndex:
return None
if self.mudWorld.transitions.has_key(objType):
self.mudWorld.loggers["engine.db"].warning("Object created with deprecated object type [%s]." % (objType))
objType = transitions[objType]
newObjDict = {"_ID":ID, "_objType":objType, "_persist":False, "_versions":{}}
if self.HasObj(ID):
self.DelObj(ID)
self.objCache[ID] = newObjDict
newObjRef = self.GetObjRef(ID)
ExParentToChild(newObjRef, "_Sys_Create", **createArgs)
ExParentToChild(newObjRef, "_Sys_Load")
self.db[ID] = mudPickle.dumps(newObjDict)
return newObjRef
def DelObj(self, objRef):
ExChildToParent(objRef, "_Sys_Unload")
ExChildToParent(objRef, "_Sys_Destroy")
ID = objRef._ID
self.UnloadObjDict(ID)
del self.db[ID]
del self.refDict[ID]