From 3eb4cddf42cd744bbb0a8368bb15e54c235ef563 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 30 Apr 2009 15:01:59 +0000 Subject: [PATCH] - implemented @destroy as per the MUX help specifications. As part of this, fixed the object recycling routines to actually properly replace GARBAGE-flagged objects (it crashed before). - Set up a global cleaner event to clean all @destroyed objects every 30 minutes (makes their dbrefs available). - Added the @recover command for recovering @destroyed objects up until the point that the cleaner runs and actually destroys them. This can recover @destroyed objects, rooms and exits to the same state as before @destroy. It could easily be made to recover player objects too, but I'm thinking this would be a security issue. - Added to @dig in order to allow for creating rooms with a particular parent. Also auto-creates exits in each room if desired. The only things that is not implemented is the aliases of the exits, I don't really know how to do that. - Changed the @create command format to match the @dig (it uses : to mark the parent instead of = now, since MUX' @dig reserve = to the exit list.) - Added extra security in the example event to guard against the bug that causes the whole scheduler to freak out if the event_function() gives a traceback. - Changed many instances of type to point to the defines_global.OTYPE instead of giving the integer explicitly. /Starkiel --- game/gamesrc/events/example.py | 11 +- .../parents/examples/custom_basicobject.py | 2 +- game/gamesrc/parents/examples/red_button.py | 2 +- src/cmdtable.py | 4 +- src/commands/objmanip.py | 263 ++++++++++++++---- src/events.py | 26 +- src/objects/managers/object.py | 31 ++- src/objects/models.py | 12 +- 8 files changed, 274 insertions(+), 77 deletions(-) diff --git a/game/gamesrc/events/example.py b/game/gamesrc/events/example.py index 87a1230a34..c66a7c976e 100644 --- a/game/gamesrc/events/example.py +++ b/game/gamesrc/events/example.py @@ -13,8 +13,8 @@ from src.events import IntervalEvent from src.scheduler import add_event from src.objects.models import Object -#the logger is useful for debugging since there is no source object to send to -from src.logger import log_infomsg +#the logger is useful for debugging +from src.logger import log_errmsg #Example of the event system. This example adds an event to the red_button parent #in parents/examples. It makes the button blink temptingly at a regular interval. @@ -46,7 +46,6 @@ class EventBlinkButton(IntervalEvent): #stored with the gamesrc/parent/ drawer as a base) parent = 'examples.red_button' buttons = Object.objects.global_object_script_parent_search(parent) - #log_infomsg("buttons found: %s" % buttons) for b in buttons: try: @@ -55,10 +54,10 @@ class EventBlinkButton(IntervalEvent): #button has no blink() method. Just ignore. pass except: - #show other tracebacks to owner of object. - #this is important, we must handle this exception - #gracefully! + #show other tracebacks to log and owner of object. + #This is important, we must handle these exceptions gracefully! b.get_owner().emit_to(sys.exc_info()[1]) + log_errmsg(sys.exc_info()[1]) #create and add the event to the global handler blink_event = EventBlinkButton() diff --git a/game/gamesrc/parents/examples/custom_basicobject.py b/game/gamesrc/parents/examples/custom_basicobject.py index f49e2c8911..3f9f60837b 100644 --- a/game/gamesrc/parents/examples/custom_basicobject.py +++ b/game/gamesrc/parents/examples/custom_basicobject.py @@ -6,7 +6,7 @@ gamesrc/parents and set SCRIPT_DEFAULT_OBJECT = 'custom_basicobject' in game/settings.py. Generally, if you want to conveniently set future objects to inherit from this -script parent (not as a default), this files and others like it need to be +script parent, this files and others like it need to be located under the game/gamesrc/parent directory. """ from game.gamesrc.parents.base.basicobject import BasicObject diff --git a/game/gamesrc/parents/examples/red_button.py b/game/gamesrc/parents/examples/red_button.py index 9f66038912..90857756e6 100644 --- a/game/gamesrc/parents/examples/red_button.py +++ b/game/gamesrc/parents/examples/red_button.py @@ -5,7 +5,7 @@ particular object. See example.py in gamesrc/commands for more info on the pluggable command system. Assuming this script remains in gamesrc/parents/examples, create an object -of this type using @create button=examples.red_button +of this type using @create button:examples.red_button This file also shows the use of the Event system to make the button send a message to the players at regular intervals. Note that if you create a diff --git a/src/cmdtable.py b/src/cmdtable.py index bb1e69f8cc..d8f574d05e 100644 --- a/src/cmdtable.py +++ b/src/cmdtable.py @@ -28,7 +28,7 @@ class CommandTable(object): self.ctable = {} def add_command(self, command_string, function, priv_tuple=None, - extra_vals=None, auto_help=False, staff_help=False): + extra_vals=None, auto_help=False, staff_only=False): """ Adds a command to the command table. @@ -57,7 +57,7 @@ class CommandTable(object): #add automatic help text from the command's doc string topicstr = command_string entrytext = function.__doc__ - add_help(topicstr, entrytext, staff_only=staff_help, + add_help(topicstr, entrytext, staff_only=staff_only, force_create=True, auto_help=True) def get_command_tuple(self, func_name): diff --git a/src/commands/objmanip.py b/src/commands/objmanip.py index e3cce34e64..210872ba40 100644 --- a/src/commands/objmanip.py +++ b/src/commands/objmanip.py @@ -195,9 +195,9 @@ def cmd_set(command): attrib_args = eq_args[1].split(':', 1) if len(attrib_args) > 1: # We're dealing with an attribute/value pair. - attrib_name = attrib_args[0].upper() + attrib_name = attrib_args[0] splicenum = eq_args[1].find(':') + 1 - attrib_value = eq_args[1][splicenum:] + attrib_value = (eq_args[1][splicenum:]).strip() # In global_defines.py, see NOSET_ATTRIBS for protected attribute names. if not Attribute.objects.is_modifiable_attrib(attrib_name): @@ -210,7 +210,10 @@ def cmd_set(command): victim.set_attribute(attrib_name, attrib_value) else: # No value was given, this means we delete the attribute. - verb = 'cleared' + ok = victim.clear_attribute(attrib_name) + if ok: verb = 'attribute cleared' + else: verb = 'is not a known attribute. If it is a flag, use !flag to clear it' + victim.clear_attribute(attrib_name) source_object.emit_to("%s - %s %s." % (victim.get_name(), attrib_name, verb)) else: @@ -266,14 +269,14 @@ def cmd_create(command): """ @create - Usage: @create objname [=parent] + Usage: @create objname [:parent] Creates a new object. If parent is given, the object is created as a child of this parent. The parent script is assumed to be located under game/gamesrc/parents and any further directory structure is given in Python notation. So if you have a correct parent object defined in parents/examples/red_button.py, you could load create a new object inheriting from this parent like this: - @create button=example.red_button + @create button:examples.red_button """ source_object = command.source_object @@ -281,13 +284,13 @@ def cmd_create(command): source_object.emit_to("You must supply a name!") return - eq_args = command.command_argument.split('=', 1) + eq_args = command.command_argument.split(':', 1) target_name = eq_args[0] # Create and set the object up. # TODO: This dictionary stuff is silly. Feex. odat = {"name": target_name, - "type": 3, + "type": defines_global.OTYPE_THING, "location": source_object, "owner": source_object} new_object = Object.objects.create_object(odat) @@ -417,8 +420,8 @@ def cmd_open(command): if len(eq_args) > 1: # Opening an exit to another location via @open =[,]. comma_split = eq_args[1].split(',', 1) - destination = source_object.search_for_object(comma_split[0]) # Use search_for_object to handle duplicate/nonexistant results. + destination = source_object.search_for_object(comma_split[0]) if not destination: return @@ -427,7 +430,7 @@ def cmd_open(command): return odat = {"name": exit_name, - "type": 4, + "type": defines_global.OTYPE_EXIT, "location": source_object.get_location(), "owner": source_object, "home": destination} @@ -439,7 +442,7 @@ def cmd_open(command): if len(comma_split) > 1: second_exit_name = ','.join(comma_split[1:]) odat = {"name": second_exit_name, - "type": 4, + "type": defines_global.OTYPE_EXIT, "location": destination, "owner": source_object, "home": source_object.get_location()} @@ -451,7 +454,7 @@ def cmd_open(command): else: # Create an un-linked exit. odat = {"name": exit_name, - "type": 4, + "type": defines_global.OTYPE_EXIT, "location": source_object.get_location(), "owner": source_object, "home": None} @@ -644,26 +647,85 @@ GLOBAL_CMD_TABLE.add_command("@unlink", cmd_unlink, def cmd_dig(command): """ - Creates a new object of type 'ROOM'. + Creates a new room object. - @dig + @dig[/teleport] roomname [:parent] [=exitthere,exithere] + """ source_object = command.source_object - roomname = command.command_argument + args = command.command_argument + switches = command.command_switches + + parent = '' + exits = [] + + #handle arguments + if ':' in args: + roomname, args = args.split(':',1) + if '=' in args: + parent, args = args.split('=',1) + if ',' in args: + exits = args.split(',',1) + else: + exits = args + else: + parent = args + elif '=' in args: + roomname, args = args.split('=',1) + if ',' in args: + exits = args.split(',',1) + else: + exits = [args] + else: + roomname = args + if not roomname: - source_object.emit_to("You must supply a name!") + source_object.emit_to("You must supply a new room name.") else: # Create and set the object up. odat = {"name": roomname, - "type": 2, + "type": defines_global.OTYPE_ROOM, "location": None, "owner": source_object} - new_object = Object.objects.create_object(odat) + new_room = Object.objects.create_object(odat) + source_object.emit_to("Created a new room '%s'." % (new_room,)) - source_object.emit_to("You create a new room: %s" % (new_object,)) + if parent: + #(try to) set the script parent + if not new_room.set_script_parent(parent): + source_object.emit_to("%s is not a valid parent. Used default room." % parent) + if exits: + #create exits to (and possibly back from) the new room) + destination = new_room #search_for_object(roomname) + + if destination and not destination.is_exit(): + location = source_object.get_location() + + #create an exit from here to the new room. + odat = {"name": exits[0].strip(), + "type": defines_global.OTYPE_EXIT, + "location": location, + "owner": source_object, + "home": destination} + new_object = Object.objects.create_object(odat) + source_object.emit_to("Created exit from %s to %s named '%s'." % (location,destination,new_object)) + + if len(exits)>1: + #create exit back to this room + odat = {"name": exits[1].strip(), + "type": defines_global.OTYPE_EXIT, + "location": destination, + "owner": source_object, + "home": location} + new_object = Object.objects.create_object(odat) + source_object.emit_to("Created exit back from %s to %s named '%s'" % (destination, location, new_object)) + if 'teleport' in switches: + source_object.move_to(new_room) + + GLOBAL_CMD_TABLE.add_command("@dig", cmd_dig, - priv_tuple=("genperms.builder")) + priv_tuple=("genperms.builder"),) def cmd_name(command): """ @@ -741,45 +803,144 @@ def cmd_description(command): target_obj.set_description(new_desc) GLOBAL_CMD_TABLE.add_command("@describe", cmd_description) +def cmd_recover(command): + """ + @recover + + Recovers @destroyed non-player objects. + + Usage: + @recover [dbref [,dbref2, etc]] + + switches: + ROOM - recover as ROOM type instead of THING + EXIT - recover as EXIT type instead of THING + + If no argument is given, a list of all recoverable objects will be given. + + Objects scheduled for destruction with the @destroy command is cleaned out + by the game at regular intervals. Up until the time of the next cleanup you can + recover the object using this command (use @ps to check when the next cleanup is due). + Note that exits and objects in @destroyed rooms will not be automatically recovered + to its former state, you have to @recover those objects manually. + + The object type is forgotten, so the object is returned as type ITEM if not the + switches /ROOM or /EXIT is given. Note that recovering an item as the wrong type will + most likely make it nonfunctional. + """ + + source_object = command.source_object + args = command.command_argument + switches = command.command_switches + going_objects = Object.objects.filter(type__exact=defines_global.OTYPE_GOING) + + if not args: + s = " Objects scheduled for destruction:" + if going_objects: + for o in going_objects: + s += '\n %s' % o + else: + s += " None." + source_object.emit_to(s) + return + + if ',' in args: + objlist = args.split(',') + else: + objlist = [args] + + for objname in objlist: + obj = Object.objects.list_search_object_namestr(going_objects, objname) + if len(obj) == 1: + if 'ROOM' in switches: + obj[0].type = defines_global.OTYPE_ROOM + source_object.emit_to("%s recovered as type ROOM." % obj[0]) + elif 'EXIT' in switches: + obj[0].type = defines_global.OTYPE_EXIT + source_object.emit_to("%s recovered as type EXIT." % obj[0]) + else: + obj[0].type = defines_global.OTYPE_THING + source_object.emit_to("%s recovered as type THING." % obj[0]) + obj[0].save() + else: + source_object.emit_to("No (or multiple) matches for %s." % objname) + + +GLOBAL_CMD_TABLE.add_command("@recover", cmd_recover, + priv_tuple=("genperms.builder"),auto_help=True,staff_only=True) + def cmd_destroy(command): """ - Destroy an object. + @destroy + + Destroys one or many objects. + + Usage: + @destroy[/] obj [,obj2, obj3, ...] + + switches: + override - The @destroy command will usually avoid accidentally destroying + player objects as well as objects with the SAFE flag. This + switch overrides this safety. + instant - Destroy the object immediately, without delay. + + The objects are set to GOING and will be permanently destroyed next time the system + does cleanup. Until then non-player objects can still be saved by using the + @recover command. The contents of a room will be moved out before it is destroyed, + but its exits will also be destroyed. Note that player objects can not be recovered. """ + source_object = command.source_object - switch_override = False - - if not command.command_argument: + args = command.command_argument + switches = command.command_switches + + if not args: source_object.emit_to("Destroy what?") - return + return + if ',' in args: + targetlist = args.split(',') + else: + targetlist = [args] # Safety feature. Switch required to delete players and SAFE objects. - if "override" in command.command_switches: + switch_override = False + if "override" in switches: switch_override = True + + for targetname in targetlist: + target_obj = source_object.search_for_object(targetname) + # Use search_for_object to handle duplicate/nonexistant results. + if not target_obj: + return + if target_obj.is_player() or target_obj.has_flag('SAFE'): + if source_object.id == target_obj.id: + source_object.emit_to("You can't destroy yourself.") + return + if not switch_override: + source_object.emit_to("You must use @destroy/override on Players and objects with the SAFE flag set.") + return + if target_obj.is_superuser(): + source_object.emit_to("You can't destroy a superuser.") + return + elif target_obj.is_garbage(): + source_object.emit_to("That object is already destroyed.") + return + elif target_obj.is_going() and 'instant' not in switches: + source_object.emit_to("That object is already scheduled for destruction.") + return + + # Run any scripted things that happen before destruction. + target_obj.scriptlink.at_object_destruction(pobject=source_object) + + #destroy the object (sets it to GOING) + target_obj.destroy() + + if 'instant' in switches: + #sets to GARBAGE right away (makes dbref available) + target_obj.delete() + source_object.emit_to("You destroy %s." % target_obj.get_name()) + else: + source_object.emit_to("You schedule %s for destruction." % target_obj.get_name()) - target_obj = source_object.search_for_object(command.command_argument) - # Use search_for_object to handle duplicate/nonexistant results. - if not target_obj: - return - - if target_obj.is_player(): - if source_object.id == target_obj.id: - source_object.emit_to("You can't destroy yourself.") - return - if not switch_override: - source_object.emit_to("You must use @destroy/override on players.") - return - if target_obj.is_superuser(): - source_object.emit_to("You can't destroy a superuser.") - return - elif target_obj.is_going() or target_obj.is_garbage(): - source_object.emit_to("That object is already destroyed.") - return - - # Run any scripted things that happen before destruction. - target_obj.scriptlink.at_object_destruction(pobject=source_object) - - # Notify destroyer and do the deed. - source_object.emit_to("You destroy %s." % target_obj.get_name()) - target_obj.destroy() GLOBAL_CMD_TABLE.add_command("@destroy", cmd_destroy, - priv_tuple=("genperms.builder")) + priv_tuple=("genperms.builder"),auto_help=True,staff_only=True) diff --git a/src/events.py b/src/events.py index 601f7d9ac6..63dc92e0d7 100644 --- a/src/events.py +++ b/src/events.py @@ -9,6 +9,8 @@ import time from twisted.internet import task import session_mgr from src import scheduler +from src import defines_global +from src.objects.models import Object class IntervalEvent(object): """ @@ -71,6 +73,8 @@ class IntervalEvent(object): self.set_lastfired() self.event_function() + + class IEvt_Check_Sessions(IntervalEvent): """ Event: Check all of the connected sessions. @@ -87,10 +91,30 @@ class IEvt_Check_Sessions(IntervalEvent): """ session_mgr.check_all_sessions() +class IEvt_Destroy_Objects(IntervalEvent): + """ + Event: Clean out all objects marked for destruction. + """ + def __init__(self): + super(IEvt_Destroy_Objects, self).__init__() + self.name = 'IEvt_Destroy_Objects' + self.interval = 1800 + self.description = "Destroy objects with the GOING flag set." + + def event_function(self): + """ + This is the function that is fired every self.interval seconds. + """ + going_objects = Object.objects.filter(type__exact=defines_global.OTYPE_GOING) + for obj in going_objects: + obj.delete() + def add_global_events(): """ When the server is started up, this is triggered to add all of the events in this file to the scheduler. """ # Create an instance and add it to the scheduler. - scheduler.add_event(IEvt_Check_Sessions()) \ No newline at end of file + scheduler.add_event(IEvt_Check_Sessions()) + scheduler.add_event(IEvt_Destroy_Objects()) + diff --git a/src/objects/managers/object.py b/src/objects/managers/object.py index 6e8dbaeeb8..2e26f37f78 100644 --- a/src/objects/managers/object.py +++ b/src/objects/managers/object.py @@ -60,7 +60,7 @@ class ObjectManager(models.Manager): nextfree = self.filter(type__exact=defines_global.OTYPE_GARBAGE) if nextfree: # We've got at least one garbage object to recycle. - return nextfree.id + return nextfree[0] else: # No garbage to recycle, find the highest dbnum and increment it # for our next free. @@ -74,15 +74,16 @@ class ObjectManager(models.Manager): o_query = self.filter(name__iexact=ostring) else: o_query = self.filter(name__icontains=ostring) - + return o_query.exclude(type__in=[defines_global.OTYPE_GARBAGE, defines_global.OTYPE_GOING]) - + def global_object_script_parent_search(self, script_parent): """ Searches through all objects returning those which has a certain script parent. """ o_query = self.filter(script_parent__exact=script_parent) + return o_query.exclude(type__in=[defines_global.OTYPE_GARBAGE, defines_global.OTYPE_GOING]) @@ -107,6 +108,7 @@ class ObjectManager(models.Manager): else: return [prospect for prospect in searchlist if prospect.name_match(ostring, match_type=match_type)] + def object_totals(self): """ Returns a dictionary with database object totals. @@ -121,6 +123,8 @@ class ObjectManager(models.Manager): } return dbtotals + + def player_alias_search(self, searcher, ostring): """ Search players by alias. Returns a list of objects whose "ALIAS" @@ -136,7 +140,7 @@ class ObjectManager(models.Manager): model="attribute").model_class() results = Attribute.objects.select_related().filter(attr_name__exact="ALIAS").filter(attr_value__iexact=ostring) return [prospect.get_object() for prospect in results if prospect.get_object().is_player()] - + def player_name_search(self, search_string): """ Combines an alias and global search for a player's name. If there are @@ -263,19 +267,26 @@ class ObjectManager(models.Manager): * home: Reference to another object to home to. If not specified, use location key for home. """ + #get_nextfree_dbnum() returns either an integer or an object to recycle. next_dbref = self.get_nextfree_dbnum() - Object = ContentType.objects.get(app_label="objects", - model="object").model_class() - new_object = Object() - new_object.id = next_dbref - new_object.type = odat["type"] + if type(next_dbref) == type(int()): + #create object with new dbref + Object = ContentType.objects.get(app_label="objects", + model="object").model_class() + new_object = Object() + new_object.id = next_dbref + else: + #recycle an old object's dbref + new_object = next_dbref + + new_object.type = odat["type"] new_object.set_name(odat["name"]) # If this is a player, we don't want him owned by anyone. # The get_owner() function will return that the player owns # himself. - if odat["type"] == 1: + if odat["type"] == defines_global.OTYPE_PLAYER: new_object.owner = None new_object.zone = None else: diff --git a/src/objects/models.py b/src/objects/models.py index d8700a4898..f816a58eed 100755 --- a/src/objects/models.py +++ b/src/objects/models.py @@ -502,7 +502,7 @@ class Object(models.Model): % (self,)) # Set the object type to GOING - self.type = 5 + self.type = defines_global.OTYPE_GOING # Destroy any exits to and from this room, do this first self.clear_exits() # Clear out any objects located within the object @@ -519,7 +519,7 @@ class Object(models.Model): uobj[0].delete() # Set the object to type GARBAGE. - self.type = 6 + self.type = defines_global.OTYPE_GARBAGE self.save() # Clear all attributes @@ -530,8 +530,8 @@ class Object(models.Model): Destroys all of the exits and any exits pointing to this object as a destination. """ - exits = self.get_contents(filter_type=4) - exits += self.obj_home.all().filter(type__exact=4) + exits = self.get_contents(filter_type=defines_global.OTYPE_EXIT) + exits += self.obj_home.all().filter(type__exact=defines_global.OTYPE_EXIT) for exit in exits: exit.destroy() @@ -781,6 +781,7 @@ class Object(models.Model): parent_str: (string) String pythonic import path of the script parent assuming the python path is game/gamesrc/parents. """ + if parent_str == None: if self.is_player(): self.script_parent = settings.SCRIPT_DEFAULT_PLAYER @@ -789,9 +790,10 @@ class Object(models.Model): elif parent_str: #check if this is actually a reasonable script parent #(storing with a non-valid parent path causes havoc!) + parent_str = str(parent_str).strip() if not scripthandler.scriptlink(self, parent_str): return False - self.script_parent = parent_str.strip() + self.script_parent = parent_str self.save() return True