mirror of
https://github.com/evennia/evennia.git
synced 2025-10-29 19:35:56 +00:00
206 lines
8.1 KiB
Python
206 lines
8.1 KiB
Python
"""
|
|
The default command parser. Use your own by assigning
|
|
`settings.COMMAND_PARSER` to a Python path to a module containing the
|
|
replacing cmdparser function. The replacement parser must accept the
|
|
same inputs as the default one.
|
|
|
|
"""
|
|
|
|
|
|
import re
|
|
|
|
from django.conf import settings
|
|
|
|
from evennia.utils.logger import log_trace
|
|
|
|
_MULTIMATCH_REGEX = re.compile(settings.SEARCH_MULTIMATCH_REGEX, re.I + re.U)
|
|
_CMD_IGNORE_PREFIXES = settings.CMD_IGNORE_PREFIXES
|
|
|
|
|
|
def create_match(cmdname, string, cmdobj, raw_cmdname):
|
|
"""
|
|
Builds a command match by splitting the incoming string and
|
|
evaluating the quality of the match.
|
|
|
|
Args:
|
|
cmdname (str): Name of command to check for.
|
|
string (str): The string to match against.
|
|
cmdobj (str): The full Command instance.
|
|
raw_cmdname (str, optional): If CMD_IGNORE_PREFIX is set and the cmdname starts with
|
|
one of the prefixes to ignore, this contains the raw, unstripped cmdname,
|
|
otherwise it is None.
|
|
|
|
Returns:
|
|
match (tuple): This is on the form (cmdname, args, cmdobj, cmdlen, mratio, raw_cmdname),
|
|
where `cmdname` is the command's name and `args` is the rest of the incoming
|
|
string, without said command name. `cmdobj` is
|
|
the Command instance, the cmdlen is the same as len(cmdname) and mratio
|
|
is a measure of how big a part of the full input string the cmdname
|
|
takes up - an exact match would be 1.0. Finally, the `raw_cmdname` is
|
|
the cmdname unmodified by eventual prefix-stripping.
|
|
|
|
"""
|
|
cmdlen, strlen = len(str(cmdname)), len(str(string))
|
|
mratio = 1 - (strlen - cmdlen) / (1.0 * strlen)
|
|
args = string[cmdlen:]
|
|
return (cmdname, args, cmdobj, cmdlen, mratio, raw_cmdname)
|
|
|
|
|
|
def build_matches(raw_string, cmdset, include_prefixes=False):
|
|
"""
|
|
Build match tuples by matching raw_string against available commands.
|
|
|
|
Args:
|
|
raw_string (str): Input string that can look in any way; the only assumption is
|
|
that the sought command's name/alias must be *first* in the string.
|
|
cmdset (CmdSet): The current cmdset to pick Commands from.
|
|
include_prefixes (bool): If set, include prefixes like @, ! etc (specified in settings)
|
|
in the match, otherwise strip them before matching.
|
|
|
|
Returns:
|
|
matches (list) A list of match tuples created by `cmdparser.create_match`.
|
|
|
|
"""
|
|
matches = []
|
|
try:
|
|
orig_string = raw_string
|
|
if not include_prefixes and len(raw_string) > 1:
|
|
raw_string = raw_string.lstrip(_CMD_IGNORE_PREFIXES)
|
|
search_string = raw_string.lower()
|
|
for cmd in cmdset:
|
|
cmdname, raw_cmdname = cmd.match(search_string, include_prefixes=include_prefixes)
|
|
if cmdname:
|
|
matches.append(create_match(cmdname, raw_string, cmd, raw_cmdname))
|
|
except Exception:
|
|
log_trace("cmdhandler error. raw_input:%s" % raw_string)
|
|
return matches
|
|
|
|
|
|
def try_num_differentiators(raw_string):
|
|
"""
|
|
Test if user tried to separate multi-matches with a number separator
|
|
(default 1-name, 2-name etc). This is usually called last, if no other
|
|
match was found.
|
|
|
|
Args:
|
|
raw_string (str): The user input to parse.
|
|
|
|
Returns:
|
|
mindex, new_raw_string (tuple): If a multimatch-separator was detected,
|
|
this is stripped out as an integer to separate between the matches. The
|
|
new_raw_string is the result of stripping out that identifier. If no
|
|
such form was found, returns (None, None).
|
|
|
|
Example:
|
|
In the default configuration, entering 2-ball (e.g. in a room will more
|
|
than one 'ball' object), will lead to a multimatch and this function
|
|
will parse `"2-ball"` and return `(2, "ball")`.
|
|
|
|
"""
|
|
# no matches found
|
|
num_ref_match = _MULTIMATCH_REGEX.match(raw_string)
|
|
if num_ref_match:
|
|
# the user might be trying to identify the command
|
|
# with a #num-command style syntax. We expect the regex to
|
|
# contain the groups "number" and "name".
|
|
mindex, new_raw_string = (
|
|
num_ref_match.group("number"),
|
|
num_ref_match.group("name") + num_ref_match.group("args"),
|
|
)
|
|
return int(mindex), new_raw_string
|
|
else:
|
|
return None, None
|
|
|
|
|
|
def cmdparser(raw_string, cmdset, caller, match_index=None):
|
|
"""
|
|
This function is called by the cmdhandler once it has
|
|
gathered and merged all valid cmdsets valid for this particular parsing.
|
|
|
|
Args:
|
|
raw_string (str): The unparsed text entered by the caller.
|
|
cmdset (CmdSet): The merged, currently valid cmdset
|
|
caller (Session, Account or Object): The caller triggering this parsing.
|
|
match_index (int, optional): Index to pick a given match in a
|
|
list of same-named command matches. If this is given, it suggests
|
|
this is not the first time this function was called: normally
|
|
the first run resulted in a multimatch, and the index is given
|
|
to select between the results for the second run.
|
|
|
|
Returns:
|
|
matches (list): This is a list of match-tuples as returned by `create_match`.
|
|
If no matches were found, this is an empty list.
|
|
|
|
Notes:
|
|
The cmdparser understand the following command combinations (where
|
|
[] marks optional parts.
|
|
|
|
```
|
|
[cmdname[ cmdname2 cmdname3 ...] [the rest]
|
|
```
|
|
|
|
A command may consist of any number of space-separated words of any
|
|
length, and contain any character. It may also be empty.
|
|
|
|
The parser makes use of the cmdset to find command candidates. The
|
|
parser return a list of matches. Each match is a tuple with its
|
|
first three elements being the parsed cmdname (lower case),
|
|
the remaining arguments, and the matched cmdobject from the cmdset.
|
|
|
|
"""
|
|
if not raw_string:
|
|
return []
|
|
|
|
# find matches, first using the full name
|
|
matches = build_matches(raw_string, cmdset, include_prefixes=True)
|
|
|
|
if not matches or len(matches) > 1:
|
|
# no single match, try parsing for optional numerical tags like 1-cmd
|
|
# or cmd-2, cmd.2 etc
|
|
match_index, new_raw_string = try_num_differentiators(raw_string)
|
|
if match_index is not None:
|
|
matches.extend(build_matches(new_raw_string, cmdset, include_prefixes=True))
|
|
|
|
if not matches and _CMD_IGNORE_PREFIXES:
|
|
# still no match. Try to strip prefixes
|
|
raw_string = raw_string.lstrip(_CMD_IGNORE_PREFIXES) if len(raw_string) > 1 else raw_string
|
|
matches = build_matches(raw_string, cmdset, include_prefixes=False)
|
|
|
|
# only select command matches we are actually allowed to call.
|
|
matches = [match for match in matches if match[2].access(caller, "cmd")]
|
|
|
|
# try to bring the number of matches down to 1
|
|
if len(matches) > 1:
|
|
# See if it helps to analyze the match with preserved case but only if
|
|
# it leaves at least one match.
|
|
trimmed = [match for match in matches if raw_string.startswith(match[0])]
|
|
if trimmed:
|
|
matches = trimmed
|
|
|
|
if len(matches) > 1:
|
|
# we still have multiple matches. Sort them by count quality.
|
|
matches = sorted(matches, key=lambda m: m[3])
|
|
# only pick the matches with highest count quality
|
|
quality = [mat[3] for mat in matches]
|
|
matches = matches[-quality.count(quality[-1]) :]
|
|
|
|
if len(matches) > 1:
|
|
# still multiple matches. Fall back to ratio-based quality.
|
|
matches = sorted(matches, key=lambda m: m[4])
|
|
# only pick the highest rated ratio match
|
|
quality = [mat[4] for mat in matches]
|
|
matches = matches[-quality.count(quality[-1]) :]
|
|
|
|
if len(matches) > 1 and match_index is not None:
|
|
# We couldn't separate match by quality, but we have an
|
|
# index argument to tell us which match to use.
|
|
if 0 < match_index <= len(matches):
|
|
matches = [matches[match_index - 1]]
|
|
else:
|
|
# we tried to give an index outside of the range - this means
|
|
# a no-match
|
|
matches = []
|
|
|
|
# no matter what we have at this point, we have to return it.
|
|
return matches
|