diff --git a/src/commands/command.py b/src/commands/command.py index 895e2e8d40..15d82e97d6 100644 --- a/src/commands/command.py +++ b/src/commands/command.py @@ -141,8 +141,10 @@ class Command(object): input can be either a cmd object or the name of a command. """ try: + # first assume input is a command (the most common case) return cmd.key in self._matchset - except AttributeError: # got a string + except AttributeError: + # probably got a string return cmd in self._matchset def __contains__(self, query): @@ -154,7 +156,7 @@ class Command(object): query (str) - query to match against. Should be lower case. """ - return any(query in keyalias for keyalias in self._matchset) + return any(query in keyalias for keyalias in self._keyaliases) def match(self, cmdname): """ diff --git a/src/commands/default/system.py b/src/commands/default/system.py index 47bc58cc21..a4ebe5d10b 100644 --- a/src/commands/default/system.py +++ b/src/commands/default/system.py @@ -13,7 +13,7 @@ from src.server.sessionhandler import SESSIONS from src.scripts.models import ScriptDB from src.objects.models import ObjectDB from src.players.models import PlayerDB -from src.utils import logger, utils, gametime +from src.utils import logger, utils, gametime, create from src.commands.default.muxcommand import MuxCommand # limit symbol import for API @@ -213,9 +213,10 @@ class CmdScripts(MuxCommand): Operate on scripts. Usage: - @scripts[/switches] [] + @scripts[/switches] [] Switches: + start - start a script (must supply a script path) stop - stops an existing script kill - kills a script - without running its cleanup hooks validate - run a validation on the script(s) @@ -225,9 +226,11 @@ class CmdScripts(MuxCommand): will be searched for all scripts defined on it, or an script name or dbref. For using the /stop switch, a unique script dbref is required since whole classes of scripts often have the same name. + + Use @script for managing commands on objects. """ key = "@scripts" - aliases = "@listscripts" + aliases = ["@globalscript", "@listscripts"] locks = "cmd:perm(listscripts) or perm(Wizards)" help_category = "System" @@ -239,6 +242,14 @@ class CmdScripts(MuxCommand): string = "" if args: + if "start" in self.switches: + # global script-start mode + new_script = create.create_script(args) + if new_script: + caller.msg("Global script %s was started successfully." % args) + else: + caller.msg("Global script %s could not start correctly. See logs." % args) + return # test first if this is a script match scripts = ScriptDB.objects.get_all_scripts(key=args) diff --git a/src/scripts/scripts.py b/src/scripts/scripts.py index 120b98f142..bb5b274246 100644 --- a/src/scripts/scripts.py +++ b/src/scripts/scripts.py @@ -5,17 +5,22 @@ scripts are inheriting from. It also defines a few common scripts. """ +from sys import getsizeof from time import time +from collections import defaultdict from twisted.internet.defer import maybeDeferred from twisted.internet.task import LoopingCall -from twisted.internet import task +from django.conf import settings from src.server.sessionhandler import SESSIONS from src.typeclasses.typeclass import TypeClass +from src.typeclasses.models import _ATTRIBUTE_CACHE from src.scripts.models import ScriptDB from src.comms import channelhandler from src.utils import logger -__all__ = ("Script", "DoNothing", "CheckSessions", "ValidateScripts", "ValidateChannelHandler", "AddCmdSet") +__all__ = ("Script", "DoNothing", "CheckSessions", "ValidateScripts", "ValidateChannelHandler", "ClearAttributeCache") + +_ATTRIBUTE_CACHE_MAXSIZE = settings.ATTRIBUTE_CACHE_MAXSIZE # attr-cache size in MB. # # Base script, inherit from Script below instead. @@ -142,7 +147,7 @@ class ScriptClass(TypeClass): if obj: # check so the scripted object is valid and initalized try: - dummy = object.__getattribute__(obj, 'cmdset') + object.__getattribute__(obj, 'cmdset') except AttributeError: # this means the object is not initialized. self.dbobj.is_active = False @@ -182,7 +187,7 @@ class ScriptClass(TypeClass): if self.dbobj.db_interval > 0: try: self._stop_task() - except Exception, e: + except Exception: logger.log_trace("Stopping script %s(%s)" % (self.key, self.dbid)) pass try: @@ -217,7 +222,7 @@ class ScriptClass(TypeClass): self.ndb._paused_time = dt self._start_task(start_now=False) del self.db._paused_time - except Exception, e: + except Exception: logger.log_trace() self.dbobj.is_active = False return False @@ -387,7 +392,7 @@ class DoNothing(Script): def at_script_creation(self): "Setup the script" self.key = "sys_do_nothing" - self.desc = "This is a placeholder script." + self.desc = "This is an empty placeholder script." class CheckSessions(Script): "Check sessions regularly." @@ -420,7 +425,6 @@ class ValidateScripts(Script): class ValidateChannelHandler(Script): "Update the channelhandler to make sure it's in sync." - def at_script_creation(self): "Setup the script" self.key = "sys_channels_validate" @@ -433,44 +437,16 @@ class ValidateChannelHandler(Script): #print "ValidateChannelHandler run." channelhandler.CHANNELHANDLER.update() -class AddCmdSet(Script): - """ - This script permanently assigns a command set - to an object whenever it is started. This is not - used by the core system anymore, it's here mostly - as an example. - """ +class ClearAttributeCache(Script): + "Clear the attribute cache." def at_script_creation(self): "Setup the script" - if not self.key: - self.key = "add_cmdset" - if not self.desc: - self.desc = "Adds a cmdset to an object." + self.key = "sys_cache_clear" + self.desc = "Clears the Attribute Cache" + self.interval = 3600 * 2 self.persistent = True - - # this needs to be assigned to upon creation. - # It should be a string pointing to the right - # cmdset module and cmdset class name, e.g. - # 'examples.cmdset_redbutton.RedButtonCmdSet' - # self.db.cmdset = - # self.db.add_default = - - def at_start(self): - "Get cmdset and assign it." - cmdset = self.db.cmdset - if cmdset: - if self.db.add_default: - self.obj.cmdset.add_default(cmdset) - else: - self.obj.cmdset.add(cmdset) - - def at_stop(self): - """ - This removes the cmdset when the script stops - """ - cmdset = self.db.cmdset - if cmdset: - if self.db.add_default: - self.obj.cmdset.delete_default() - else: - self.obj.cmdset.delete(cmdset) + def at_repeat(self): + "called every 2 hours. Sets a max attr-cache limit to 100 MB." # enough for normal usage? + global _ATTRIBUTE_CACHE + if getsizeof(_ATTRIBUTE_CACHE) / 1024.0 > _ATTRIBUTE_CACHE_MAXSIZE: + _ATTRIBUTE_CACHE = defaultdict(dict) diff --git a/src/server/initial_setup.py b/src/server/initial_setup.py index 837a0927b2..2859780bc7 100644 --- a/src/server/initial_setup.py +++ b/src/server/initial_setup.py @@ -135,7 +135,9 @@ def create_system_scripts(): script2 = create.create_script(scripts.ValidateScripts) # update the channel handler to make sure it's in sync script3 = create.create_script(scripts.ValidateChannelHandler) - if not script1 or not script2 or not script3: + # clear the attribute cache regularly + script4 = create.create_script(scripts.ClearAttributeCache) + if not script1 or not script2 or not script3 or not script4: print _(" Error creating system scripts.") def start_game_time(): diff --git a/src/settings_default.py b/src/settings_default.py index 3eca481319..77a1e912f3 100644 --- a/src/settings_default.py +++ b/src/settings_default.py @@ -99,10 +99,15 @@ IDLE_COMMAND = "idle" ENCODINGS = ["utf-8", "latin-1", "ISO-8859-1"] # The game server opens an AMP port so that the portal can # communicate with it. This is an internal functionality of Evennia, usually -# operating between the two processes on the same machine. Don't change unless -# you know what you are doing. +# operating between two processes on the same machine. You usually don't need to +# change this unless you cannot use the default AMP port/host for whatever reason. AMP_HOST = 'localhost' AMP_PORT = 5000 +# Attributes on objects are cached aggressively for speed. If the number of +# objects is large (and their attributes are often accessed) this can use up a lot of +# memory. So every now and then Evennia checks the size of this cache and resets +# it if it's too big. This variable sets the maximum size (in MB). +ATTRIBUTE_CACHE_MAXSIZE = 100 ###################################################################### # Evennia Database config diff --git a/src/typeclasses/models.py b/src/typeclasses/models.py index c9919328e6..d3eb509ec7 100644 --- a/src/typeclasses/models.py +++ b/src/typeclasses/models.py @@ -32,6 +32,8 @@ try: except ImportError: import pickle import traceback +from collections import defaultdict + from django.db import models from django.conf import settings from django.utils.encoding import smart_str @@ -77,6 +79,11 @@ def _del_cache(obj, name): except AttributeError: pass +# this cache holds the attributes loaded on objects, one dictionary +# of attributes per object. +_ATTRIBUTE_CACHE = defaultdict(dict) + + #------------------------------------------------------------ # # Attributes @@ -673,6 +680,7 @@ class TypeNickHandler(object): # #------------------------------------------------------------ + class TypedObject(SharedMemoryModel): """ Abstract Django model. @@ -1213,18 +1221,17 @@ class TypedObject(SharedMemoryModel): # Helper methods for persistent attributes - _attribute_cache = {} def has_attribute(self, attribute_name): """ See if we have an attribute set on the object. attribute_name: (str) The attribute's name. """ - if attribute_name not in _GA(self, "_attribute_cache"): + if attribute_name not in _ATTRIBUTE_CACHE[self]: attrib_obj = _GA(self, "_attribute_class").objects.filter(db_obj=self).filter( db_key__iexact=attribute_name) if attrib_obj: - _GA(self, "_attribute_cache")[attribute_name] = attrib_obj[0] + _ATTRIBUTE_CACHE[self][attribute_name] = attrib_obj[0] else: return False return True @@ -1238,7 +1245,7 @@ class TypedObject(SharedMemoryModel): new_value: (python obj) The value to set the attribute to. If this is not a str, the object will be stored as a pickle. """ - attrib_obj = _GA(self, "_attribute_cache").get("attribute_name") + attrib_obj = _ATTRIBUTE_CACHE[self].get("attribute_name") if not attrib_obj: attrclass = _GA(self, "_attribute_class") # check if attribute already exists. @@ -1252,7 +1259,7 @@ class TypedObject(SharedMemoryModel): attrib_obj = attrclass(db_key=attribute_name, db_obj=self) # re-set an old attribute value attrib_obj.value = new_value - _GA(self,"_attribute_cache")[attribute_name] = attrib_obj + _ATTRIBUTE_CACHE[self][attribute_name] = attrib_obj def get_attribute(self, attribute_name, default=None): """ @@ -1263,14 +1270,14 @@ class TypedObject(SharedMemoryModel): attribute_name: (str) The attribute's name. default: What to return if no attribute is found """ - attrib_obj = _GA(self,"_attribute_cache").get(attribute_name) + attrib_obj = _ATTRIBUTE_CACHE[self].get(attribute_name) if not attrib_obj: attrib_obj = _GA(self, "_attribute_class").objects.filter( db_obj=self).filter(db_key__iexact=attribute_name) if not attrib_obj: return default - _GA(self,"_attribute_cache")[attribute_name] = attrib_obj[0] #query is first evaluated here - return _GA(self, "_attribute_cache")[attribute_name].value + _ATTRIBUTE_CACHE[self][attribute_name] = attrib_obj[0] #query is first evaluated here + return _ATTRIBUTE_CACHE[self][attribute_name].value return attrib_obj.value def get_attribute_raise(self, attribute_name): @@ -1280,14 +1287,14 @@ class TypedObject(SharedMemoryModel): attribute_name: (str) The attribute's name. """ - attrib_obj = _GA(self, "_attribute_cache.get")(attribute_name) + attrib_obj = _ATTRIBUTE_CACHE[self].get(attribute_name) if not attrib_obj: attrib_obj = _GA(self, "_attribute_class").objects.filter( db_obj=self).filter(db_key__iexact=attribute_name) if not attrib_obj: raise AttributeError - _GA(self, "_attribute_cache")[attribute_name] = attrib_obj[0] #query is first evaluated here - return _GA(self, "_attribute_cache")[attribute_name].value + _ATTRIBUTE_CACHE[self][attribute_name] = attrib_obj[0] #query is first evaluated here + return _ATTRIBUTE_CACHE[self][attribute_name].value return attrib_obj.value def del_attribute(self, attribute_name): @@ -1296,9 +1303,9 @@ class TypedObject(SharedMemoryModel): attribute_name: (str) The attribute's name. """ - attr_obj = _GA(self, "_attribute_cache").get(attribute_name) + attr_obj = _ATTRIBUTE_CACHE[self].get(attribute_name) if attr_obj: - del _GA(self, "_attribute_cache")[attribute_name] + del _ATTRIBUTE_CACHE[self][attribute_name] attr_obj.delete() else: try: @@ -1314,9 +1321,9 @@ class TypedObject(SharedMemoryModel): attribute_name: (str) The attribute's name. """ - attr_obj = _GA(self, "_attribute_cache").get(attribute_name) + attr_obj = _ATTRIBUTE_CACHE[self].get(attribute_name) if attr_obj: - del _GA(self, "_attribute_cache")[attribute_name] + del _ATTRIBUTE_CACHE[self][attribute_name] attr_obj.delete() else: try: diff --git a/src/utils/dummyrunner_actions.py b/src/utils/dummyrunner_actions.py index c621b48aa1..7bea56bc82 100644 --- a/src/utils/dummyrunner_actions.py +++ b/src/utils/dummyrunner_actions.py @@ -1,17 +1,17 @@ """ -These are actions for the dummy client runner, using -the default command set and intended for unmodified Evennia. +These are actions for the dummy client runner, using +the default command set and intended for unmodified Evennia. Each client action is defined as a function. The clients -will perform these actions randomly (except the login action). +will perform these actions randomly (except the login action). Each action-definition function should take one argument- "client", which is a reference to the client currently performing the action -Use the client object for saving data between actions. +Use the client object for saving data between actions. The client object has the following relevant properties and methods: - cid - unique client id - istep - the current step + cid - unique client id + istep - the current step exits - an empty list. Can be used to store exit names objs - an empty list. Can be used to store object names counter() - get an integer value. This counts up for every call and @@ -19,7 +19,7 @@ The client object has the following relevant properties and methods: The action-definition function should return the command that the client should send to the server (as if it was input in a mud client). -It should also return a string detailing the action taken. This string is +It should also return a string detailing the action taken. This string is used by the "brief verbose" mode of the runner and is prepended by "Client N " to produce output like "Client 3 is creating objects ..." @@ -30,10 +30,10 @@ are 2-tuples (probability, action_func), where probability defines how common it is for that particular action to happen. The runner will randomly pick between those functions based on the probability. -ACTIONS = (login_func, (0.3, func1), (0.1, func2) ... ) +ACTIONS = (login_func, (0.3, func1), (0.1, func2) ... ) To change the runner to use your custom ACTION and/or action -definitions, edit settings.py and add +definitions, edit settings.py and add DUMMYRUNNER_ACTIONS_MODULE = "path.to.your.module" @@ -45,27 +45,27 @@ definitions, edit settings.py and add import time RUNID = time.time() -# some convenient templates +# some convenient templates -START_ROOM = "testing_room_start-%s-%s" % (RUNID, "%i") +START_ROOM = "testing_room_start-%s-%s" % (RUNID, "%i") ROOM_TEMPLATE = "testing_room_%s-%s" % (RUNID, "%i") EXIT_TEMPLATE = "exit_%s-%s" % (RUNID, "%i") OBJ_TEMPLATE = "testing_obj_%s-%s" % (RUNID, "%i") TOBJ_TEMPLATE = "testing_button_%s-%s" % (RUNID, "%i") TOBJ_TYPECLASS = "examples.red_button.RedButton" -# action function definitions +# action function definitions def c_login(client): - "logins to the game" + "logins to the game" cname = "Dummy-%s-%i" % (RUNID, client.cid) cemail = "%s@dummy.com" % (cname.lower()) cpwd = "%s-%s" % (RUNID, client.cid) - cmd = ('create "%s" %s %s' % (cname, cemail, cpwd), + cmd = ('create "%s" %s %s' % (cname, cemail, cpwd), 'connect %s %s' % (cemail, cpwd), '@dig %s' % START_ROOM % client.cid, '@teleport %s' % START_ROOM % client.cid) - + return cmd, "logs in as %s ..." % cname def c_logout(client): @@ -75,18 +75,18 @@ def c_logout(client): def c_looks(client): "looks at various objects" cmd = ["look %s" % obj for obj in client.objs] - if not cmd: + if not cmd: cmd = ["look %s" % exi for exi in client.exits] - if not cmd: + if not cmd: cmd = "look" return cmd, "looks ..." def c_examines(client): "examines various objects" cmd = ["examine %s" % obj for obj in client.objs] - if not cmd: + if not cmd: cmd = ["examine %s" % exi for exi in client.exits] - if not cmd: + if not cmd: cmd = "examine me" return cmd, "examines objs ..." @@ -96,30 +96,30 @@ def c_help(client): 'help @teleport', 'help look', 'help @tunnel', - 'help @dig') + 'help @dig') return cmd, "reads help ..." def c_digs(client): - "digs a new room, storing exit names on client" + "digs a new room, storing exit names on client" roomname = ROOM_TEMPLATE % client.counter() exitname1 = EXIT_TEMPLATE % client.counter() exitname2 = EXIT_TEMPLATE % client.counter() - client.exits.extend([exitname1, exitname2]) + client.exits.extend([exitname1, exitname2]) cmd = '@dig %s = %s, %s' % (roomname, exitname1, exitname2) return cmd, "digs ..." def c_creates_obj(client): - "creates normal objects, storing their name on client" + "creates normal objects, storing their name on client" objname = OBJ_TEMPLATE % client.counter() client.objs.append(objname) cmd = ('@create %s' % objname, '@desc %s = "this is a test object' % objname, '@set %s/testattr = this is a test attribute value.' % objname, - '@set %s/testattr2 = this is a second test attribute.' % objname) + '@set %s/testattr2 = this is a second test attribute.' % objname) return cmd, "creates obj ..." def c_creates_button(client): - "creates example button, storing name on client" + "creates example button, storing name on client" objname = TOBJ_TEMPLATE % client.counter() client.objs.append(objname) cmd = ('@create %s:%s' % (objname, TOBJ_TYPECLASS), @@ -134,21 +134,43 @@ def c_moves(client): # Action tuple (required) -# +# # This is a tuple of client action functions. The first element is the # function the client should use to log into the game and move to -# STARTROOM . The second element is the logout command, for cleanly +# STARTROOM . The second element is the logout command, for cleanly # exiting the mud. The following elements are 2-tuples of (probability, # action_function). The probablities should normally sum up to 1, -# otherwise the system will normalize them. +# otherwise the system will normalize them. # -ACTIONS = ( c_login, - c_logout, - (0.2, c_looks), - (0.1, c_examines), - (0.2, c_help), - (0.1, c_digs), - (0.1, c_creates_obj), +# heavy builder definition +#ACTIONS = ( c_login, +# c_logout, +# (0.2, c_looks), +# (0.1, c_examines), +# (0.2, c_help), +# (0.1, c_digs), +# (0.1, c_creates_obj), +# #(0.1, c_creates_button), +# (0.2, c_moves)) +# "normal builder" definition +ACTIONS = ( c_login, + c_logout, + (0.5, c_looks), + (0.08, c_examines), + (0.1, c_help), + (0.01, c_digs), + (0.01, c_creates_obj), #(0.1, c_creates_button), - (0.2, c_moves)) + (0.3, c_moves)) +# "normal player" definition +#ACTIONS = ( c_login, +# c_logout, +# (0.4, c_looks), +# #(0.1, c_examines), +# (0.2, c_help), +# #(0.1, c_digs), +# #(0.1, c_creates_obj), +# #(0.1, c_creates_button), +# (0.4, c_moves)) + diff --git a/src/utils/utils.py b/src/utils/utils.py index f2ce897081..0ec397cf76 100644 --- a/src/utils/utils.py +++ b/src/utils/utils.py @@ -8,7 +8,6 @@ be of use when designing your own game. """ from inspect import ismodule import os, sys, imp, types, math -from collections import Counter import textwrap import datetime import random @@ -460,7 +459,14 @@ def run_async(async_func, at_return=None, at_err=None): Use this function with restrain and only for features/commands that you know has no influence on the cause-and-effect order of your game (commands given after the async function might be executed before - it has finished). + it has finished). Accessing the same property from different threads can + lead to unpredicted behaviour if you are not careful (this is called a + "race condition"). + + Also note that some databases, notably sqlite3, don't support access from + multiple threads simultaneously, so if you do heavy database access from + your async_func under sqlite3 you will probably run very slow or even get + tracebacks. async_func() - function that should be run asynchroneously at_return(r) - if given, this function will be called when async_func returns