diff --git a/game/gamesrc/commands/examples/state_example.py b/game/gamesrc/commands/examples/state_example.py index 0ea4d1a3c8..0da6608223 100644 --- a/game/gamesrc/commands/examples/state_example.py +++ b/game/gamesrc/commands/examples/state_example.py @@ -106,6 +106,9 @@ def print_menu(source_obj,choice=None): #Add the 'entry' command to the normal command table GLOBAL_CMD_TABLE.add_command("entermenu", cmd_entermenu) +#create the state. +GLOBAL_STATE_TABLE.add_state(STATENAME) + #Add the menu commands to the state table by tying them to the 'menu' state. It is #important that the name of the state matches what we set the player-object to in #the 'entry' command. Since auto_help is on, we will have help entries for all commands @@ -113,3 +116,107 @@ GLOBAL_CMD_TABLE.add_command("entermenu", cmd_entermenu) GLOBAL_STATE_TABLE.add_command(STATENAME, "option1", menu_cmd_option1,auto_help=True) GLOBAL_STATE_TABLE.add_command(STATENAME, "option2", menu_cmd_option2,auto_help=True) GLOBAL_STATE_TABLE.add_command(STATENAME, "menu", menu_cmd_menu,auto_help=True) + + + + + +#-----------------------testing the depth of the state system +# This is a test suite that shows off all the features of the state system. +# It sets up a test command @test_state that takes an argument 1-6 for moving into states +# with different characteristics. Note that the only difference as to how the +# various states are created lies in the options given to the add_state() function. +# Use @exit to leave any state. +# +# All states includes a small test function named "test". +# 1: A very limited state; only contains the "test" command. +# 2: All global commands are included (so this should be the same as normal operation, +# except you cannot traverse exits and use object-based cmds) +# 3: Only the global commands "get" and "inventory" are included into the state. +# 4: All global commands /except/ "get" and "inventory" are available +# 5: All global commands availabe + ability to traverse exits (not use object-based cmds). +# 6: Only the "test" command, but ability to both traverse exits and use object-based cmds. +# +# Examples of in-game use: +# 1: was used for the menu system above. +# 2: could be used in order to stop someone from moving despite exits being open (tied up?) +# 3: someone incapacitated or blinded might get only limited commands available +# 4: in e.g. a combat state, things like crafting should not be possible +# 5: Pretty much default operation, maybe limiting the use of magical weapons in a room etc? +# 6: A state of panic? You can move, but not take in your surroundings? +# ... the possibilities are endless. + +#defining the test-state names so they are the same everywhere +TSTATE1 = 'no_globals' +TSTATE2 = 'all_globals' +TSTATE3 = 'include_some_globals' +TSTATE4 = 'exclude_some_globals' +TSTATE5 = 'global_allow_exits' +TSTATE6 = 'noglobal_allow_exits_obj_cmds' + +#the test command @test_state +def cmd_test_state(command): + "testing the new state system" + source_object = command.source_object + args = command.command_argument + if not args: + source_object.emit_to("Usage: @test_state [1..6]") + return + arg = args.strip() + if arg=='1': + state = TSTATE1 + elif arg=='2': + state = TSTATE2 + elif arg=='3': + state = TSTATE3 + elif arg=='4': + state = TSTATE4 + elif arg=='5': + state = TSTATE5 + elif arg=='6': + state = TSTATE6 + else: + source_object.emit_to("Usage: @test_state [1..6]") + return + #set the state + source_object.set_state(state) + source_object.emit_to("Now in state '%s' ..." % state) + +#a simple command to include in all states. +def cmd_instate_cmd(command): + "test command in state" + command.source_object.emit_to("This command works!") + +#define some global commands to filter for +cmdfilter = ['get','inventory'] + +#1: A simple, basic state +GLOBAL_STATE_TABLE.add_state(TSTATE1,exit_command=True) +#2: Include all normal commands in the state +GLOBAL_STATE_TABLE.add_state(TSTATE2,exit_command=True,global_cmds='all') +#3: Include only the two global commands in cmdfilter +GLOBAL_STATE_TABLE.add_state(TSTATE3,exit_command=True, + global_cmds='include',global_filter=cmdfilter) +#4: Include all global commands except the ones in cmdfilter +GLOBAL_STATE_TABLE.add_state(TSTATE4,exit_command=True, + global_cmds='exclude',global_filter=cmdfilter) +#5: Include all global commands + ability to traverse exits +GLOBAL_STATE_TABLE.add_state(TSTATE5,exit_command=True, + global_cmds='all', + allow_exits=True) +#6: No global commands, allow exits and commands defined on objects. +GLOBAL_STATE_TABLE.add_state(TSTATE6,exit_command=True, + allow_exits=True,allow_obj_cmds=True) + +#append the "test" function to all states +GLOBAL_STATE_TABLE.add_command(TSTATE1,'test',cmd_instate_cmd) +GLOBAL_STATE_TABLE.add_command(TSTATE2,'test',cmd_instate_cmd) +GLOBAL_STATE_TABLE.add_command(TSTATE3,'test',cmd_instate_cmd) +GLOBAL_STATE_TABLE.add_command(TSTATE4,'test',cmd_instate_cmd) +GLOBAL_STATE_TABLE.add_command(TSTATE5,'test',cmd_instate_cmd) +GLOBAL_STATE_TABLE.add_command(TSTATE6,'test',cmd_instate_cmd) + +#create the entry function for testing all states +GLOBAL_CMD_TABLE.add_command('@test_state',cmd_test_state) + + diff --git a/game/gamesrc/parents/examples/custom_basicobject.py b/game/gamesrc/parents/examples/custom_basicobject.py index 3f9f60837b..b5817b1b47 100644 --- a/game/gamesrc/parents/examples/custom_basicobject.py +++ b/game/gamesrc/parents/examples/custom_basicobject.py @@ -44,6 +44,24 @@ class CustomBasicObject(BasicObject): """ pass + def at_before_move(self, target_location): + """ + This hook is called just before the object is moved. + Input: + target_location (obj): The location the player is about to move to. + Return value: + If this function returns anything but None (no return value), + the move is aborted. This allows for character-based move + restrictions (not only exit locks). + """ + pass + + def at_after_move(self): + """ + This hook is called just after the player has been successfully moved. + """ + pass + def class_factory(source_obj): """ diff --git a/game/gamesrc/parents/examples/custom_basicplayer.py b/game/gamesrc/parents/examples/custom_basicplayer.py index 691d38c470..458a61a518 100644 --- a/game/gamesrc/parents/examples/custom_basicplayer.py +++ b/game/gamesrc/parents/examples/custom_basicplayer.py @@ -56,6 +56,13 @@ class CustomBasicPlayer(BasicPlayer): #show us our surroundings pobject.execute_cmd("look") + def at_move(self): + """ + This is triggered whenever the object is moved to a new location + (for whatever reason) using the src.objects.models.move_to() function. + """ + pass + def class_factory(source_obj): """ diff --git a/src/cmdhandler.py b/src/cmdhandler.py index 6b99ee93ba..063359b530 100755 --- a/src/cmdhandler.py +++ b/src/cmdhandler.py @@ -18,6 +18,16 @@ class UnknownCommand(Exception): Throw this when a user enters an an invalid command. """ pass + +class CommandNotInState(Exception): + """ + Throw this when a user tries a global command that exists, but + don't happen to be defined in the current game state. + err_string: The error string returned to the user. + """ + def __init__(self,err_string): + self.err_string = err_string + class ExitCommandHandler(Exception): """ Thrown when something happens and it's time to exit the command handler. @@ -155,41 +165,7 @@ def match_idle(command): command.session.count_command(silently=True) raise ExitCommandHandler -def match_exits(command): - """ - See if we can find an input match to exits. - """ - # If we're not logged in, don't check exits. - source_object = command.source_object - location = source_object.get_location() - - if location == None: - logger.log_errmsg("cmdhandler.match_exits(): Object '%s' no location." % - source_object) - return - - exits = location.get_contents(filter_type=defines_global.OTYPE_EXIT) - Object = ContentType.objects.get(app_label="objects", - model="object").model_class() - exit_matches = Object.objects.list_search_object_namestr(exits, - command.command_string, - match_type="exact") - if exit_matches: - # Only interested in the first match. - targ_exit = exit_matches[0] - # An exit's home is its destination. If the exit has a None home value, - # it's not traversible. - if targ_exit.get_home(): - # SCRIPT: See if the player can traverse the exit - if not targ_exit.scriptlink.default_lock(source_object): - source_object.emit_to("You can't traverse that exit.") - else: - source_object.move_to(targ_exit.get_home()) - else: - source_object.emit_to("That exit leads to nowhere.") - # We found a match, kill the command handler. - raise ExitCommandHandler - + def match_alias(command): """ Checks to see if the entered command matches an alias. If so, replaces @@ -225,6 +201,7 @@ def match_alias(command): elif first_char == ':': command.command_argument = get_aliased_message() command.command_string = "pose" +# command.command_string = "emote" # Pose without space alias. elif first_char == ';': command.command_argument = get_aliased_message() @@ -263,15 +240,62 @@ def match_channel(command): command.command_string = "@cemit" command.command_switches = ["sendername", "quiet"] command.command_argument = second_arg + return True + +def match_exits(command,test=False): + """ + See if we can find an input match to exits. + command - the command we are testing for. + if a match, move obj and exit + test - just return Truee if it is an exit command, + do not move the object there. + """ + # If we're not logged in, don't check exits. + source_object = command.source_object + location = source_object.get_location() + + if location == None: + logger.log_errmsg("cmdhandler.match_exits(): Object '%s' has no location." % + source_object) + return + + exits = location.get_contents(filter_type=defines_global.OTYPE_EXIT) + Object = ContentType.objects.get(app_label="objects", + model="object").model_class() + exit_matches = Object.objects.list_search_object_namestr(exits, + command.command_string, + match_type="exact") + if exit_matches: + if test: + return True + + # Only interested in the first match. + targ_exit = exit_matches[0] + # An exit's home is its destination. If the exit has a None home value, + # it's not traversible. + if targ_exit.get_home(): + # SCRIPT: See if the player can traverse the exit + if not targ_exit.scriptlink.default_lock(source_object): + source_object.emit_to("You can't traverse that exit.") + else: + source_object.move_to(targ_exit.get_home()) + else: + source_object.emit_to("That exit leads nowhere.") + # We found a match, kill the command handler. + raise ExitCommandHandler + -def command_table_lookup(command, command_table, eval_perms=True): +def command_table_lookup(command, command_table, eval_perms=True,test=False): """ Performs a command table lookup on the specified command table. Also evaluates the permissions tuple. + The test flag only checks without manipulating the command """ # Get the command's function reference (Or False) cmdtuple = command_table.get_command_tuple(command.command_string) if cmdtuple: + if test: + return True # If there is a permissions element to the entry, check perms. if eval_perms and cmdtuple[1]: if not command.source_object.has_perm_list(cmdtuple[1]): @@ -281,24 +305,30 @@ def command_table_lookup(command, command_table, eval_perms=True): command.command_function = cmdtuple[0] command.extra_vars = cmdtuple[2] return True + -def match_neighbor_ctables(command): +def match_neighbor_ctables(command,test=False): """ Looks through the command tables of neighboring objects for command matches. + test mode just checks if the command is a match, without manipulating + any commands. """ source_object = command.source_object if source_object.location != None: neighbors = source_object.location.get_contents() for neighbor in neighbors: if command_table_lookup(command, - neighbor.scriptlink.command_table): + neighbor.scriptlink.command_table, test=test): # If there was a command match, set the scripted_obj attribute # for the script parent to pick up. + if test: + return True command.scripted_obj = neighbor return True - # No matches - return False + else: + #no matches + return False def handle(command): """ @@ -315,44 +345,52 @@ def handle(command): # Nothing sent in of value, ignore it. raise ExitCommandHandler + state = None #no state by default + if command.session and not command.session.logged_in: # Not logged in, look through the unlogged-in command table. - command_table_lookup(command, cmdtable.GLOBAL_UNCON_CMD_TABLE, - eval_perms=False) - - else: + command_table_lookup(command, cmdtable.GLOBAL_UNCON_CMD_TABLE,eval_perms=False) + else: + # User is logged in. + # Match against the 'idle' command. + match_idle(command) + # See if this is an aliased command. + match_alias(command) - state_name = command.source_object.get_state() - state_cmd_table = statetable.GLOBAL_STATE_TABLE.get_cmd_table(state_name) + state = command.source_object.get_state() + state_cmd_table = statetable.GLOBAL_STATE_TABLE.get_cmd_table(state) - if state_name and state_cmd_table: - # we are in a special state. + if state and state_cmd_table: + # Caller is in a special state. - # check idle command. - match_idle(command) - # check for channel commands - prev_command = command.command_string - match_channel(command) - if prev_command != command.command_string: - # a channel command is handled normally also in the state + state_allow_exits, state_allow_obj_cmds = \ + statetable.GLOBAL_STATE_TABLE.get_state_flags(state) + + state_lookup = True + if match_channel(command): command_table_lookup(command, cmdtable.GLOBAL_CMD_TABLE) - else: - command_table_lookup(command, state_cmd_table) + state_lookup = False + # See if the user is trying to traverse an exit. + if state_allow_exits: + match_exits(command) + # check if this is a command defined on a nearby object. + if state_allow_obj_cmds and match_neighbor_ctables(command): + state_lookup = False + #if nothing has happened to change our mind, search the state table. + if state_lookup: + command_table_lookup(command, state_cmd_table) else: - #normal operation + # Not in a state. Normal operation. + state = None #make sure, in case the object had a malformed statename. - # Match against the 'idle' command. - match_idle(command) - # See if this is an aliased command. - match_alias(command) - # Check if the user is using a channel command. + # Check if the user is using a channel command. match_channel(command) - # See if the user is trying to traverse an exit. + # See if the user is trying to traverse an exit. match_exits(command) - neighbor_match_found = match_neighbor_ctables(command) - if not neighbor_match_found: - # Retrieve the appropriate (if any) command function. - command_table_lookup(command, cmdtable.GLOBAL_CMD_TABLE) + # check if this is a command defined on a nearby object + if not match_neighbor_ctables(command): + command_table_lookup(command, cmdtable.GLOBAL_CMD_TABLE) + """ By this point, we assume that the user has entered a command and not @@ -379,16 +417,37 @@ def handle(command): raise ExitCommandHandler else: # If we reach this point, we haven't matched anything. + + if state: + # if we are in a state, it could be that the command exists, but + # it is temporarily not available. If so, we want a different error message. + if match_exits(command,test=True): + raise CommandNotInState("Movement is not possible right now.") + if match_neighbor_ctables(command,test=True): + raise CommandNotInState("You can not do that at the moment.") + if command_table_lookup(command,cmdtable.GLOBAL_CMD_TABLE,test=True): + raise CommandNotInState("This command is not available right now.") raise UnknownCommand except ExitCommandHandler: # When this is thrown, just get out and do nothing. It doesn't mean # something bad has happened. pass + except CommandNotInState, e: + # The command exists, but not in the current state + if command.source_object != None: + # The logged-in error message + command.source_object.emit_to(e.err_string) + elif command.session != None: + # States are not available before login, so this should never + # be reached. But better safe than sorry. + command.session.msg("%s %s" % (e.err_string," (Type \"help\" for help.)")) + else: + pass except UnknownCommand: # Default fall-through. No valid command match. if command.source_object != None: - # A typical logged in or object-based error message. + # A typical logged in or object-based error message. command.source_object.emit_to("Huh? (Type \"help\" for help.)") elif command.session != None: # This is hit when invalid commands are sent at the login screen diff --git a/src/cmdtable.py b/src/cmdtable.py index ae0916a405..c276326920 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, state=None): + extra_vals=None, auto_help=False, staff_help=False): """ Adds a command to the command table. diff --git a/src/commands/objmanip.py b/src/commands/objmanip.py index 6d4661cd1e..e5c004cdaf 100644 --- a/src/commands/objmanip.py +++ b/src/commands/objmanip.py @@ -290,11 +290,6 @@ def cmd_create(command): target_name = eq_args[0] # Create and set the object up. - # TODO: This dictionary stuff is silly. Feex. - #odat = {"name": target_name, - # "type": defines_global.OTYPE_THING, - # "location": source_object, - # "owner": source_object} new_object = Object.objects.create_object(target_name, defines_global.OTYPE_THING, source_object, @@ -434,11 +429,6 @@ def cmd_open(command): source_object.emit_to("You can't open an exit to an exit!") return - #odat = {"name": exit_name, - # "type": defines_global.OTYPE_EXIT, - # "location": source_object.get_location(), - # "owner": source_object, - # "home": destination} new_object = Object.objects.create_object(exit_name, defines_global.OTYPE_EXIT, source_object.get_location(), @@ -466,11 +456,6 @@ def cmd_open(command): else: # Create an un-linked exit. - #odat = {"name": exit_name, - # "type": defines_global.OTYPE_EXIT, - # "location": source_object.get_location(), - # "owner": source_object, - # "home": None} new_object = Object.objects.create_object(exit_name, defines_global.OTYPE_EXIT, source_object.get_location(), diff --git a/src/events.py b/src/events.py index 63dc92e0d7..ccd15a0c91 100644 --- a/src/events.py +++ b/src/events.py @@ -4,6 +4,8 @@ Holds the global events scheduled in scheduler.py. Events are sub-classed from IntervalEvent (which is not to be used directly). Create your sub-class, call src.scheduler.add_event(YourEventClass()) to add it to the global scheduler. + +Use @ps to view the event list. """ import time from twisted.internet import task @@ -58,7 +60,7 @@ class IntervalEvent(object): """ Returns a value in seconds when the event is going to fire off next. """ - return (self.time_last_executed + self.interval) - time.time() + return max(0,(self.time_last_executed + self.interval) - time.time()) def set_lastfired(self): """ @@ -73,8 +75,6 @@ class IntervalEvent(object): self.set_lastfired() self.event_function() - - class IEvt_Check_Sessions(IntervalEvent): """ Event: Check all of the connected sessions. @@ -99,7 +99,7 @@ class IEvt_Destroy_Objects(IntervalEvent): super(IEvt_Destroy_Objects, self).__init__() self.name = 'IEvt_Destroy_Objects' self.interval = 1800 - self.description = "Destroy objects with the GOING flag set." + self.description = "Clean out objects marked for destruction." def event_function(self): """ diff --git a/src/imc2/events.py b/src/imc2/events.py index 9590a778b3..99769bdd54 100644 --- a/src/imc2/events.py +++ b/src/imc2/events.py @@ -27,7 +27,12 @@ class IEvt_IMC2_Send_IsAlive(events.IntervalEvent): """ This is the function that is fired every self.interval seconds. """ - imc2_conn.IMC2_PROTOCOL_INSTANCE.send_packet(IMC2PacketIsAlive()) + try: + imc2_conn.IMC2_PROTOCOL_INSTANCE.send_packet(IMC2PacketIsAlive()) + except AttributeError: + #this will happen if we are not online in the first place + #(like during development) /Griatch + pass class IEvt_IMC2_Send_Keepalive_Request(events.IntervalEvent): """ @@ -77,4 +82,4 @@ def add_events(): """ scheduler.add_event(IEvt_IMC2_Send_IsAlive()) scheduler.add_event(IEvt_IMC2_Prune_Inactive_Muds()) - scheduler.add_event(IEvt_IMC2_Send_Keepalive_Request()) \ No newline at end of file + scheduler.add_event(IEvt_IMC2_Send_Keepalive_Request()) diff --git a/src/initial_setup.py b/src/initial_setup.py index 32042d6263..0ab91b27fd 100644 --- a/src/initial_setup.py +++ b/src/initial_setup.py @@ -1,5 +1,5 @@ """ -This module handles initial database propagation, which is only ran the first +This module handles initial database propagation, which is only run the first time the game starts. It will create some default channels, objects, and other things. @@ -30,11 +30,14 @@ def create_objects(): """ # Set the initial User's account object's username on the #1 object. god_user = get_god_user() + god_user.is_superuser = True + god_user.is_staff = True # Create the matching PLAYER object in the object DB. god_user_obj = Object(id=1, type=defines_global.OTYPE_PLAYER) god_user_obj.set_name(god_user.username) god_user_obj.set_attribute('desc', 'You are Player #1.') god_user_obj.scriptlink.at_player_creation() + god_user_obj.save() # Limbo is the initial starting room. @@ -91,8 +94,9 @@ def create_connect_screens(): Creates the default connect screen(s). """ ConnectScreen(name="Default", - text="%ch%cb==================================================================%cn\r\n Welcome to Evennia! Please type one of the following to begin:\r\n\r\n If you have an existing account, connect to it by typing:\r\n %chconnect %cn\r\n If you need to create an account, type (without the <>'s):\r\n %chcreate \"\" %cn\r\n%ch%cb==================================================================%cn\r\n", + text="%ch%cb==================================================================%cn\r\n Welcome to %chEvennia%cn! Please type one of the following to begin:\r\n\r\n If you have an existing account, connect to it by typing:\r\n %chconnect %cn\r\n If you need to create an account, type (without the <>'s):\r\n %chcreate \"\" %cn\r\n%ch%cb==================================================================%cn\r\n", is_active=True).save() + def create_aliases(): """ @@ -107,7 +111,7 @@ def create_aliases(): CommandAlias(user_input="l", equiv_command="look").save() CommandAlias(user_input="ex", equiv_command="examine").save() CommandAlias(user_input="sa", equiv_command="say").save() - CommandAlias(user_input="emote", equiv_command="pose").save() + #CommandAlias(user_input="emote", equiv_command="pose").save() CommandAlias(user_input="p", equiv_command="page").save() def import_help_files(): diff --git a/src/objects/managers/object.py b/src/objects/managers/object.py index f802272bc1..73cc461021 100644 --- a/src/objects/managers/object.py +++ b/src/objects/managers/object.py @@ -8,6 +8,7 @@ from django.db import connection from django.contrib.auth.models import User from django.db.models import Q from django.contrib.contenttypes.models import ContentType +from django.conf import settings from src.config.models import ConfigValue from src.objects.exceptions import ObjectNotExist @@ -83,7 +84,6 @@ class ObjectManager(models.Manager): 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]) @@ -124,7 +124,6 @@ class ObjectManager(models.Manager): return dbtotals - def player_alias_search(self, searcher, ostring): """ Search players by alias. Returns a list of objects whose "ALIAS" @@ -288,8 +287,10 @@ class ObjectManager(models.Manager): if otype == defines_global.OTYPE_PLAYER: new_object.owner = None new_object.zone = None + new_object.script_parent = settings.SCRIPT_DEFAULT_PLAYER else: new_object.owner = owner + new_object.script_parent = settings.SCRIPT_DEFAULT_OBJECT if new_object.get_owner().get_zone(): new_object.zone = new_object.get_owner().get_zone() @@ -308,7 +309,7 @@ class ObjectManager(models.Manager): # Rooms have a NULL location. if not new_object.is_room(): - new_object.move_to(location, force_look=False) + new_object.move_to(location, quiet=True, force_look=False) return new_object @@ -350,11 +351,6 @@ class ObjectManager(models.Manager): user = User.objects.get(id=uid) # Create a player object of the same ID in the Objects table. - #odat = {"id": uid, - # "name": uname, - # "type": defines_global.OTYPE_PLAYER, - # "location": start_room_obj, - # "owner": None} user_object = self.create_object(uname, defines_global.OTYPE_PLAYER, start_room_obj, @@ -368,9 +364,12 @@ class ObjectManager(models.Manager): command.session.login(user) logger.log_infomsg('Registration: %s' % user_object.get_name()) - user_object.emit_to("Welcome to %s, %s.\n\r" % ( - ConfigValue.objects.get_configvalue('site_name'), - user_object.get_name(show_dbref=False))) + + #Don't show the greeting; it messes with using the login hooks for + #making character creation wizards. /Griatch + #user_object.emit_to("Welcome to %s, %s.\n\r" % ( + # ConfigValue.objects.get_configvalue('site_name'), + # user_object.get_name(show_dbref=False))) # Add the user to all of the CommChannel objects that are flagged # is_joined_by_default. diff --git a/src/objects/models.py b/src/objects/models.py index ebccb80412..7f7c9d4c18 100755 --- a/src/objects/models.py +++ b/src/objects/models.py @@ -127,7 +127,7 @@ class Object(models.Model): objects = ObjectManager() - #state system can set a particular command table to be used. + #state system can set a particular command table to be used (not persistent). state = None def __cmp__(self, other): @@ -141,6 +141,12 @@ class Object(models.Model): class Meta: ordering = ['-date_created', 'id'] + + def dbref(self): + """Returns the object's dbref id on the form #NN, directly + usable by Object.objects.dbref_search() + """ + return "#%s" % str(self.id) """ BEGIN COMMON METHODS @@ -245,8 +251,7 @@ class Object(models.Model): Returns True if the object is a staff player. """ if not self.is_player(): - return False - + return False try: profile = self.get_user_account() return profile.is_staff @@ -303,7 +308,7 @@ class Object(models.Model): user. This form accepts an iterable of strings representing permissions, if the user has any of them return true. - perm: (iterable) An iterable of strings of permissions. + perm_list: (iterable) An iterable of strings of permissions. """ if not self.is_player(): return False @@ -705,9 +710,9 @@ class Object(models.Model): # in SQLite. flags = str(self.flags).split() nosave_flags = str(self.nosave_flags).split() - return flag in flags or flag in nosave_flags + return flag.upper() in flags or flag in nosave_flags - def set_flag(self, flag, value): + def set_flag(self, flag, value=True): """ Add a flag to our object's flag list. @@ -735,7 +740,7 @@ class Object(models.Model): # Object doesn't have the flag to begin with. pass elif value == True and has_flag: - # We've already go it. + # We've already got it. pass else: # Setting a flag. @@ -755,6 +760,9 @@ class Object(models.Model): flags.append(flag) self.flags = ' '.join(flags) self.save() + + def unset_flag(self, flag): + self.set_flag(flag,value=False) def is_connected_plr(self): """ @@ -885,8 +893,13 @@ class Object(models.Model): target: (Object) Reference to the object to move to. quiet: (bool) If true, don't emit left/arrived messages. - force_look: (bool) If true and target is a player, make them 'look'. + force_look: (bool) If true and self is a player, make them 'look'. """ + + #before the move, call the appropriate hook + if self.scriptlink.at_before_move(target) != None: + return + if not quiet: location = self.get_location() if location: @@ -897,17 +910,21 @@ class Object(models.Model): (self.get_name())) self.location = target - self.save() + self.save() if not quiet: arrival_message = "%s has arrived." % (self.get_name()) self.get_location().emit_to_contents(arrival_message, exclude=self) if self.location.is_player(): self.location.emit_to("%s is now in your inventory." % (self.get_name())) - + + #execute eventual extra commands on this object after moving it + self.scriptlink.at_after_move() + if force_look: self.execute_cmd('look') - + + def dbref_match(self, oname): """ Check if the input (oname) can be used to identify this particular object diff --git a/src/scheduler.py b/src/scheduler.py index 65fc8c12af..6fca7cac3f 100644 --- a/src/scheduler.py +++ b/src/scheduler.py @@ -21,8 +21,9 @@ def add_event(event): * event: (IntervalEvent) The event to add to the scheduler. """ - #don't add multiple instances of the same event - if event in schedule: + #don't add multiple instances of the same event, instead replace + if event in schedule: + schedule[schedule.index(event)] = event return else: schedule.append(event) diff --git a/src/script_parents/basicobject.py b/src/script_parents/basicobject.py index 7c4e111cc3..12c0a4a08e 100644 --- a/src/script_parents/basicobject.py +++ b/src/script_parents/basicobject.py @@ -72,6 +72,24 @@ class EvenniaBasicObject(object): #print "SCRIPT TEST: %s got %s." % (pobject, self.scripted_obj) pass + def at_before_move(self, target_location): + """ + This hook is called just before the object is moved. + arg: + target_location (obj): the place where this object is to be moved + returns: + if this function returns anything but None, the move is cancelled. + + """ + pass + + def at_after_move(self): + """ + This hook is called just after the object was successfully moved. + No return values. + """ + pass + def at_drop(self, pobject): """ Perform this action when someone uses the DROP command on the object. diff --git a/src/script_parents/basicplayer.py b/src/script_parents/basicplayer.py index 67a8204e16..4cf428b958 100644 --- a/src/script_parents/basicplayer.py +++ b/src/script_parents/basicplayer.py @@ -44,3 +44,32 @@ class EvenniaBasicPlayer(object): pobject.get_location().emit_to_contents("%s has connected." % (pobject.get_name(show_dbref=False),), exclude=pobject) pobject.execute_cmd("look") + + def at_before_move(self, target_location): + """ + This hook is called just before the object is moved. + Input: + target_location (obj): The location the player is about to move to. + Return value: + If this function returns anything but None (no return value), + the move is aborted. This allows for character-based move + restrictions (not only exit locks). + """ + pass + + def at_after_move(self): + """ + This hook is called just after the player has been successfully moved. + """ + pass + + + def at_disconnect(self): + """ + This is called just before the session disconnects, for whatever reason. + """ + pobject = self.scripted_obj + + location = pobject.get_location() + if location != None: + location.emit_to_contents("%s has disconnected." % (pobject.get_name(show_dbref=False),), exclude=pobject) diff --git a/src/session.py b/src/session.py index ebcc7d8d15..0e0c3699ba 100755 --- a/src/session.py +++ b/src/session.py @@ -20,7 +20,7 @@ import ansi class SessionProtocol(StatefulTelnetProtocol): """ - This class represents a player's sesssion. From here we branch down into + This class represents a player's session. From here we branch down into other various classes, please try to keep this one tidy! """ @@ -110,12 +110,12 @@ class SessionProtocol(StatefulTelnetProtocol): """ pobject = self.get_pobject() if pobject: - pobject.set_flag("CONNECTED", False) - - location = pobject.get_location() - if location != None: - location.emit_to_contents("%s has disconnected." % (pobject.get_name(show_dbref=False),), exclude=pobject) + #call hook function + pobject.scriptlink.at_disconnect() + + pobject.set_flag("CONNECTED", False) + uaccount = pobject.get_user_account() uaccount.last_login = datetime.now() uaccount.save() @@ -169,9 +169,8 @@ class SessionProtocol(StatefulTelnetProtocol): # This will cache with the first call of this function. self.get_pobject() #session_mgr.disconnect_duplicate_session(self) - + self.pobject.scriptlink.at_pre_login(self) - self.pobject.scriptlink.at_post_login(self) logger.log_infomsg("Logged in: %s" % self) self.cemit_info('Logged in: %s' % self) @@ -184,6 +183,9 @@ class SessionProtocol(StatefulTelnetProtocol): if self.pobject.name != user.username: self.pobject.set_name(user.username) self.pobject.save() + + self.pobject.scriptlink.at_post_login(self) + def msg(self, message): """ @@ -198,6 +200,15 @@ class SessionProtocol(StatefulTelnetProtocol): Adds the player to the default channels. """ # Add the default channels. + if self.pobject.is_superuser(): + "Have the super users join these too." + chan = CommChannel.objects(name=settings.COMMCHAN_MUD_INFO) + chan_alias = 'info' + src.comsys.plr_set_channel(self, chan_alias, chan.name, True) + chan = CommChannel.objects(name=settings.COMMCHAN_MUD_CONNECTIONS) + chan_alias = 'conns' + src.comsys.plr_set_channel(self, chan_alias, chan.name, True) + for chan in CommChannel.objects.filter(is_joined_by_default=True): logger.log_infomsg("ADDING BY DEFAULT %s" % chan) chan_alias = chan.get_default_chan_alias() diff --git a/src/statetable.py b/src/statetable.py index dec786718c..d7d28a5083 100644 --- a/src/statetable.py +++ b/src/statetable.py @@ -1,11 +1,13 @@ """ -The state system allows the player to enter a state where only a special set of commands +The state system allows the player to enter states/modes where only a special set of commands are available. This can be used for all sorts of useful things: - - in-game menus (picking an option from a list) + - in-game menus (picking an option from a list, much more powerful than using 'rooms') - inline text editors (entering text into a buffer) - npc conversation (select replies) - - commands only available while in 'combat mode' etc -This allows for more power than using rooms to build 'menus'. + - only allowing certain commands in special situations (e.g. special attack commands + when in 'combat' mode) + - adding special commands to the normal set (e.g. a 'howl' command when in 'werewolf' state) + - deactivating certain commands (e.g. removing the 'say' command in said 'werewolf' state ...) Basically the GLOBAL_STATE_TABLE contains a dict with command tables keyed after the name of the state. To use, a function must set the 'state' variable on a player object @@ -15,97 +17,181 @@ of the normal global command table. The state system is pluggable, in the same way that commands are added to the global command table, commands are added to the GLOBAL_STATE_TABLE using add_command supplying in -addition the name of the state. +addition the name of the state. The main difference is that new states must first be created +using the GLOBAL_STATE_TABLE.add_state() command. See examples in game/gamesrc. """ -from cmdtable import CommandTable +from cmdtable import CommandTable, GLOBAL_CMD_TABLE from logger import log_errmsg import src.helpsys.management.commands.edit_helpfiles as edit_help - class StateTable(object): state_table = None - def __init__(self): - self.state_table = {} - self.help_index = StateHelpIndex() + def __init__(self): + self.state_table = {} #of the form {statename:CommandTable, ...} + self.help_index = StateHelpIndex() + self.state_flags = {} + + def add_state(self, state_name, + global_cmds=None, global_filter=[], + allow_exits=False, allow_obj_cmds=False, + exit_command=False): + """ + Creates a new game state. Each state has its own unique set of commands and + can change how the game works. + + state_name (str) : name of the state. If state already exists, + it will be overwritten. + global_cmds(None or str): A flag to control imports of global commands into the state. + None - Do not include any global commands in this state (default). + 'all' - Include all global commands from GLOBAL_CMD_TABLE, + without using the global_filter argument at all. + 'include' - Include only global commands from GLOBAL_CMD_TABLE + that are listed in global_filter. OBS:the global + 'help' command will always automatically be included. + 'exclude' - Include all global commands from GLOBAL_CMD_TABLE + /except/ those listed in global_filter. + global_filter (list): This should be a list of command strings (like + ['look', '@create',...]). Depending on the global_cmds setting, this + list is used to control which global commands are imported from + GLOBAL_CMD_TABLE into this state. + allow_exits (bool): Evennia works so that an exit might have any name. A user + may just write e.g. 'outside' and if there is an exit in the room + named 'outside' the engine interprets this as a command for + traversing this exit. Normally a state disables this check, so + a user may not traverse exits while in the state. This switch turns + the check back on and allows users to move around also + while in the state. Observe that to make this work well you will + have to have at least a 'look' command defined in your state + (since this is called whenever you enter a new room). + allow_obj_cmds (bool): Any object in a room can have its own command table assigned + to it (such as the ability to 'play' on a flute). Normally these + commands are not accessible while in a state. This switch + allows the interpreter to also search objects for commands and will + use them before using any same-named state commands. + exit_command(bool): Adds a special '@exit' command that immediately quits the + state. This is useful for testing. Many states might however require + special conditions or clean-up operations before allowing a player + to exit (e.g. combat states and text editors), in which case this + feature should be turned off and handled by custom exit commands. + """ + + state_name = state_name.strip() + #create state + self.state_table[state_name] = CommandTable() + + if global_cmds != None: + if global_cmds == 'all': + f = lambda c: True + elif global_cmds == 'include': + f = lambda c: c in global_filter + if not 'help' in global_filter: + global_filter.append('help') + elif global_cmds == 'exclude': + f = lambda c: c not in global_filter + else: + log_errmsg("ERROR: in statetable, state %s: Unknown global_cmds flag '%s'." % + (state_name, global_cmds)) + return + for cmd in filter(f,GLOBAL_CMD_TABLE.ctable.keys()): + self.state_table[state_name].ctable[cmd] = \ + GLOBAL_CMD_TABLE.get_command_tuple(cmd) + if exit_command: + #if we import global commands, we use the normal help index; thus add + #help for @exit to the global index. + self.state_table[state_name].add_command("@exit", + cmd_state_exit, + auto_help=True) + else: + #when no global cmds are imported, we create a small custom + #state-based help index instead + self.help_index.add_state(state_name) + self.add_command(state_name,'help',cmd_state_help) + + if exit_command: + #add the @exit command + self.state_table[state_name].add_command("@exit", + cmd_state_exit) + self.help_index.add_state_help(state_name, "@exit", + cmd_state_exit.__doc__) + #store special state flags + self.state_flags[state_name] = {} + self.state_flags[state_name]['exits'] = allow_exits + self.state_flags[state_name]['obj_cmds'] = allow_obj_cmds + + def del_state(self, state_name): + """ + Permanently deletes a state from the state table. Make sure no users are in + the state upon calling this command. Note that setting an object to a + non-existing state name is harmless, if the state does not exist the + interpreter ignores it and assumes normal operation. + Auto-created global help entries will have to be deleted manually. + """ + if self.state_table.has_key(state_name): + del self.state_table[state_name] + + + def del_command(self, state_name, command_string): + """ + Deactivate a command within a state. This is mostly useful for states that + also includes the full global command table, allowing for deactivating individual + commands dynamically. + + state_name (str) : name of the state to delete a command from + command_string (str) : command name to deactivate, e.g. @edit, look etc + """ + + if not self.state_table[state_name].has_key(): + return + try: + del self.state_table[state_name].ctable[command_string] + err = self.help_index.del_state_help(state_name,command_string) + except KeyError: + pass def add_command(self, state_name, command_string, function, priv_tuple=None, - extra_vals=None, auto_help=False, staff_help=False, help_global=False, - exit_command=True): + extra_vals=None, auto_help=False, staff_help=False): """ - Access function; transparently add commands to a specific command table to - represent a particular state. This command is similar to the normal + Transparently add commands to a specific state. + This command is similar to the normal command_table.add_command() function. See example in gamesrc/commands/examples. + state_name: (str) identifier of the state we tie this to. command_string: (str) command name to run, like look, @list etc function: (reference) command function object - state_name: (str) identifier of the state we tie this to. priv_tuple: (tuple) String tuple of permissions required for command. extra_vals: (dict) Dictionary to add to the Command object. - auto_help: (bool) Activate the auto_help system. By default this stores the help inside the statetable only (not in the main help database), and so the help entries are only available when the player is actually inside the state. Note that the auto_help system of state-commands do not support <> markup. staff_help: (bool) Help entry is only available for staff players. - help_global: (bool) Also auto_add the help entry to the main help database. Be - careful with overwriting same-named help topics if you define special - versions of commands inside your state. - - exit_command: (bool) Sets if the default @exit command is added to the state. Only - one command needs to include this statement in order to add @exit. This is - usually a good idea to make sure the player is not stuck, but you might want - to turn this off if you know what you're doing and want to avoid players - 'escaping' the state (like in a combat state or similar), or when - you need to do some special final cleanup or save operations before - exiting (like in a text editor). """ - if not state_name: - log_errmsg("Command %s was not given a state. Not added." % command_string) + if state_name not in self.state_table.keys(): + log_errmsg("State %s is not a valid state for command %s. Not added." % \ + (state_name, command_string)) return state_name = state_name.strip() - if not self.state_table.has_key(state_name): - - #create the state - self.state_table[state_name] = CommandTable() - #always add a help index even though it might not be used. - self.help_index.add_state(state_name) - - if exit_command: - #add the @exit command - self.state_table[state_name].add_command("@exit", - cmd_state_exit, - auto_help=True) - if auto_help: - #add help for @exit command - self.help_index.add_state_help(state_name, "@exit", - cmd_state_exit.__doc__) - - #handle auto-help for the state + #handle auto-help for the state commands if auto_help: + if self.help_index.has_state(state_name): + #add the help text to state help index only, don't add + #it to the global help index + self.help_index.add_state_help(state_name,command_string, + function.__doc__, + staff_help=staff_help) + auto_help = False - #make sure the state's help command is in place - self.state_table[state_name].add_command('help',cmd_state_help) - - #add the help text - helptext = function.__doc__ - self.help_index.add_state_help(state_name,command_string, - helptext,staff_only=staff_help) - if not help_global: - #if we don't want global help access, we need to - #turn off auto_help before adding the command. - auto_help = False - - #finally add the new command to the state + #finally add the new command to the state's command table self.state_table[state_name].add_command(command_string, - function, priv_tuple, - extra_vals,auto_help, - staff_help) + function, priv_tuple, + extra_vals,auto_help=auto_help, + staff_help=staff_help) def get_cmd_table(self, state_name): """ @@ -115,6 +201,16 @@ class StateTable(object): return self.state_table[state_name] else: return None + + def get_state_flags(self, state_name): + """ + Return the state flags for a particular state. + """ + if self.state_flags.has_key(state_name): + return self.state_flags[state_name]['exits'],\ + self.state_flags[state_name]['obj_cmds'] + else: + return False, False class StateHelpIndex(object): @@ -130,6 +226,9 @@ class StateHelpIndex(object): "Create a new state" self.help_index[state_name] = {} + def has_state(self,state_name): + return self.help_index.has_key(state_name) + def add_state_help(self, state,command,text,staff_only=False): """Store help for a command under a certain state. Supports <> and <> markup.""" @@ -147,7 +246,7 @@ class StateHelpIndex(object): self.help_index[state][command] = (staff_only, text) def del_state_help(self, state, topic): - """Manually delete an help topic from state help system. Note that this is + """Manually delete a help topic from state help system. Note that this is only going to last until the next @reload unless you also turn off auto_help for the relevant topic.""" if self.help_index.has_key(state) and self.help_index[state].has_key(topic): @@ -185,21 +284,21 @@ class StateHelpIndex(object): def cmd_state_exit(command): """@exit (when in a state) - This command only works when inside a special game 'state' (like a menu or - editor or similar place where you have fewer commands available than normal). + This command only works when inside certain special game 'states' (like a menu or + editor or similar situations). It aborts what you were doing and force-exits back to the normal mode of - gameplay. Some states might deactivate the @exit command for various reasons. - In those cases, read the help when in the state to learn more.""" + gameplay. Some states might deactivate the @exit command for various reasons.""" source_object = command.source_object source_object.clear_state() source_object.emit_to("... Exited.") source_object.execute_cmd('look') + def cmd_state_help(command): """ - help (while in a state) + help (while in a special state) In-state help system. This is NOT tied to the normal help system and is not stored in the database. It is intended as a quick @@ -248,5 +347,5 @@ def cmd_state_help(command): else: source_object.emit_to("No help available on %s." % args) - +#import this into modules GLOBAL_STATE_TABLE = StateTable()