diff --git a/evennia/commands/default/general.py b/evennia/commands/default/general.py index e06d930ac3..c5075b8744 100644 --- a/evennia/commands/default/general.py +++ b/evennia/commands/default/general.py @@ -417,7 +417,7 @@ class CmdSay(COMMAND_DEFAULT_CLASS): return # Call the at_after_say hook on the character - caller.at_say(speech) + caller.at_say(speech, msg_self=True) class CmdWhisper(COMMAND_DEFAULT_CLASS): @@ -425,10 +425,11 @@ class CmdWhisper(COMMAND_DEFAULT_CLASS): Speak privately as your character to another Usage: - whisper = + whisper = + whisper , = = ") + caller.msg("Usage: whisper = ") return - receiver = caller.search(self.lhs) + receivers = [recv.strip() for recv in self.lhs.split(",")] - if not receiver: - return + receivers = [caller.search(receiver) for receiver in receivers] + receivers = [recv for recv in receivers if recv] speech = self.rhs - # Call a hook to change the speech before whispering - speech = caller.at_before_say(speech, whisper=True, receiver=receiver) - # If the speech is empty, abort the command - if not speech: + if not speech or not receivers: return - # Call the at_after_whisper hook for feedback - caller.at_say(speech, receiver=receiver, whisper=True) + # Call a hook to change the speech before whispering + speech = caller.at_before_say(speech, whisper=True, receivers=receivers) + + # no need for self-message if we are whispering to ourselves (for some reason) + msg_self = None if caller in receivers else True + caller.at_say(speech, msg_self=msg_self, receivers=receivers, whisper=True) class CmdPose(COMMAND_DEFAULT_CLASS): diff --git a/evennia/contrib/mail.py b/evennia/contrib/mail.py index 0f82fad47f..48369083d1 100644 --- a/evennia/contrib/mail.py +++ b/evennia/contrib/mail.py @@ -55,6 +55,7 @@ class CmdMail(default_cmds.MuxCommand): @mail/delete 6 @mail/forward feend78 Griatch=4/You guys should read this. @mail/reply 9=Thanks for the info! + """ key = "@mail" aliases = ["mail"] @@ -86,6 +87,7 @@ class CmdMail(default_cmds.MuxCommand): Returns: messages (list): list of Msg objects. + """ # mail_messages = Msg.objects.get_by_tag(category="mail") # messages = [] @@ -105,6 +107,7 @@ class CmdMail(default_cmds.MuxCommand): subject (str): The header or subject of the message to be delivered. message (str): The body of the message being sent. caller (obj): The object (or Account or Character) that is sending the message. + """ for recipient in recipients: recipient.msg("You have received a new @mail from %s" % caller) @@ -130,7 +133,8 @@ class CmdMail(default_cmds.MuxCommand): return else: all_mail = self.get_all_mail() - mind = int(self.lhs) - 1 + mind_max = all_mail.count() - 1 + mind = max(0, min(mind_max, int(self.lhs) - 1)) if all_mail[mind]: all_mail[mind].delete() self.caller.msg("Message %s deleted" % self.lhs) @@ -150,9 +154,10 @@ class CmdMail(default_cmds.MuxCommand): return else: all_mail = self.get_all_mail() + mind_max = all_mail.count() - 1 if "/" in self.rhs: - message_number, message = self.rhs.split("/") - mind = int(message_number) - 1 + message_number, message = self.rhs.split("/", 1) + mind = max(0, min(mind_max, int(message_number) - 1)) if all_mail[mind]: old_message = all_mail[mind] @@ -164,7 +169,7 @@ class CmdMail(default_cmds.MuxCommand): else: raise IndexError else: - mind = int(self.rhs) - 1 + mind = max(0, min(mind_max, int(self.rhs) - 1)) if all_mail[mind]: old_message = all_mail[mind] self.send_mail(self.search_targets(self.lhslist), "FWD: " + old_message.header, @@ -188,7 +193,8 @@ class CmdMail(default_cmds.MuxCommand): return else: all_mail = self.get_all_mail() - mind = int(self.lhs) - 1 + mind_max = all_mail.count() - 1 + mind = max(0, min(mind_max, int(self.lhs) - 1)) if all_mail[mind]: old_message = all_mail[mind] self.send_mail(old_message.senders, "RE: " + old_message.header, @@ -211,8 +217,11 @@ class CmdMail(default_cmds.MuxCommand): body = self.rhs self.send_mail(self.search_targets(self.lhslist), subject, body, self.caller) else: + all_mail = self.get_all_mail() + mind_max = all_mail.count() - 1 try: - message = self.get_all_mail()[int(self.lhs) - 1] + mind = max(0, min(mind_max, self.lhs - 1)) + message = all_mail[mind] except (ValueError, IndexError): self.caller.msg("'%s' is not a valid mail id." % self.lhs) return diff --git a/evennia/contrib/multidescer.py b/evennia/contrib/multidescer.py index 32b2c00d53..cf6c5ec039 100644 --- a/evennia/contrib/multidescer.py +++ b/evennia/contrib/multidescer.py @@ -16,7 +16,7 @@ also adds the short descriptions and the `sdesc` command). Installation: Edit `mygame/commands/default_cmdsets.py` and add -`from contrib.multidesc import CmdMultiDesc` to the top. +`from evennia.contrib.multidescer import CmdMultiDesc` to the top. Next, look up the `at_cmdset_create` method of the `CharacterCmdSet` class and add a line `self.add(CmdMultiDesc())` to the end diff --git a/evennia/contrib/rpsystem.py b/evennia/contrib/rpsystem.py index e211d5b71a..ed74e2286d 100644 --- a/evennia/contrib/rpsystem.py +++ b/evennia/contrib/rpsystem.py @@ -858,7 +858,7 @@ class CmdSay(RPCommand): # replaces standard say return # calling the speech hook on the location - speech = caller.location.at_say(caller, self.args) + speech = caller.location.at_before_say(caller, self.args) # preparing the speech with sdesc/speech parsing. speech = "/me says, \"{speech}\"".format(speech=speech) targets = self.caller.location.contents diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 0181aef740..4479da8af7 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -822,10 +822,8 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): returns the new clone name on the form keyXX """ key = self.key - num = 1 - for inum in (obj for obj in self.location.contents - if obj.key.startswith(key) and obj.key.lstrip(key).isdigit()): - num += 1 + num = sum(1 for obj in self.location.contents + if obj.key.startswith(key) and obj.key.lstrip(key).isdigit()) return "%s%03i" % (key, num) new_key = new_key or find_clone_key() return ObjectDB.objects.copy_object(self, new_key=new_key) @@ -1205,7 +1203,7 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): mapping.update({ "object": self, - "exit": exits[0] if exits else "somwhere", + "exit": exits[0] if exits else "somewhere", "origin": location or "nowhere", "destination": destination or "nowhere", }) @@ -1562,7 +1560,7 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): a say. This is sent by the whisper command by default. Other verbal commands could use this hook in similar ways. - receiver (Object): If set, this is a target for the say/whisper. + receivers (Object or iterable): If set, this is the target or targets for the say/whisper. Returns: message (str): The (possibly modified) text to be spoken. @@ -1571,7 +1569,7 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): return message def at_say(self, message, msg_self=None, msg_location=None, - receiver=None, msg_receiver=None, mapping=None, **kwargs): + receivers=None, msg_receivers=None, **kwargs): """ Display the actual say (or whisper) of self. @@ -1582,69 +1580,98 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): re-writing it completely. Args: - message (str): The text to be conveyed by self. - msg_self (str, optional): The message to echo to self. + message (str): The message to convey. + msg_self (bool or str, optional): If boolean True, echo `message` to self. If a string, + return that message. If False or unset, don't echo to self. msg_location (str, optional): The message to echo to self's location. - receiver (Object, optional): An eventual receiver of the message + receivers (Object or iterable, optional): An eventual receiver or receivers of the message (by default only used by whispers). - msg_receiver(str, optional): Specific message for receiver only. - mapping (dict, optional): Additional mapping in messages. + msg_receivers(str): Specific message to pass to the receiver(s). This will parsed + with the {receiver} placeholder replaced with the given receiver. Kwargs: whisper (bool): If this is a whisper rather than a say. Kwargs can be used by other verbal commands in a similar way. + mapping (dict): Pass an additional mapping to the message. Notes: - Messages can contain {} markers, which must - If used, `msg_self`, `msg_receiver` and `msg_location` should contain - references to other objects between braces, the way `location.msg_contents` - would allow. For instance: + + Messages can contain {} markers. These are substituted against the values + passed in the `mapping` argument. + msg_self = 'You say: "{speech}"' msg_location = '{object} says: "{speech}"' - msg_receiver = '{object} whispers: "{speech}"' + msg_receivers = '{object} whispers: "{speech}"' - The following mappings can be used in both messages: - object: the object speaking. - location: the location where object is. - speech: the text spoken by self. - - You can use additional mappings if you want to add other - information in your messages. + Supported markers by default: + {self}: text to self-reference with (default 'You') + {speech}: the text spoken/whispered by self. + {object}: the object speaking. + {receiver}: replaced with a single receiver only for strings meant for a specific + receiver (otherwise 'None'). + {all_receivers}: comma-separated list of all receivers, + if more than one, otherwise same as receiver + {location}: the location where object is. """ + msg_type = 'say' if kwargs.get("whisper", False): # whisper mode - msg_self = msg_self or 'You whisper to {receiver}, "{speech}"|n' - msg_receiver = msg_receiver or '{object} whispers: "{speech}"|n' + msg_type = 'whisper' + msg_self = '{self} whisper to {all_receivers}, "{speech}"' if msg_self is True else msg_self + msg_receivers = '{object} whispers: "{speech}"' msg_location = None else: - msg_self = msg_self or 'You say, "{speech}"|n' - msg_receiver = None - msg_location = msg_location or '{object} says, "{speech}"|n' + msg_self = '{self} say, "{speech}"' if msg_self is True else msg_self + msg_receivers = None + msg_location = msg_location or '{object} says, "{speech}"' - mapping = mapping or {} - mapping.update({ - "object": self, - "location": self.location, - "speech": message, - "receiver": receiver - }) + custom_mapping = kwargs.get('mapping', {}) + receivers = make_iter(receivers) if receivers else None + location = self.location if msg_self: - self_mapping = {key: "yourself" if key == "receiver" and val is self - else val.get_display_name(self) if hasattr(val, "get_display_name") - else str(val) for key, val in mapping.items()} - self.msg(msg_self.format(**self_mapping)) + self_mapping = {"self": "You", + "object": self.get_display_name(self), + "location": location.get_display_name(self) if location else None, + "receiver": None, + "all_receivers": ", ".join( + recv.get_display_name(self) + for recv in receivers) if receivers else None, + "speech": message} + self_mapping.update(custom_mapping) + self.msg(text=(msg_self.format(**self_mapping), {"type": msg_type})) - if receiver and msg_receiver: - receiver_mapping = {key: val.get_display_name(receiver) - if hasattr(val, "get_display_name") - else str(val) for key, val in mapping.items()} - receiver.msg(msg_receiver.format(**receiver_mapping)) + if receivers and msg_receivers: + receiver_mapping = {"self": "You", + "object": None, + "location": None, + "receiver": None, + "all_receivers": None, + "speech": message} + for receiver in make_iter(receivers): + individual_mapping = {"object": self.get_display_name(receiver), + "location": location.get_display_name(receiver), + "receiver": receiver.get_display_name(receiver), + "all_receivers": ", ".join( + recv.get_display_name(recv) + for recv in receivers) if receivers else None} + receiver_mapping.update(individual_mapping) + receiver_mapping.update(custom_mapping) + receiver.msg(text=(msg_receivers.format(**receiver_mapping), {"type": msg_type})) if self.location and msg_location: - self.location.msg_contents(msg_location, exclude=(self, ), - mapping=mapping) + location_mapping = {"self": "You", + "object": self, + "location": location, + "all_receivers": ", ".join(recv for recv in receivers) if receivers else None, + "receiver": None, + "speech": message} + location_mapping.update(custom_mapping) + self.location.msg_contents(text=(msg_location, {"type": msg_type}), + from_obj=self, + exclude=(self, ) if msg_self else None, + mapping=location_mapping) # diff --git a/evennia/typeclasses/migrations/0010_delete_old_player_tables.py b/evennia/typeclasses/migrations/0010_delete_old_player_tables.py index b3881b68ae..30abe534b1 100644 --- a/evennia/typeclasses/migrations/0010_delete_old_player_tables.py +++ b/evennia/typeclasses/migrations/0010_delete_old_player_tables.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals from django.db import migrations, connection +_ENGINE = None def _table_exists(db_cursor, tablename): "Returns bool if table exists or not" @@ -11,9 +12,25 @@ def _table_exists(db_cursor, tablename): def _drop_table(db_cursor, table_name): + global _ENGINE + if not _ENGINE: + from django.conf import settings + try: + _ENGINE = settings.DATABASES["default"]["ENGINE"] + except KeyError: + _ENGINE = settings.DATABASE_ENGINE + if _table_exists(db_cursor, table_name): - sql_drop = "DROP TABLE %s;" % table_name - db_cursor.execute(sql_drop) + if _ENGINE == "django.db.backends.mysql": + db_cursor.execute("SET FOREIGN_KEY_CHECKS=0;") + db_cursor.execute("DROP TABLE {table};".format(table=table_name)) + db_cursor.execute("SET FOREIGN_KEY_CHECKS=1;") + elif _ENGINE == "postgresql_psycopg2": + db_cursor.execute("ALTER TABLE {table} DISABLE TRIGGER ALL;".format(table=table_name)) + db_cursor.execute("DROP TABLE {table};".format(table=table_name)) + db_cursor.execute("ALTER TABLE {table} ENABLE TRIGGER ALL;".format(table=table_name)) + else: # sqlite3, other databases + db_cursor.execute("DROP TABLE {table};".format(table=table_name)) def drop_tables(apps, schema_migrator): @@ -23,6 +40,9 @@ def drop_tables(apps, schema_migrator): _drop_table(db_cursor, "players_playerdb_db_tags") _drop_table(db_cursor, "players_playerdb_groups") _drop_table(db_cursor, "players_playerdb_user_permissions") + _drop_table(db_cursor, "comms_msg_db_sender_players") + _drop_table(db_cursor, "comms_msg_db_receivers_players") + _drop_table(db_cursor, "comms_msg_db_hide_from_players") class Migration(migrations.Migration): diff --git a/evennia/typeclasses/models.py b/evennia/typeclasses/models.py index cdbf8a91d6..8a20ba9148 100644 --- a/evennia/typeclasses/models.py +++ b/evennia/typeclasses/models.py @@ -674,8 +674,9 @@ class TypedObject(SharedMemoryModel): Displays the name of the object in a viewer-aware manner. Args: - looker (TypedObject): The object or account that is looking - at/getting inforamtion for this object. + looker (TypedObject, optional): The object or account that is looking + at/getting inforamtion for this object. If not given, some + 'safe' minimum level should be returned. Returns: name (str): A string containing the name of the object, diff --git a/evennia/utils/batchprocessors.py b/evennia/utils/batchprocessors.py index c3a16a1575..a35e6d37f8 100644 --- a/evennia/utils/batchprocessors.py +++ b/evennia/utils/batchprocessors.py @@ -180,7 +180,7 @@ from django.conf import settings from evennia.utils import utils _ENCODINGS = settings.ENCODINGS -_RE_INSERT = re.compile(r"^\#INSERT (.*)", re.MULTILINE) +_RE_INSERT = re.compile(r"^\#INSERT (.*)$", re.MULTILINE) _RE_CLEANBLOCK = re.compile(r"^\#.*?$|^\s*$", re.MULTILINE) _RE_CMD_SPLIT = re.compile(r"^\#.*?$", re.MULTILINE) _RE_CODE_OR_HEADER = re.compile(r"((?:\A|^)#CODE|(?:/A|^)#HEADER|\A)(.*?)$(.*?)(?=^#CODE.*?$|^#HEADER.*?$|\Z)", @@ -273,15 +273,10 @@ class BatchCommandProcessor(object): def replace_insert(match): """Map replace entries""" - return "\n#".join(self.parse_file(match.group(1))) + return "\n#\n".join(self.parse_file(match.group(1))) - # insert commands from inserted files text = _RE_INSERT.sub(replace_insert, text) - # re.sub(r"^\#INSERT (.*?)", replace_insert, text, flags=re.MULTILINE) - # get all commands commands = _RE_CMD_SPLIT.split(text) - # re.split(r"^\#.*?$", text, flags=re.MULTILINE) - # remove eventual newline at the end of commands commands = [c.strip('\r\n') for c in commands] commands = [c for c in commands if c] diff --git a/evennia/utils/inlinefuncs.py b/evennia/utils/inlinefuncs.py index 8baa89cabc..e103e217d7 100644 --- a/evennia/utils/inlinefuncs.py +++ b/evennia/utils/inlinefuncs.py @@ -283,64 +283,64 @@ def parse_inlinefunc(string, strip=False, **kwargs): # no cached stack; build a new stack and continue stack = ParseStack() - # process string on stack - ncallable = 0 - for match in _RE_TOKEN.finditer(string): - gdict = match.groupdict() - if gdict["singlequote"]: - stack.append(gdict["singlequote"]) - elif gdict["doublequote"]: - stack.append(gdict["doublequote"]) - elif gdict["end"]: - if ncallable <= 0: - stack.append(")") - continue - args = [] - while stack: - operation = stack.pop() - if callable(operation): - if not strip: - stack.append((operation, [arg for arg in reversed(args)])) - ncallable -= 1 - break + # process string on stack + ncallable = 0 + for match in _RE_TOKEN.finditer(string): + gdict = match.groupdict() + if gdict["singlequote"]: + stack.append(gdict["singlequote"]) + elif gdict["doublequote"]: + stack.append(gdict["doublequote"]) + elif gdict["end"]: + if ncallable <= 0: + stack.append(")") + continue + args = [] + while stack: + operation = stack.pop() + if callable(operation): + if not strip: + stack.append((operation, [arg for arg in reversed(args)])) + ncallable -= 1 + break + else: + args.append(operation) + elif gdict["start"]: + funcname = _RE_STARTTOKEN.match(gdict["start"]).group(1) + try: + # try to fetch the matching inlinefunc from storage + stack.append(_INLINE_FUNCS[funcname]) + except KeyError: + stack.append(_INLINE_FUNCS["nomatch"]) + stack.append(funcname) + ncallable += 1 + elif gdict["escaped"]: + # escaped tokens + token = gdict["escaped"].lstrip("\\") + stack.append(token) + elif gdict["comma"]: + if ncallable > 0: + # commas outside strings and inside a callable are + # used to mark argument separation - we use None + # in the stack to indicate such a separation. + stack.append(None) else: - args.append(operation) - elif gdict["start"]: - funcname = _RE_STARTTOKEN.match(gdict["start"]).group(1) - try: - # try to fetch the matching inlinefunc from storage - stack.append(_INLINE_FUNCS[funcname]) - except KeyError: - stack.append(_INLINE_FUNCS["nomatch"]) - stack.append(funcname) - ncallable += 1 - elif gdict["escaped"]: - # escaped tokens - token = gdict["escaped"].lstrip("\\") - stack.append(token) - elif gdict["comma"]: - if ncallable > 0: - # commas outside strings and inside a callable are - # used to mark argument separation - we use None - # in the stack to indicate such a separation. - stack.append(None) + # no callable active - just a string + stack.append(",") else: - # no callable active - just a string - stack.append(",") + # the rest + stack.append(gdict["rest"]) + + if ncallable > 0: + # this means not all inlinefuncs were complete + return string + + if _STACK_MAXSIZE > 0 and _STACK_MAXSIZE < len(stack): + # if stack is larger than limit, throw away parsing + return string + gdict["stackfull"](*args, **kwargs) else: - # the rest - stack.append(gdict["rest"]) - - if ncallable > 0: - # this means not all inlinefuncs were complete - return string - - if _STACK_MAXSIZE > 0 and _STACK_MAXSIZE < len(stack): - # if stack is larger than limit, throw away parsing - return string + gdict["stackfull"](*args, **kwargs) - else: - # cache the stack - _PARSING_CACHE[string] = stack + # cache the stack + _PARSING_CACHE[string] = stack # run the stack recursively def _run_stack(item, depth=0):