diff --git a/docs/0.9.5/api/evennia.commands.default.building.html b/docs/0.9.5/api/evennia.commands.default.building.html
index 6c2da67b83..221e8941c4 100644
--- a/docs/0.9.5/api/evennia.commands.default.building.html
+++ b/docs/0.9.5/api/evennia.commands.default.building.html
@@ -493,7 +493,7 @@ You can specify the /force switch to bypass this confirmation.
diff --git a/docs/0.9.5/api/evennia.commands.default.system.html b/docs/0.9.5/api/evennia.commands.default.system.html
index 5be6caf945..f04668de96 100644
--- a/docs/0.9.5/api/evennia.commands.default.system.html
+++ b/docs/0.9.5/api/evennia.commands.default.system.html
@@ -293,7 +293,7 @@ required since whole classes of scripts often have the same name.
diff --git a/docs/0.9.5/api/evennia.contrib.chargen.html b/docs/0.9.5/api/evennia.contrib.chargen.html
index 8279191a21..0c51c9ce03 100644
--- a/docs/0.9.5/api/evennia.contrib.chargen.html
+++ b/docs/0.9.5/api/evennia.contrib.chargen.html
@@ -76,7 +76,7 @@ at them with this command.
diff --git a/docs/0.9.5/api/evennia.contrib.dice.html b/docs/0.9.5/api/evennia.contrib.dice.html
index 366e7900eb..091a1c7b56 100644
--- a/docs/0.9.5/api/evennia.contrib.dice.html
+++ b/docs/0.9.5/api/evennia.contrib.dice.html
@@ -148,7 +148,7 @@ everyone but the person rolling.
diff --git a/docs/0.9.5/api/evennia.contrib.email_login.html b/docs/0.9.5/api/evennia.contrib.email_login.html
index 8f5cd61876..1ff6e9ec22 100644
--- a/docs/0.9.5/api/evennia.contrib.email_login.html
+++ b/docs/0.9.5/api/evennia.contrib.email_login.html
@@ -73,7 +73,7 @@ the module given by settings.CONNECTION_SCREEN_MODULE.
diff --git a/docs/0.9.5/api/evennia.utils.evmore.html b/docs/0.9.5/api/evennia.utils.evmore.html
index 0d690847fc..d30bdf95d3 100644
--- a/docs/0.9.5/api/evennia.utils.evmore.html
+++ b/docs/0.9.5/api/evennia.utils.evmore.html
@@ -74,7 +74,7 @@ the caller.msg() construct every time the page is updated.
diff --git a/docs/0.9.5/api/evennia.utils.inlinefuncs.html b/docs/0.9.5/api/evennia.utils.inlinefuncs.html
index c6c4ca50a3..22e66b9abc 100644
--- a/docs/0.9.5/api/evennia.utils.inlinefuncs.html
+++ b/docs/0.9.5/api/evennia.utils.inlinefuncs.html
@@ -370,7 +370,7 @@ etc to match the regex.
diff --git a/docs/1.0-dev/.buildinfo b/docs/1.0-dev/.buildinfo
index 4b369ba532..6149dfcd48 100644
--- a/docs/1.0-dev/.buildinfo
+++ b/docs/1.0-dev/.buildinfo
@@ -1,4 +1,4 @@
# Sphinx build info version 1
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
-config: 533466af4453a76ae723816bc0754617
+config: 81975fe04912467597fe483b3539ba20
tags: 645f666f9bcd5a90fca523b33c5a78b7
diff --git a/docs/1.0-dev/Components/FuncParser.html b/docs/1.0-dev/Components/FuncParser.html
new file mode 100644
index 0000000000..2d236a569a
--- /dev/null
+++ b/docs/1.0-dev/Components/FuncParser.html
@@ -0,0 +1,548 @@
+
+
+
+
+
+
+
+ The Inline Function Parser — Evennia 1.0-dev documentation
+
+
+
+
+
+
+
+
+
+
+
+
The FuncParser extracts and executes
+‘inline functions’
+embedded in a string on the form $funcname(args,kwargs). Under the hood, this will
+lead to a call to a Python function you control. The inline function call will be replaced by
+the return from the function.
+
1
+2
+3
+4
+5
+6
+7
+8
fromevennia.utils.funcparserimportFuncParser
+
+def_power_callable(*args,**kwargs):
+ """This will be callable as $square(number, power=<num>) in string"""
+ pow=int(kwargs.get('power',2))
+ returnfloat(args[0])**pow
+
+parser=FuncParser({"pow":_power_callable})
+
+
+
Next, just pass a string into the parser, optionally containing $func(...) markers:
+
1
+2
parser.parse("We have that 4 x 4 x 4 is $pow(4, power=3).")
+"We have that 4 x 4 x 4 is 64."
+
+
+
Normally the return is always converted to a string but you can also get the actual data type from the call:
+
1
+2
parser.parse_to_any("$pow(4)")
+16
+
+
+
To show a $func() verbatim in your code without parsing it, escape it as either $$func() or \$func():
+
1
+2
parser.parse("This is an escaped $$pow(4) and so is this \$pow(3)")
+"This is an escaped $pow(4) and so is this $pow(3)"
+
The FuncParser can be applied to any string. Out of the box it’s applied in a few situations:
+
+
Outgoing messages. All messages sent from the server is processed through FuncParser and every
+callable is provided the Session of the object receiving the message. This potentially
+allows a message to be modified on the fly to look different for different recipients.
+
Prototype values. A Prototype dict’s values are run through the parser such that every
+callable gets a reference to the rest of the prototype. In the Prototype ORM, this would allow builders
+to safely call functions to set non-string values to prototype values, get random values, reference
+other fields of the prototype, and more.
+
Actor-stance in messages to others. In the
+Object.msg_contents method,
+the outgoing string is parsed for special $You() and $conj() callables to decide if a given recipient
+should see “You” or the character’s name.
+
+
+
Important
+
The inline-function parser is not intended as a ‘softcode’ programming language. It does not
+have things like loops and conditionals, for example. While you could in principle extend it to
+do very advanced things and allow builders a lot of power, all-out coding is something
+Evennia expects you to do in a proper text editor, outside of the game, not from inside it.
You can apply inline function parsing to any string. The
+FuncParser is found in evennia.utils.funcparser.py.
+
1
+2
+3
+4
+5
+6
+7
+8
+9
fromevennia.utilsimportfuncparser
+
+parser=FuncParser(callables,**default_kwargs)
+parsed_string=parser.parser(input_string,raise_errors=False,
+ escape=False,strip=False,
+ return_str=True,**reserved_kwargs)
+
+# callables can also be passed as paths to modules
+parser=FuncParser(["game.myfuncparser_callables","game.more_funcparser_callables"])
+
+
+
Here, callables points to a collection of normal Python functions (see next section) for you to make
+available to the parser as you parse strings with it. It can either be
+
+
A dict of {"functionname":callable,...}. This allows you do pick and choose exactly which callables
+to include and how they should be named. Do you want a callable to be available under more than one name?
+Just add it multiple times to the dict, with a different key.
+
A module or (more commonly) a python-path to a module. This module can define a dict
+FUNCPARSER_CALLABLES={"funcname":callable,...} - this will be imported and used like ther dict above.
+If no such variable is defined, every top-level function in the module (whose name doesn’t start with
+an underscore _) will be considered a suitable callable. The name of the function will be the $funcname
+by which it can be called.
+
A list of modules/paths. This allows you to pull in modules from many sources for your parsing.
+
+
The other arguments to the parser:
+
+
raise_errors - By default, any errors from a callable will be quietly ignored and the result
+will be that the failing function call will show verbatim. If raise_errors is set,
+then parsing will stop and whatever exception happened will be raised. It’d be up to you to handle
+this properly.
+
escape - Returns a string where every $func(...) has been escaped as \$func().
+
strip - Remove all $func(...) calls from string (as if each returned '').
+
return_str - When True (default), parser always returns a string. If False, it may return
+the return value of a single function call in the string. This is the same as using the .parse_to_any
+method.
+
The **default/reserved_keywords are optional and allow you to pass custom data into every function
+call. This is great for including things like the current session or config options. Defaults can be
+replaced if the user gives the same-named kwarg in the string’s function call. Reserved kwargs are always passed,
+ignoring defaults or what the user passed. In addition, the funcparser and raise_errors
+reserved kwargs are always passed - the first is a back-reference to the FuncParser instance and the second
+is the raise_errors boolean passed into FuncParser.parse.
+
+
Here’s an example of using the default/reserved keywords:
The mydefault=2 kwarg could be overwritten if we made the call as $test(mydefault=...)
+but myreserved=[1,2,3] will always be sent as-is and will override a call $test(myreserved=...).
+The funcparser/raise_errors kwargs are also always included as reserved kwargs.
The *args and **kwargs must always be included. If you are unsure how *args and **kwargs work in Python,
+read about them here.
+
+
The input from the innermost $funcname(...) call in your callable will always be a str. Here’s
+an example of an $toint function; it converts numbers to integers.
+
"There's a $toint(22.0)% chance of survival."
+
+
+
What will enter the $toint callable (as args[0]) is the string"22.0". The function is responsible
+for converting this to a number so that we can convert it to an integer. We must also properly handle invalid
+inputs (like non-numbers).
+
If you want to mark an error, raise evennia.utils.funcparser.ParsingError. This stops the entire parsing
+of the string and may or may not raise the exception depending on what you set raise_errors to when you
+created the parser.
+
However, if you nest functions, the return of the innermost function may be something other than
+a string. Let’s introduce the $eval function, which evaluates simple expressions using
+Python’s literal_eval and/or simple_eval.
+
"There's a $toint($eval(10 * 2.2))% chance of survival."
+
+
+
Since the $eval is the innermost call, it will get a string as input - the string "10*2.2".
+It evaluates this and returns the float22.0. This time the outermost $toint will be called with
+this float instead of with a string.
+
+
It’s important to safely validate your inputs since users may end up nesting your callables in any order.
+See the next section for useful tools to help with this.
+
+
In these examples, the result will be embedded in the larger string, so the result of the entire parsing
+will be a string:
+
1
+2
parser.parse(above_string)
+ "There's a 22% chance of survival."
+
+
+
However, if you use the parse_to_any (or parse(...,return_str=True)) and don’t add any extra string around the outermost function call,
+you’ll get the return type of the outermost callable back:
Since you don’t know in which order users may use your callables, they should always check the types
+of its inputs and convert to the type the callable needs. Note also that when converting from strings,
+there are limits what inputs you can support. This is because FunctionParser strings are often used by
+non-developer players/builders and some things (such as complex classes/callables etc) are just not
+safe/possible to convert from string representation.
+
In evennia.utils.utils is a helper called
+safe_convert_to_types. This function
+automates the conversion of simple data types in a safe way:
fromevennia.utils.utilsimportsafe_convert_to_types
+
+def_process_callable(*args,**kwargs):
+ """
+ A callable with a lot of custom options
+
+ $process(expression, local, extra=34, extra2=foo)
+
+ """
+ args,kwargs=safe_convert_to_type(
+ (('py','py'),{'extra1':int,'extra2':str}),
+ *args,**kwargs)
+
+ # args/kwargs should be correct types now
+
Each converter should be a callable taking one argument - this will be the arg/kwarg-value to convert. The
+special converter "py" will try to convert a string argument to a Python structure with the help of the
+following tools (which you may also find useful to experiment with on your own):
+
+
ast.literal_eval is an in-built Python
+function. It
+only supports strings, bytes, numbers, tuples, lists, dicts, sets, booleans and None. That’s
+it - no arithmetic or modifications of data is allowed. This is good for converting individual values and
+lists/dicts from the input line to real Python objects.
+
simpleeval is a third-party tool included with Evennia. This
+allows for evaluation of simple (and thus safe) expressions. One can operate on numbers and strings
+with +-/* as well as do simple comparisons like 4>3 and more. It does not accept more complex
+containers like lists/dicts etc, so this and literal_eval are complementary to each other.
+
+
+
Warning
+
It may be tempting to run use Python’s in-built eval() or exec() functions as converters since
+these are able to convert any valid Python source code to Python. NEVER DO THIS unless you really, really
+know that ONLY developers will ever modify the string going into the callable. The parser is intended
+for untrusted users (if you were trusted you’d have access to Python already). Letting untrusted users
+pass strings to eval/exec is a MAJOR security risk. It allows the caller to run arbitrary
+Python code on your server. This is the path to maliciously deleted hard drives. Just don’t do it and
+sleep better at night.
These are some example callables you can import and add your parser. They are divided into
+global-level dicts in evennia.utils.funcparser. Just import the dict(s) and merge/add one or
+more to them when you create your FuncParser instance to have those callables be available.
$eval(expression) (code) -
+this uses literal_eval and simple_eval (see previous section) attemt to convert a string expression
+to a python object. This handles e.g. lists of literals [1,2,3] and simple expressions like "1+2".
+
$toint(number) (code) -
+always converts an output to an integer, if possible.
+
$add/sub/mult/div(obj1,obj2) (code) -
+this adds/subtracts/multiplies and divides to elements together. While simple addition could be done with
+$eval, this could for example be used also to add two lists together, which is not possible with eval;
+for example $add($eval([1,2,3]),$eval([4,5,6]))->[1,2,3,4,5,6].
+
$round(float,significant) (code) -
+rounds an input float into the number of provided significant digits. For example $round(3.54343,3)->3.543.
+
$random([start,[end]]) (code) -
+this works like the Python random() function, but will randomize to an integer value if both start/end are
+integers. Without argument, will return a float between 0 and 1.
+
$randint([start,[end]]) (code) -
+works like the randint() python function and always returns an integer.
+
$choice(list) (code) -
+the input will automatically be parsed the same way as $eval and is expected to be an iterable. A random
+element of this list will be returned.
+
$pad(text[,width,align,fillchar]) (code) -
+this will pad content. $pad("Hello",30,c,-) will lead to a text centered in a 30-wide block surrounded by -
+characters.
+
$crop(text,width=78,suffix='[...]') (code) -
+this will crop a text longer than the width, by default ending it with a [...]-suffix that also fits within
+the width. If no width is given, the client width or settings.DEFAULT_CLIENT_WIDTH will be used.
+
$space(num) (code) -
+this will insert num spaces.
+
$just(string,width=40,align=c,indent=2) (code) -
+justifies the text to a given width, aligning it left/right/center or ‘f’ for full (spread text across width).
+
$ljust - shortcut to justify-left. Takes all other kwarg of $just.
+
$rjust - shortcut to right justify.
+
$cjust - shortcut to center justify.
+
$clr(startcolor,text[,endcolor]) (code) -
+color text. The color is given with one or two characters without the preceeding |. If no endcolor is
+given, the string will go back to neutral, so $clr(r,Hello) is equivalent to |rHello|n.
These are callables that requires access-checks in order to search for objects. So they require some
+extra reserved kwargs to be passed when running the parser:
The caller is required, it’s the the object to do the access-check for. The access kwarg is the
+lock type to check, default being "control".
+
+
$search(query,type=account|script,return_list=False) (code) -
+this will look up and try to match an object by key or alias. Use the type kwarg to
+search for account or script instead. By default this will return nothing if there are more than one
+match; if return_list is True a list of 0, 1 or more matches will be returned instead.
+
$obj(query), $dbref(query) - legacy aliases for $search.
+
$objlist(query) - legacy alias for $search, always returning a list.
Here the caller is the one sending the message and receiver the one to see it. The mapping contains
+references to other objects accessible via these callables.
+
+
$you([key]) (code) -
+if no key is given, this represents the caller, otherwise an object from mapping
+will be used. As this message is sent to different recipients, the receiver will change and this will
+be replaced either with the string you (if you and the receiver is the same entity) or with the
+result of you_obj.get_display_name(looker=receiver). This allows for a single string to echo differently
+depending on who sees it, and also to reference other people in the same way.
+
$You([key]) - same as $you but always capitalized.
+
$conj(verb) (code) – conjugates a verb between 4nd person presens to 3rd person presence depending on who
+sees the string. For example "$You()$conj(smiles)". will show as “You smile.” and “Tom smiles.” depending
+on who sees it. This makes use of the tools in evennia.utils.verb_conjugation
+to do this, and only works for English verbs.
Above we define two callables _dashline and _uptime and map them to names "dashline" and "uptime",
+which is what we then can call as $header and $uptime in the string. We also have access to
+all the defaults (like $toint()).
+
The parsed result of the above would be something like this:
+
+
+
\ No newline at end of file
diff --git a/docs/1.0-dev/Concepts/Banning.html b/docs/1.0-dev/Concepts/Banning.html
index 98530ee458..3547cd5320 100644
--- a/docs/1.0-dev/Concepts/Banning.html
+++ b/docs/1.0-dev/Concepts/Banning.html
@@ -41,7 +41,7 @@
Whether due to abuse, blatant breaking of your rules, or some other reason, you will eventually find
no other recourse but to kick out a particularly troublesome player. The default command set has
-admin tools to handle this, primarily @ban, @unban, and @boot.
+admin tools to handle this, primarily ban, unban, and boot.
Say we have a troublesome player “YouSuck” - this is a person that refuses common courtesy - an
@@ -51,19 +51,19 @@ have tried to be nice. Now you just want this troll gone.
The easiest recourse is to block the account YouSuck from ever connecting again.
-
@banYouSuck
+
banYouSuck
This will lock the name YouSuck (as well as ‘yousuck’ and any other capitalization combination), and
next time they try to log in with this name the server will not let them!
You can also give a reason so you remember later why this was a good thing (the banned account will
never see this)
-
@banYouSuck:Thisisjustatroll.
+
banYouSuck:Thisisjustatroll.
If you are sure this is just a spam account, you might even consider deleting the player account
outright:
-
@delaccountYouSuck
+
accounts/deleteYouSuck
Generally, banning the name is the easier and safer way to stop the use of an account – if you
@@ -82,7 +82,7 @@ the who
The “Host” bit is the IP address from which the account is connecting. Use this to define the ban
instead of the name:
-
@ban237.333.0.223
+
ban237.333.0.223
This will stop YouSuckMore connecting from their computer. Note however that IP address might change
@@ -91,7 +91,7 @@ changing computers. You can make a more general ban by putting asterisks
-
@ban237.333.0.*
+
ban237.333.0.*
You should combine the IP ban with a name-ban too of course, so the account YouSuckMore is truly
@@ -104,17 +104,17 @@ blocking out innocent players who just happen to connect from the same subnet as
YouSuck is not really noticing all this banning yet though - and won’t until having logged out and
trying to log back in again. Let’s help the troll along.
-
@bootYouSuck
+
bootYouSuck
Good riddance. You can give a reason for booting too (to be echoed to the player before getting
kicked out).
Evennia supports clickable links for clients that supports it. This marks certain text so it can be
+clicked by a mouse and trigger a given Evennia command. To support clickable links, Evennia requires
+the webclient or an third-party telnet client with MXP
+support (Note: Evennia only supports clickable links, no other MXP features).
+
+
|lc to start the link, by defining the command to execute.
+
|lt to continue with the text to show to the user (the link text).
+
|le to end the link text and the link definition.
+
+
All elements must appear in exactly this order to make a valid link. For example,
+
"If you go |lcnorth|ltto the north|le you will find a cottage."
+
+
+
This will display as “If you go to the north you will find a cottage.” where clicking the link
+will execute the command north. If the client does not support clickable links, only the link text
+will be shown.
Note that the Documentation does not display colour the way it would look on the screen.
+
Color can be a very useful tool for your game. It can be used to increase readability and make your
+game more appealing visually.
+
Remember however that, with the exception of the webclient, you generally don’t control the client
+used to connect to the game. There is, for example, one special tag meaning “yellow”. But exactly
+which hue of yellow is actually displayed on the user’s screen depends on the settings of their
+particular mud client. They could even swap the colours around or turn them off altogether if so
+desired. Some clients don’t even support color - text games are also played with special reading
+equipment by people who are blind or have otherwise diminished eyesight.
+
So a good rule of thumb is to use colour to enhance your game but don’t rely on it to display
+critical information. If you are coding the game, you can add functionality to let users disable
+colours as they please, as described here.
+
To see which colours your client support, use the default @color command. This will list all
+available colours for ANSI and Xterm256 along with the codes you use for them. You can find a list
+of all the parsed ANSI-colour codes in evennia/utils/ansi.py.
Evennia supports the ANSI standard for text. This is by far the most supported MUD-color standard,
+available in all but the most ancient mud clients. The ANSI colours are red, green,
+yellow, blue, magenta, cyan, white and black. They are abbreviated by their
+first letter except for black which is abbreviated with the letter x. In ANSI there are “bright”
+and “normal” (darker) versions of each color, adding up to a total of 16 colours to use for
+foreground text. There are also 8 “background” colours. These have no bright alternative in ANSI
+(but Evennia uses the Xterm256 extension behind the scenes to offer
+them anyway).
+
To colour your text you put special tags in it. Evennia will parse these and convert them to the
+correct markup for the client used. If the user’s client/console/display supports ANSI colour, they
+will see the text in the specified colour, otherwise the tags will be stripped (uncolored text).
+This works also for non-terminal clients, such as the webclient. For the webclient, Evennia will
+translate the codes to HTML RGB colors.
|n - this tag will turn off all color formatting, including background colors.
+
|#- markup marks the start of foreground color. The case defines if the text is “bright” or
+“normal”. So |g is a bright green and |G is “normal” (darker) green.
+
|[# is used to add a background colour to the text. The case again specifies if it is “bright”
+or “normal”, so |[c starts a bright cyan background and |[C a darker cyan background.
+
|!# is used to add foreground color without any enforced brightness/normal information.
+These are normal-intensity and are thus always given as uppercase, such as
+|!R for red. The difference between e.g. |!R and |R is that
+|!R will “inherit” the brightness setting from previously set color tags, whereas |R will
+always reset to the normal-intensity red. The |# format contains an implicit |h/|H tag in it:
+disabling highlighting when switching to a normal color, and enabling it for bright ones. So |btest|!Rtest2 will result in a bright red test2 since the brightness setting from |b “bleeds over”.
+You could use this to for example quickly switch the intensity of a multitude of color tags. There
+is no background-color equivalent to |! style tags.
+
|h is used to make any following foreground ANSI colors bright (it has no effect on Xterm
+colors). This is only relevant to use with |! type tags and will be valid until the next |n,
+|H or normal (upper-case) |# tag. This tag will never affect background colors, those have to be
+set bright/normal explicitly. Technically, |h|!G is identical to |g.
+
|H negates the effects |h and returns all ANSI foreground colors (|! and | types) to
+‘normal’ intensity. It has no effect on background and Xterm colors.
+
+
+
Note: The ANSI standard does not actually support bright backgrounds like |[r - the standard
+only supports “normal” intensity backgrounds. To get around this Evennia instead implements these
+as Xterm256 colours behind the scenes. If the client does not support
+Xterm256 the ANSI colors will be used instead and there will be no visible difference between using
+upper- and lower-case background tags.
+
+
If you want to display an ANSI marker as output text (without having any effect), you need to escape
+it by preceding its | with another |:
+
sayThe||rANSImarkerchangestextcolortobrightred.
+
+
+
This will output the raw |r without any color change. This can also be necessary if you are doing
+ansi art that uses | with a letter directly following it.
+
Use the command
+
@coloransi
+
+
+
to get a list of all supported ANSI colours and the tags used to produce them.
+
A few additional ANSI codes are supported:
+
+
|/ A line break. You cannot put the normal Python \n line breaks in text entered inside the
+game (Evennia will filter this for security reasons). This is what you use instead: use the |/
+marker to format text with line breaks from the game command line.
+
`` This will translate into a TAB character. This will not always show (or show differently) to
+the client since it depends on their local settings. It’s often better to use multiple spaces.
+
|_ This is a space. You can usually use the normal space character, but if the space is at the
+end of the line, Evennia will likely crop it. This tag will not be cropped but always result in a
+space.
+
|* This will invert the current text/background colours. Can be useful to mark things (but see
+below).
The |* tag (inverse video) is an old ANSI standard and should usually not be used for more than to
+mark short snippets of text. If combined with other tags it comes with a series of potentially
+confusing behaviors:
+
+
The |* tag will only work once in a row:, ie: after using it once it won’t have an effect again
+until you declare another tag. This is an example:
+
Normaltext,|*reversedtext|*,stillreversedtext.
+
+
+
that is, it will not reverse to normal at the second |*. You need to reset it manually:
+
Normaltext,|*reversedtext|n,normalagain.
+
+
+
+
The |* tag does not take “bright” colors into account:
+
|RNormalred,|hnowbrightened.|*BGisnormalred.
+
+
+
+
+
So |* only considers the ‘true’ foreground color, ignoring any highlighting. Think of the bright
+state (|h) as something like like <strong> in HTML: it modifies the appearance of a normal
+foreground color to match its bright counterpart, without changing its normal color.
+
+
Finally, after a |*, if the previous background was set to a dark color (via |[), |!#) will
+actually change the background color instead of the foreground:
+
|*reversed text |!R now BG is red.
+
+
+
+
+
For a detailed explanation of these caveats, see the [Understanding Color Tags](Understanding-Color-
+Tags) tutorial. But most of the time you might be better off to simply avoid |* and mark your text
+manually instead.
The Xterm256 standard is a colour scheme that supports 256 colours for text and/or background.
+While this offers many more possibilities than traditional ANSI colours, be wary that too many text
+colors will be confusing to the eye. Also, not all clients support Xterm256 - these will instead see
+the closest equivalent ANSI color. You can mix Xterm256 tags with ANSI tags as you please.
|### - markup consists of three digits, each an integer from 0 to 5. The three digits describe
+the amount of red, green and blue (RGB) components used in the colour. So |500 means
+maximum red and none of the other colours - the result is a bright red. |520 is red with a touch
+of green - the result is orange. As opposed to ANSI colors, Xterm256 syntax does not worry about
+bright/normal intensity, a brighter (lighter) color is just achieved by upping all RGB values with
+the same amount.
+
|[### - this works the same way but produces a coloured background.
+
|=# - markup produces the xterm256 gray scale tones, where # is a letter from a (black) to
+z (white). This offers many more nuances of gray than the normal |### markup (which only has
+four gray tones between solid black and white (|000, |111, |222, |333 and |444)).
+
|[=# - this works in the same way but produces background gray scale tones.
+
+
If you have a client that supports Xterm256, you can use
+
@colorxterm256
+
+
+
to get a table of all the 256 colours and the codes that produce them. If the table looks broken up
+into a few blocks of colors, it means Xterm256 is not supported and ANSI are used as a replacement.
+You can use the @options command to see if xterm256 is active for you. This depends on if your
+client told Evennia what it supports - if not, and you know what your client supports, you may have
+to activate some features manually.
There is an Understanding Color Tags tutorial which expands on the
+use of ANSI color tags and the pitfalls of mixing ANSI and Xterms256 color tags in the same context.
This documentation details the various text tags supported by Evennia, namely colours, command
-links and inline functions.
-
There is also an Understanding Color Tags tutorial which expands on the
-use of ANSI color tags and the pitfalls of mixing ANSI and Xterms256 color tags in the same context.
Note that the Documentation does not display colour the way it would look on the screen.
-
Color can be a very useful tool for your game. It can be used to increase readability and make your
-game more appealing visually.
-
Remember however that, with the exception of the webclient, you generally don’t control the client
-used to connect to the game. There is, for example, one special tag meaning “yellow”. But exactly
-which hue of yellow is actually displayed on the user’s screen depends on the settings of their
-particular mud client. They could even swap the colours around or turn them off altogether if so
-desired. Some clients don’t even support color - text games are also played with special reading
-equipment by people who are blind or have otherwise diminished eyesight.
-
So a good rule of thumb is to use colour to enhance your game but don’t rely on it to display
-critical information. If you are coding the game, you can add functionality to let users disable
-colours as they please, as described here.
-
To see which colours your client support, use the default @color command. This will list all
-available colours for ANSI and Xterm256 along with the codes you use for them. You can find a list
-of all the parsed ANSI-colour codes in evennia/utils/ansi.py.
Evennia supports the ANSI standard for text. This is by far the most supported MUD-color standard,
-available in all but the most ancient mud clients. The ANSI colours are red, green,
-yellow, blue, magenta, cyan, white and black. They are abbreviated by their
-first letter except for black which is abbreviated with the letter x. In ANSI there are “bright”
-and “normal” (darker) versions of each color, adding up to a total of 16 colours to use for
-foreground text. There are also 8 “background” colours. These have no bright alternative in ANSI
-(but Evennia uses the Xterm256 extension behind the scenes to offer
-them anyway).
-
To colour your text you put special tags in it. Evennia will parse these and convert them to the
-correct markup for the client used. If the user’s client/console/display supports ANSI colour, they
-will see the text in the specified colour, otherwise the tags will be stripped (uncolored text).
-This works also for non-terminal clients, such as the webclient. For the webclient, Evennia will
-translate the codes to HTML RGB colors.
Evennia understands various extra information embedded in text:
-
|n - this tag will turn off all color formatting, including background colors.
-
|#- markup marks the start of foreground color. The case defines if the text is “bright” or
-“normal”. So |g is a bright green and |G is “normal” (darker) green.
-
|[# is used to add a background colour to the text. The case again specifies if it is “bright”
-or “normal”, so |[c starts a bright cyan background and |[C a darker cyan background.
-
|!# is used to add foreground color without any enforced brightness/normal information.
-These are normal-intensity and are thus always given as uppercase, such as
-|!R for red. The difference between e.g. |!R and |R is that
-|!R will “inherit” the brightness setting from previously set color tags, whereas |R will
-always reset to the normal-intensity red. The |# format contains an implicit |h/|H tag in it:
-disabling highlighting when switching to a normal color, and enabling it for bright ones. So |btest|!Rtest2 will result in a bright red test2 since the brightness setting from |b “bleeds over”.
-You could use this to for example quickly switch the intensity of a multitude of color tags. There
-is no background-color equivalent to |! style tags.
-
|h is used to make any following foreground ANSI colors bright (it has no effect on Xterm
-colors). This is only relevant to use with |! type tags and will be valid until the next |n,
-|H or normal (upper-case) |# tag. This tag will never affect background colors, those have to be
-set bright/normal explicitly. Technically, |h|!G is identical to |g.
-
|H negates the effects |h and returns all ANSI foreground colors (|! and | types) to
-‘normal’ intensity. It has no effect on background and Xterm colors.
+
Colors - Using |r, |n etc can be used to mark parts of text with a color. The color will
+become ANSI/XTerm256 color tags for Telnet connections and CSS information for the webclient.
+
Clickable links - This allows you to provide a text the user can click to execute an
+in-game command. This is on the form |lccommand|lttext|le.
+
FuncParser callables - These are full-fledged function calls on the form $funcname(args,kwargs)
+that lead to calls to Python functions. The parser can be run with different available callables in different
+circumstances. The parser is run on all outgoing messages if settings.FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED=True
+(disabled by default).
-
-
Note: The ANSI standard does not actually support bright backgrounds like |[r - the standard
-only supports “normal” intensity backgrounds. To get around this Evennia instead implements these
-as Xterm256 colours behind the scenes. If the client does not support
-Xterm256 the ANSI colors will be used instead and there will be no visible difference between using
-upper- and lower-case background tags.
-
-
If you want to display an ANSI marker as output text (without having any effect), you need to escape
-it by preceding its | with another |:
-
sayThe||rANSImarkerchangestextcolortobrightred.
-
-
-
This will output the raw |r without any color change. This can also be necessary if you are doing
-ansi art that uses | with a letter directly following it.
-
Use the command
-
@coloransi
-
-
-
to get a list of all supported ANSI colours and the tags used to produce them.
-
A few additional ANSI codes are supported:
-
-
|/ A line break. You cannot put the normal Python \n line breaks in text entered inside the
-game (Evennia will filter this for security reasons). This is what you use instead: use the |/
-marker to format text with line breaks from the game command line.
-
`` This will translate into a TAB character. This will not always show (or show differently) to
-the client since it depends on their local settings. It’s often better to use multiple spaces.
-
|_ This is a space. You can usually use the normal space character, but if the space is at the
-end of the line, Evennia will likely crop it. This tag will not be cropped but always result in a
-space.
-
|* This will invert the current text/background colours. Can be useful to mark things (but see
-below).
The |* tag (inverse video) is an old ANSI standard and should usually not be used for more than to
-mark short snippets of text. If combined with other tags it comes with a series of potentially
-confusing behaviors:
+
-
The |* tag will only work once in a row:, ie: after using it once it won’t have an effect again
-until you declare another tag. This is an example:
-
Normaltext,|*reversedtext|*,stillreversedtext.
-
-
-
that is, it will not reverse to normal at the second |*. You need to reset it manually:
So |* only considers the ‘true’ foreground color, ignoring any highlighting. Think of the bright
-state (|h) as something like like <strong> in HTML: it modifies the appearance of a normal
-foreground color to match its bright counterpart, without changing its normal color.
-
-
Finally, after a |*, if the previous background was set to a dark color (via |[), |!#) will
-actually change the background color instead of the foreground:
-
|*reversed text |!R now BG is red.
-
-
-
For a detailed explanation of these caveats, see the [Understanding Color Tags](Understanding-Color-
-Tags) tutorial. But most of the time you might be better off to simply avoid |* and mark your text
-manually instead.
The Xterm256 standard is a colour scheme that supports 256 colours for text and/or background.
-While this offers many more possibilities than traditional ANSI colours, be wary that too many text
-colors will be confusing to the eye. Also, not all clients support Xterm256 - these will instead see
-the closest equivalent ANSI color. You can mix Xterm256 tags with ANSI tags as you please.
|### - markup consists of three digits, each an integer from 0 to 5. The three digits describe
-the amount of red, green and blue (RGB) components used in the colour. So |500 means
-maximum red and none of the other colours - the result is a bright red. |520 is red with a touch
-of green - the result is orange. As opposed to ANSI colors, Xterm256 syntax does not worry about
-bright/normal intensity, a brighter (lighter) color is just achieved by upping all RGB values with
-the same amount.
-
|[### - this works the same way but produces a coloured background.
-
|=# - markup produces the xterm256 gray scale tones, where # is a letter from a (black) to
-z (white). This offers many more nuances of gray than the normal |### markup (which only has
-four gray tones between solid black and white (|000, |111, |222, |333 and |444)).
-
|[=# - this works in the same way but produces background gray scale tones.
-
-
If you have a client that supports Xterm256, you can use
-
@colorxterm256
-
-
-
to get a table of all the 256 colours and the codes that produce them. If the table looks broken up
-into a few blocks of colors, it means Xterm256 is not supported and ANSI are used as a replacement.
-You can use the @options command to see if xterm256 is active for you. This depends on if your
-client told Evennia what it supports - if not, and you know what your client supports, you may have
-to activate some features manually.
Evennia supports clickable links for clients that supports it. This marks certain text so it can be
-clicked by a mouse and trigger a given Evennia command. To support clickable links, Evennia requires
-the webclient or an third-party telnet client with MXP
-support (Note: Evennia only supports clickable links, no other MXP features).
-
-
|lc to start the link, by defining the command to execute.
-
|lt to continue with the text to show to the user (the link text).
-
|le to end the link text and the link definition.
-
-
All elements must appear in exactly this order to make a valid link. For example,
-
"If you go |lcnorth|ltto the north|le you will find a cottage."
-
-
-
This will display as “If you go to the north you will find a cottage.” where clicking the link
-will execute the command north. If the client does not support clickable links, only the link text
-will be shown.
Note: Inlinefuncs are not activated by default. To use them you need to add
-INLINEFUNC_ENABLED=True to your settings file.
-
-
Evennia has its own inline text formatting language, known as inlinefuncs. It allows the builder
-to include special function calls in code. They are executed dynamically by each session that
-receives them.
-
To add an inlinefunc, you embed it in a text string like this:
-
"A normal string with $funcname(arg, arg, ...) embedded inside it."
-
-
-
When this string is sent to a session (with the msg() method), these embedded inlinefuncs will be
-parsed. Their return value (which always is a string) replace their call location in the finalized
-string. The interesting thing with this is that the function called will have access to which
-session is seeing the string, meaning the string can end up looking different depending on who is
-looking. It could of course also vary depending on other factors like game time.
-
Any number of comma-separated arguments can be given (or none). No keywords are supported. You can
-also nest inlinefuncs by letting an argument itself also be another $funcname(arg,arg,...) call
-(down to any depth of nesting). Function call resolution happens as in all programming languages
-inside-out, with the nested calls replacing the argument with their return strings before calling he
-parent.
-
>say"This is $pad(a center-padded text, 30,c,-) of width 30."
- Yousay,"This is ---- a center-padded text----- of width 30."
-
-
-
A special case happens if wanting to use an inlinefunc argument that itself includes a comma - this
-would be parsed as an argument separator. To escape commas you can either escape each comma manually
-with a backslash \,, or you can embed the entire string in python triple-quotes """ or ''' -
-this will escape the entire argument, including commas and any nested inlinefunc calls within.
-
Only certain functions are available to use as inlinefuncs and the game developer may add their own
-functions as needed.
To add new inlinefuncs, edit the file mygame/server/conf/inlinefuncs.py.
-
All globally defined functions in this module are considered inline functions by the system. The
-only exception is functions whose name starts with an underscore _. An inlinefunc must be of the
-following form:
where *args denotes all the arguments this function will accept as an $inlinefunc. The inline
-function is expected to clean arguments and check that they are valid. If needed arguments are not
-given, default values should be used. The function should always return a string (even if it’s
-empty). An inlinefunc should never cause a traceback regardless of the input (but it could log
-errors if desired).
-
Note that whereas the function should accept **kwargs, keyword inputs are not usable in the call
-to the inlinefunction. The kwargs part is instead intended for Evennia to be able to supply extra
-information. Currently Evennia sends a single keyword to every inline function and that is
-session, which holds the serversession this text is targeted at. Through the session
-object, a lot of dynamic possibilities are opened up for your inline functions.
-
The settings.INLINEFUNC_MODULES configuration option is a list that decides which modules should
-be parsed for inline function definitions. This will include mygame/server/conf/inlinefuncs.py but
-more could be added. The list is read from left to right so if you want to overload default
-functions you just have to put your custom module-paths later in the list and name your functions
-the same as default ones.
-
Here is an example, the crop default inlinefunction:
fromevennia.utilsimportutils
-
-defcrop(*args,**kwargs):
- """
- Inlinefunc. Crops ingoing text to given widths.
- Args:
- text (str, optional): Text to crop.
- width (str, optional): Will be converted to an integer. Width of
- crop in characters.
- suffix (str, optional): End string to mark the fact that a part
- of the string was cropped. Defaults to `[...]`.
- Kwargs:
- session (Session): Session performing the crop.
- Example:
- `$crop(text, 50, [...])`
-
- """
- text,width,suffix="",78,"[...]"
- nargs=len(args)
- ifnargs>0:
- text=args[0]
- ifnargs>1:
- width=int(args[1])ifargs[1].strip().isdigit()else78
- ifnargs>2:
- suffix=args[2]
- returnutils.crop(text,width=width,suffix=suffix)
-
defcharactername(*args,**kwargs):
- """
- Inserts the character name of whomever sees the string
- (so everyone will see their own name). Uses the account
- name for OOC communications.
-
- Example:
- say "This means YOU, $charactername()!"
-
- """
- session=kwargs["session"]
- ifsession.puppet:
- returnkwargs["session"].puppet.key
- else:
- returnsession.account.key
-
-
-
Evennia itself offers the following default inline functions (mostly as examples):
-
-
crop(text,width,suffix) - See above.
-
pad(text,width,align,fillchar) - this pads the text to width (default 78), alignment (“c”,
-“l” or “r”, defaulting to “c”) and fill-in character (defaults to space). Example: $pad(40,l,-)
-
clr(startclr,text,endclr) - A programmatic way to enter colored text for those who don’t want
-to use the normal |c type color markers for some reason. The color argument is the same as the
-color markers except without the actual pre-marker, so |r would be just r. If endclr is not
-given, it defaults to resetting the color (n). Example: $clr(b,Abluetext)
-
space(number) - Inserts the given number of spaces. If no argument is given, use 4 spaces.
-
-
@@ -414,26 +102,6 @@ given, it defaults to resetting the color (Table of Contents
-
diff --git a/docs/1.0-dev/Evennia-API.html b/docs/1.0-dev/Evennia-API.html
index b326159efe..2dee30a7d7 100644
--- a/docs/1.0-dev/Evennia-API.html
+++ b/docs/1.0-dev/Evennia-API.html
@@ -132,6 +132,7 @@ The flat API is defined in
diff --git a/docs/1.0-dev/_modules/evennia/commands/default/building.html b/docs/1.0-dev/_modules/evennia/commands/default/building.html
index a017a1d9f5..99e52156b6 100644
--- a/docs/1.0-dev/_modules/evennia/commands/default/building.html
+++ b/docs/1.0-dev/_modules/evennia/commands/default/building.html
@@ -49,7 +49,7 @@
fromevennia.objects.modelsimportObjectDBfromevennia.locks.lockhandlerimportLockExceptionfromevennia.commands.cmdhandlerimportget_and_merge_cmdsets
-fromevennia.utilsimportcreate,utils,search,logger
+fromevennia.utilsimportcreate,utils,search,logger,funcparserfromevennia.utils.utilsimport(inherits_from,class_from_module,
@@ -64,10 +64,11 @@
fromevennia.utils.evmoreimportEvMorefromevennia.prototypesimportspawner,prototypesasprotlib,menusasolc_menusfromevennia.utils.ansiimportrawasansi_raw
-fromevennia.utils.inlinefuncsimportrawasinlinefunc_rawCOMMAND_DEFAULT_CLASS=class_from_module(settings.COMMAND_DEFAULT_CLASS)
+_FUNCPARSER=None
+
# limit symbol import for API__all__=("ObjManipCommand",
@@ -2164,7 +2165,8 @@
)if"prototype"inself.switches:
- modified=spawner.batch_update_objects_with_prototype(prototype,objects=[obj])
+ modified=spawner.batch_update_objects_with_prototype(
+ prototype,objects=[obj],caller=self.caller)prototype_success=modified>0ifnotprototype_success:caller.msg("Prototype %s failed to apply."%prototype["key"])
@@ -2422,12 +2424,16 @@
value (any): Attribute value. Returns: """
+ global_FUNCPARSER
+ ifnot_FUNCPARSER:
+ _FUNCPARSER=funcparser.FuncParser(settings.FUNCPARSER_OUTGOING_MESSAGES_MODULES)
+
ifattrisNone:return"No such attribute was found."value=utils.to_str(value)ifcrop:value=utils.crop(value)
- value=inlinefunc_raw(ansi_raw(value))
+ value=_FUNCPARSER.parse(ansi_raw(value),escape=True)ifcategory:returnf"{attr}[{category}] = {value}"else:
@@ -3500,7 +3506,7 @@
"Python structures are allowed. \nMake sure to use correct ""Python syntax. Remember especially to put quotes around all ""strings inside lists and dicts.|n For more advanced uses, embed "
- "inlinefuncs in the strings."
+ "funcparser callables ($funcs) in the strings.")else:string="Expected {}, got {}.".format(expect,type(prototype))
@@ -3596,7 +3602,7 @@
returntry:n_updated=spawner.batch_update_objects_with_prototype(
- prototype,objects=existing_objects
+ prototype,objects=existing_objects,caller=caller,)exceptException:logger.log_trace()
@@ -3848,7 +3854,7 @@
# proceed to spawningtry:
- forobjinspawner.spawn(prototype):
+ forobjinspawner.spawn(prototype,caller=self.caller):self.caller.msg("Spawned %s."%obj.get_display_name(self.caller))ifnotprototype.get("location")andnotnoloc:# we don't hardcode the location in the prototype (unless the user
diff --git a/docs/1.0-dev/_modules/evennia/contrib/tutorial_world/intro_menu.html b/docs/1.0-dev/_modules/evennia/contrib/tutorial_world/intro_menu.html
index 17a038db7b..89fadacb9d 100644
--- a/docs/1.0-dev/_modules/evennia/contrib/tutorial_world/intro_menu.html
+++ b/docs/1.0-dev/_modules/evennia/contrib/tutorial_world/intro_menu.html
@@ -737,27 +737,27 @@
After playing through the tutorial-world quest, if you aim to make a game withEvennia you are wise to take a look at the |wEvennia documentation|n at
- |yhttps://github.com/evennia/evennia/wiki|n
+ |yhttps://www.evennia.com/docs/latest|n- You can start by trying to build some stuff by following the |wBuilder quick-start|n:
- |yhttps://github.com/evennia/evennia/wiki/Building-Quickstart|n
+ |yhttps://www.evennia.com/docs/latest/Building-Quickstart|n- The tutorial-world may or may not be your cup of tea, but it does show off several |wuseful tools|n of Evennia. You may want to check out how it works:
- |yhttps://github.com/evennia/evennia/wiki/Tutorial-World-Introduction|n
+ |yhttps://www.evennia.com/docs/latest/Tutorial-World-Introduction|n- You can then continue looking through the |wTutorials|n and pick one that fits your level of understanding.
- |yhttps://github.com/evennia/evennia/wiki/Tutorials|n
+ |yhttps://www.evennia.com/docs/latest/Tutorials|n- Make sure to |wjoin our forum|n and connect to our |wsupport chat|n! The Evennia community is very active and friendly and no question is too simple. You will often quickly get help. You can everything you need linked from
- |yhttp://www.evennia.com|n
+ |yhttps://www.evennia.com|n# ---------------------------------------------------------------------------------
diff --git a/docs/1.0-dev/_modules/evennia/objects/objects.html b/docs/1.0-dev/_modules/evennia/objects/objects.html
index dd8fbe351f..9bca109ab9 100644
--- a/docs/1.0-dev/_modules/evennia/objects/objects.html
+++ b/docs/1.0-dev/_modules/evennia/objects/objects.html
@@ -62,6 +62,7 @@
fromevennia.scripts.scripthandlerimportScriptHandlerfromevennia.commandsimportcmdset,commandfromevennia.commands.cmdsethandlerimportCmdSetHandler
+fromevennia.utilsimportfuncparserfromevennia.utilsimportcreatefromevennia.utilsimportsearchfromevennia.utilsimportlogger
@@ -89,6 +90,12 @@
# the sessid_max is based on the length of the db_sessid csv field (excluding commas)_SESSID_MAX=16if_MULTISESSION_MODEin(1,3)else1
+_MSG_CONTENTS_PARSER=funcparser.FuncParser(
+ {"you":funcparser.funcparser_callable_you,
+ "You":funcparser.funcparser_callable_You,
+ "conj":funcparser.funcparser_callable_conjugate
+ })
+
[docs]classObjectSessionHandler(object):"""
@@ -759,64 +766,94 @@
text (str or tuple): Message to send. If a tuple, this should be on the valid OOB outmessage form `(message, {kwargs})`, where kwargs are optional data passed to the `text`
- outputfunc.
+ outputfunc. The message will be parsed for `{key}` formatting and
+ `$You/$you()/$You(key)` and `$conj(verb)` inline function callables.
+ The `key` is taken from the `mapping` kwarg {"key": object, ...}`.
+ The `mapping[key].get_display_name(looker=recipient)` will be called
+ for that key for every recipient of the string. exclude (list, optional): A list of objects not to send to. from_obj (Object, optional): An object designated as the "sender" of the message. See `DefaultObject.msg()` for more info. mapping (dict, optional): A mapping of formatting keys
- `{"key":<object>, "key2":<object2>,...}. The keys
- must match `{key}` markers in the `text` if this is a string or
- in the internal `message` if `text` is a tuple. These
- formatting statements will be
- replaced by the return of `<object>.get_display_name(looker)`
- for every looker in contents that receives the
- message. This allows for every object to potentially
- get its own customized string.
- Keyword Args:
- Keyword arguments will be passed on to `obj.msg()` for all
- messaged objects.
+ `{"key":<object>, "key2":<object2>,...}.
+ The keys must either match `{key}` or `$You(key)/$you(key)` markers
+ in the `text` string. If `<object>` doesn't have a `get_display_name`
+ method, it will be returned as a string. If not set, a key `you` will
+ be auto-added to point to `from_obj` if given, otherwise to `self`.
+ **kwargs: Keyword arguments will be passed on to `obj.msg()` for all
+ messaged objects. Notes:
- The `mapping` argument is required if `message` contains
- {}-style format syntax. The keys of `mapping` should match
- named format tokens, and its values will have their
- `get_display_name()` function called for each object in
- the room before substitution. If an item in the mapping does
- not have `get_display_name()`, its string value will be used.
+ For 'actor-stance' reporting (You say/Name says), use the
+ `$You()/$you()/$You(key)` and `$conj(verb)` (verb-conjugation)
+ inline callables. This will use the respective `get_display_name()`
+ for all onlookers except for `from_obj or self`, which will become
+ 'You/you'. If you use `$You/you(key)`, the key must be in `mapping`.
- Example:
- Say Char is a Character object and Npc is an NPC object:
+ For 'director-stance' reporting (Name says/Name says), use {key}
+ syntax directly. For both `{key}` and `You/you(key)`,
+ `mapping[key].get_display_name(looker=recipient)` may be called
+ depending on who the recipient is.
- char.location.msg_contents(
- "{attacker} kicks {defender}",
- mapping=dict(attacker=char, defender=npc), exclude=(char, npc))
+ Examples:
- This will result in everyone in the room seeing 'Char kicks NPC'
- where everyone may potentially see different results for Char and Npc
- depending on the results of `char.get_display_name(looker)` and
- `npc.get_display_name(looker)` for each particular onlooker
+ Let's assume
+ - `player1.key -> "Player1"`,
+ `player1.get_display_name(looker=player2) -> "The First girl"`
+ - `player2.key -> "Player2"`,
+ `player2.get_display_name(looker=player1) -> "The Second girl"`
+
+ Actor-stance:
+ ::
+
+ char.location.msg_contents(
+ "$You() $conj(attack) $you(defender).",
+ mapping={"defender": player2})
+
+ - player1 will see `You attack The Second girl.`
+ - player2 will see 'The First girl attacks you.'
+
+ Director-stance:
+ ::
+
+ char.location.msg_contents(
+ "{attacker} attacks {defender}.",
+ mapping={"attacker:player1, "defender":player2})
+
+ - player1 will see: 'Player1 attacks The Second girl.'
+ - player2 will see: 'The First girl attacks Player2' """# we also accept an outcommand on the form (message, {kwargs})is_outcmd=textandis_iter(text)inmessage=text[0]ifis_outcmdelsetextoutkwargs=text[1]ifis_outcmdandlen(text)>1else{}
+ mapping=mappingor{}
+ you=from_objorself
+
+ if'you'notinmapping:
+ mapping[you]=youcontents=self.contentsifexclude:exclude=make_iter(exclude)contents=[objforobjincontentsifobjnotinexclude]
- forobjincontents:
- ifmapping:
- substitutions={
- t:sub.get_display_name(obj)ifhasattr(sub,"get_display_name")elsestr(sub)
- fort,subinmapping.items()
- }
- outmessage=inmessage.format(**substitutions)
- else:
- outmessage=inmessage
- obj.msg(text=(outmessage,outkwargs),from_obj=from_obj,**kwargs)
"""
-Protfuncs are function-strings embedded in a prototype and allows for a builder to create a
-prototype with custom logics without having access to Python. The Protfunc is parsed using the
-inlinefunc parser but is fired at the moment the spawning happens, using the creating object's
-session as input.
+Protfuncs are FuncParser-callables that can be embedded in a prototype to
+provide custom logic without having access to Python. The protfunc is parsed at
+the time of spawning, using the creating object's session as input. If the
+protfunc returns a non-string, this is what will be added to the prototype.In the prototype dict, the protfunc is specified as a string inside the prototype, e.g.: { ...
- "key": "$funcname(arg1, arg2, ...)"
+ "key": "$funcname(args, kwargs)" ... }
-and multiple functions can be nested (no keyword args are supported). The result will be used as the
-value for that prototype key for that individual spawn.
-
-Available protfuncs are callables in one of the modules of `settings.PROT_FUNC_MODULES`. They
-are specified as functions
+Available protfuncs are either all callables in one of the modules of `settings.PROT_FUNC_MODULES`
+or all callables added to a dict FUNCPARSER_CALLABLES in such a module. def funcname (*args, **kwargs)
-where *args are the arguments given in the prototype, and **kwargs are inserted by Evennia:
+At spawn-time the spawner passes the following extra kwargs into each callable (in addition to
+what is added in the call itself): - session (Session): The Session of the entity spawning using this prototype. - prototype (dict): The dict this protfunc is a part of. - current_key (str): The active key this value belongs to in the prototype.
- - testing (bool): This is set if this function is called as part of the prototype validation; if
- set, the protfunc should take care not to perform any persistent actions, such as operate on
- objects or add things to the database.Any traceback raised by this function will be handled at the time of spawning and abort the spawnbefore any object is created/updated. It must otherwise return the value to store for the specified
@@ -77,315 +72,35 @@
"""
-fromastimportliteral_eval
-fromrandomimportrandintasbase_randint,randomasbase_random,choiceasbase_choice
-importre
-
-fromevennia.utilsimportsearch
-fromevennia.utils.utilsimportjustifyasbase_justify,is_iter,to_str
-
-_PROTLIB=None
-
-_RE_DBREF=re.compile(r"\#[0-9]+")
+fromevennia.utilsimportfuncparser
-# default protfuncs
-
-
-
[docs]defprotfunc_callable_protkey(*args,**kwargs):"""
- Usage: $random()
- Returns a random value in the interval [0, 1)
-
- """
- returnbase_random()
-
-
-
[docs]defrandint(*args,**kwargs):
- """
- Usage: $randint(start, end)
- Returns random integer in interval [start, end]
-
- """
- iflen(args)!=2:
- raiseTypeError("$randint needs two arguments - start and end.")
- start,end=int(args[0]),int(args[1])
- returnbase_randint(start,end)
[docs]defchoice(*args,**kwargs):
- """
- Usage: $choice(val, val, val, ...)
- Returns one of the values randomly
- """
- ifargs:
- returnbase_choice(args)
- return""
-
-
-
[docs]deffull_justify(*args,**kwargs):
-
- """
- Usage: $full_justify(<text>)
- Returns <text> filling up screen width by adding extra space.
-
- """
- ifargs:
- returnbase_justify(args[0],align="f")
- return""
-
-
-
[docs]defprotkey(*args,**kwargs):
- """
- Usage: $protkey(<key>)
+ Usage: $protkey(keyname) Returns the value of another key in this prototoype. Will raise an error if the key is not found in this prototype. """
- ifargs:
- prototype=kwargs["prototype"]
- returnprototype[args[0].strip()]
[docs]defadd(*args,**kwargs):
- """
- Usage: $add(val1, val2)
- Returns the result of val1 + val2. Values must be
- valid simple Python structures possible to add,
- such as numbers, lists etc.
-
- """
- iflen(args)>1:
- val1,val2=args[0],args[1]
- # try to convert to python structures, otherwise, keep as strings
- try:
- val1=literal_eval(val1.strip())
- exceptException:
- pass
- try:
- val2=literal_eval(val2.strip())
- exceptException:
- pass
- returnval1+val2
- raiseValueError("$add requires two arguments.")
-
[docs]defsub(*args,**kwargs):
- """
- Usage: $del(val1, val2)
- Returns the value of val1 - val2. Values must be
- valid simple Python structures possible to
- subtract.
-
- """
- iflen(args)>1:
- val1,val2=args[0],args[1]
- # try to convert to python structures, otherwise, keep as strings
- try:
- val1=literal_eval(val1.strip())
- exceptException:
- pass
- try:
- val2=literal_eval(val2.strip())
- exceptException:
- pass
- returnval1-val2
- raiseValueError("$sub requires two arguments.")
-
-
-
[docs]defmult(*args,**kwargs):
- """
- Usage: $mul(val1, val2)
- Returns the value of val1 * val2. The values must be
- valid simple Python structures possible to
- multiply, like strings and/or numbers.
-
- """
- iflen(args)>1:
- val1,val2=args[0],args[1]
- # try to convert to python structures, otherwise, keep as strings
- try:
- val1=literal_eval(val1.strip())
- exceptException:
- pass
- try:
- val2=literal_eval(val2.strip())
- exceptException:
- pass
- returnval1*val2
- raiseValueError("$mul requires two arguments.")
-
-
-
[docs]defdiv(*args,**kwargs):
- """
- Usage: $div(val1, val2)
- Returns the value of val1 / val2. Values must be numbers and
- the result is always a float.
-
- """
- iflen(args)>1:
- val1,val2=args[0],args[1]
- # try to convert to python structures, otherwise, keep as strings
- try:
- val1=literal_eval(val1.strip())
- exceptException:
- pass
- try:
- val2=literal_eval(val2.strip())
- exceptException:
- pass
- returnval1/float(val2)
- raiseValueError("$mult requires two arguments.")
[docs]defeval(*args,**kwargs):
- """
- Usage $eval(<expression>)
- Returns evaluation of a simple Python expression. The string may *only* consist of the following
- Python literal structures: strings, numbers, tuples, lists, dicts, booleans,
- and None. The strings can also contain #dbrefs. Escape embedded protfuncs as $$protfunc(..)
- - those will then be evaluated *after* $eval.
-
- """
- global_PROTLIB
- ifnot_PROTLIB:
- fromevennia.prototypesimportprototypesas_PROTLIB
-
- string=",".join(args)
- struct=literal_eval(string)
-
- ifisinstance(struct,str):
- # we must shield the string, otherwise it will be merged as a string and future
- # literal_evas will pick up e.g. '2' as something that should be converted to a number
- struct='"{}"'.format(struct)
-
- # convert any #dbrefs to objects (also in nested structures)
- struct=_PROTLIB.value_to_obj_or_any(struct)
-
- returnstruct
-
-
-def_obj_search(*args,**kwargs):
- "Helper function to search for an object"
-
- query="".join(args)
- session=kwargs.get("session",None)
- return_list=kwargs.pop("return_list",False)
- account=None
-
- ifsession:
- account=session.account
-
- targets=search.search_object(query)
-
- ifreturn_list:
- retlist=[]
- ifaccount:
- fortargetintargets:
- iftarget.access(account,target,"control"):
- retlist.append(target)
- else:
- retlist=targets
- returnretlist
- else:
- # single-match
- ifnottargets:
- raiseValueError("$obj: Query '{}' gave no matches.".format(query))
- iflen(targets)>1:
- raiseValueError(
- "$obj: Query '{query}' gave {nmatches} matches. Limit your "
- "query or use $objlist instead.".format(query=query,nmatches=len(targets))
- )
- target=targets[0]
- ifaccount:
- ifnottarget.access(account,target,"control"):
- raiseValueError(
- "$obj: Obj {target}(#{dbref} cannot be added - "
- "Account {account} does not have 'control' access.".format(
- target=target.key,dbref=target.id,account=account
- )
- )
- returntarget
-
-
-
[docs]defobj(*args,**kwargs):
- """
- Usage $obj(<query>)
- Returns one Object searched globally by key, alias or #dbref. Error if more than one.
-
- """
- obj=_obj_search(return_list=False,*args,**kwargs)
- ifobj:
- return"#{}".format(obj.id)
- return"".join(args)
-
-
-
[docs]defobjlist(*args,**kwargs):
- """
- Usage $objlist(<query>)
- Returns list with one or more Objects searched globally by key, alias or #dbref.
-
- """
- return["#{}".format(obj.id)forobjin_obj_search(return_list=True,*args,**kwargs)]
-
-
-
[docs]defdbref(*args,**kwargs):
- """
- Usage $dbref(<#dbref>)
- Validate that a #dbref input is valid.
- """
- ifnotargsorlen(args)<1or_RE_DBREF.match(args[0])isNone:
- raiseValueError("$dbref requires a valid #dbref argument.")
-
- returnobj(args[0])
+# this is picked up by FuncParser
+FUNCPARSER_CALLABLES={
+ "protkey":protfunc_callable_protkey,
+ **funcparser.FUNCPARSER_CALLABLES,
+ **funcparser.SEARCHING_CALLABLES,
+}
[docs]defprotfunc_parser(value,available_functions=None,testing=False,stacktrace=False,caller=None,**kwargs):""" Parse a prototype value string for a protfunc and process it.
@@ -774,45 +767,27 @@
protfuncs, all other types are returned as-is. available_functions (dict, optional): Mapping of name:protfunction to use for this parsing. If not set, use default sources.
- testing (bool, optional): Passed to protfunc. If in a testing mode, some protfuncs may
- behave differently. stacktrace (bool, optional): If set, print the stack parsing process of the protfunc-parser. Keyword Args: session (Session): Passed to protfunc. Session of the entity spawning the prototype. protototype (dict): Passed to protfunc. The dict this protfunc is a part of. current_key(str): Passed to protfunc. The key in the prototype that will hold this value.
+ caller (Object or Account): This is necessary for certain protfuncs that perform object
+ searches and have to check permissions. any (any): Passed on to the protfunc. Returns:
- testresult (tuple): If `testing` is set, returns a tuple (error, result) where error is
- either None or a string detailing the error from protfunc_parser or seen when trying to
- run `literal_eval` on the parsed string.
- any (any): A structure to replace the string on the prototype level. If this is a
- callable or a (callable, (args,)) structure, it will be executed as if one had supplied
- it to the prototype directly. This structure is also passed through literal_eval so one
- can get actual Python primitives out of it (not just strings). It will also identify
- eventual object #dbrefs in the output from the protfunc.
+ any: A structure to replace the string on the prototype leve. Note
+ that FunctionParser functions $funcname(*args, **kwargs) can return any
+ data type to insert into the prototype. """ifnotisinstance(value,str):returnvalue
- available_functions=PROT_FUNCSifavailable_functionsisNoneelseavailable_functions
+ result=FUNC_PARSER.parse(value,raise_errors=True,return_str=False,caller=caller,**kwargs)
- result=inlinefuncs.parse_inlinefunc(
- value,available_funcs=available_functions,stacktrace=stacktrace,testing=testing,**kwargs
- )
-
- err=None
- try:
- result=literal_eval(result)
- exceptValueError:
- pass
- exceptExceptionasexc:
- err=str(exc)
- iftesting:
- returnerr,resultreturnresult
@@ -827,7 +802,7 @@
clr (str, optional): What coloration tag to use. """out=[]
- forprotfunc_name,protfuncinPROT_FUNCS.items():
+ forprotfunc_name,protfuncinFUNC_PARSER.callables.items():out.append("- |c${name}|n - |W{docs}".format(name=protfunc_name,docs=protfunc.__doc__.strip().replace("\n","")
@@ -952,7 +927,7 @@
returndefault
[docs]definit_spawn_value(value,validator=None,caller=None):""" Analyze the prototype value and produce a value useful at the point of spawning.
@@ -963,6 +938,8 @@
other - will be assigned depending on the variable type validator (callable, optional): If given, this will be called with the value to check and guarantee the outcome is of a given type.
+ caller (Object or Account): This is necessary for certain protfuncs that perform object
+ searches and have to check permissions. Returns: any (any): The (potentially pre-processed value to use for this prototype key)
@@ -977,7 +954,7 @@
value=validator(value[0](*make_iter(args)))else:value=validator(value)
- result=protfunc_parser(value)
+ result=protfunc_parser(value,caller=caller)ifresult!=value:returnvalidator(result)returnresult
[docs]defbatch_update_objects_with_prototype(prototype,diff=None,objects=None,
+ exact=False,caller=None):""" Update existing objects with the latest version of the prototype.
@@ -666,6 +667,7 @@
if it's not set in the prototype. With `exact=True`, all un-specified properties of the objects will be removed if they exist. This will lead to a more accurate 1:1 correlation between the object and the prototype but is usually impractical.
+ caller (Object or Account, optional): This may be used by protfuncs to do permission checks. Returns: changed (int): The number of objects that had changes applied to them.
@@ -717,33 +719,33 @@
do_save=Trueifkey=="key":
- obj.db_key=init_spawn_value(val,str)
+ obj.db_key=init_spawn_value(val,str,caller=caller)elifkey=="typeclass":
- obj.db_typeclass_path=init_spawn_value(val,str)
+ obj.db_typeclass_path=init_spawn_value(val,str,caller=caller)elifkey=="location":
- obj.db_location=init_spawn_value(val,value_to_obj)
+ obj.db_location=init_spawn_value(val,value_to_obj,caller=caller)elifkey=="home":
- obj.db_home=init_spawn_value(val,value_to_obj)
+ obj.db_home=init_spawn_value(val,value_to_obj,caller=caller)elifkey=="destination":
- obj.db_destination=init_spawn_value(val,value_to_obj)
+ obj.db_destination=init_spawn_value(val,value_to_obj,caller=caller)elifkey=="locks":ifdirective=="REPLACE":obj.locks.clear()
- obj.locks.add(init_spawn_value(val,str))
+ obj.locks.add(init_spawn_value(val,str,caller=caller))elifkey=="permissions":ifdirective=="REPLACE":obj.permissions.clear()
- obj.permissions.batch_add(*(init_spawn_value(perm,str)forperminval))
+ obj.permissions.batch_add(*(init_spawn_value(perm,str,caller=caller)forperminval))elifkey=="aliases":ifdirective=="REPLACE":obj.aliases.clear()
- obj.aliases.batch_add(*(init_spawn_value(alias,str)foraliasinval))
+ obj.aliases.batch_add(*(init_spawn_value(alias,str,caller=caller)foraliasinval))elifkey=="tags":ifdirective=="REPLACE":obj.tags.clear()obj.tags.batch_add(*(
- (init_spawn_value(ttag,str),tcategory,tdata)
+ (init_spawn_value(ttag,str,caller=caller),tcategory,tdata)forttag,tcategory,tdatainval))
@@ -753,8 +755,8 @@
obj.attributes.batch_add(*((
- init_spawn_value(akey,str),
- init_spawn_value(aval,value_to_obj),
+ init_spawn_value(akey,str,caller=caller),
+ init_spawn_value(aval,value_to_obj,caller=caller),acategory,alocks,)
@@ -765,7 +767,7 @@
# we don't auto-rerun exec statements, it would be huge security risk!passelse:
- obj.attributes.add(key,init_spawn_value(val,value_to_obj))
+ obj.attributes.add(key,init_spawn_value(val,value_to_obj,caller=caller))elifdirective=="REMOVE":do_save=Trueifkey=="key":
@@ -878,7 +880,7 @@
# Spawner mechanism
-
[docs]defspawn(*prototypes,caller=None,**kwargs):""" Spawn a number of prototyped objects.
@@ -887,6 +889,7 @@
prototype_key (will be used to find the prototype) or a full prototype dictionary. These will be batched-spawned as one object each. Keyword Args:
+ caller (Object or Account, optional): This may be used by protfuncs to do access checks. prototype_modules (str or list): A python-path to a prototype module, or a list of such paths. These will be used to build the global protparents dictionary accessible by the input
@@ -952,39 +955,39 @@
"key","Spawned-{}".format(hashlib.md5(bytes(str(time.time()),"utf-8")).hexdigest()[:6]),)
- create_kwargs["db_key"]=init_spawn_value(val,str)
+ create_kwargs["db_key"]=init_spawn_value(val,str,caller=caller)val=prot.pop("location",None)
- create_kwargs["db_location"]=init_spawn_value(val,value_to_obj)
+ create_kwargs["db_location"]=init_spawn_value(val,value_to_obj,caller=caller)val=prot.pop("home",None)ifval:
- create_kwargs["db_home"]=init_spawn_value(val,value_to_obj)
+ create_kwargs["db_home"]=init_spawn_value(val,value_to_obj,caller=caller)else:try:
- create_kwargs["db_home"]=init_spawn_value(settings.DEFAULT_HOME,value_to_obj)
+ create_kwargs["db_home"]=init_spawn_value(settings.DEFAULT_HOME,value_to_obj,caller=caller)exceptObjectDB.DoesNotExist:# settings.DEFAULT_HOME not existing is common for unittestspassval=prot.pop("destination",None)
- create_kwargs["db_destination"]=init_spawn_value(val,value_to_obj)
+ create_kwargs["db_destination"]=init_spawn_value(val,value_to_obj,caller=caller)val=prot.pop("typeclass",settings.BASE_OBJECT_TYPECLASS)
- create_kwargs["db_typeclass_path"]=init_spawn_value(val,str)
+ create_kwargs["db_typeclass_path"]=init_spawn_value(val,str,caller=caller)# extract calls to handlersval=prot.pop("permissions",[])
- permission_string=init_spawn_value(val,make_iter)
+ permission_string=init_spawn_value(val,make_iter,caller=caller)val=prot.pop("locks","")
- lock_string=init_spawn_value(val,str)
+ lock_string=init_spawn_value(val,str,caller=caller)val=prot.pop("aliases",[])
- alias_string=init_spawn_value(val,make_iter)
+ alias_string=init_spawn_value(val,make_iter,caller=caller)val=prot.pop("tags",[])tags=[]for(tag,category,*data)inval:
- tags.append((init_spawn_value(tag,str),category,data[0]ifdataelseNone))
+ tags.append((init_spawn_value(tag,str,caller=caller),category,data[0]ifdataelseNone))prototype_key=prototype.get("prototype_key",None)ifprototype_key:
@@ -992,11 +995,11 @@
tags.append((prototype_key,PROTOTYPE_TAG_CATEGORY))val=prot.pop("exec","")
- execs=init_spawn_value(val,make_iter)
+ execs=init_spawn_value(val,make_iter,caller=caller)# extract ndb assignmentsnattributes=dict(
- (key.split("_",1)[1],init_spawn_value(val,value_to_obj))
+ (key.split("_",1)[1],init_spawn_value(val,value_to_obj,caller=caller))forkey,valinprot.items()ifkey.startswith("ndb_"))
@@ -1005,7 +1008,7 @@
val=make_iter(prot.pop("attrs",[]))attributes=[]for(attrname,value,*rest)inval:
- attributes.append((attrname,init_spawn_value(value),
+ attributes.append((attrname,init_spawn_value(value,caller=caller),rest[0]ifrestelseNone,rest[1]iflen(rest)>1elseNone))simple_attributes=[]
@@ -1017,7 +1020,7 @@
continueelse:simple_attributes.append(
- (key,init_spawn_value(value,value_to_obj_or_any),None,None)
+ (key,init_spawn_value(value,value_to_obj_or_any,caller=caller),None,None))attributes=attributes+simple_attributes
diff --git a/docs/1.0-dev/_modules/evennia/server/deprecations.html b/docs/1.0-dev/_modules/evennia/server/deprecations.html
index e2a6cf8dcf..d5b5651b42 100644
--- a/docs/1.0-dev/_modules/evennia/server/deprecations.html
+++ b/docs/1.0-dev/_modules/evennia/server/deprecations.html
@@ -101,6 +101,21 @@
"Update your settings file (see evennia/settings_default.py ""for more info).")
+ depstring=(
+ "settings.{} was renamed to {}. Update your settings file (the FuncParser "
+ "replaces and generalizes that which inlinefuncs used to do).")
+ ifhasattr(settings,"INLINEFUNC_ENABLED"):
+ raiseDeprecationWarning(depstring.format(
+ "settings.INLINEFUNC_ENABLED","FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLE"))
+ ifhasattr(settings,"INLINEFUNC_STACK_MAXSIZE"):
+ raiseDeprecationWarning(depstring.format(
+ "settings.INLINEFUNC_STACK_MAXSIZE","FUNCPARSER_MAX_NESTING"))
+ ifhasattr(settings,"INLINEFUNC_MODULES"):
+ raiseDeprecationWarning(depstring.format(
+ "settings.INLINEFUNC_MODULES","FUNCPARSER_OUTGOING_MESSAGES_MODULES"))
+ ifhasattr(settings,"PROTFUNC_MODULES"):
+ raiseDeprecationWarning(depstring.format(
+ "settings.PROTFUNC_MODULES","FUNCPARSER_PROTOTYPE_VALUE_MODULES"))gametime_deprecation=("The settings TIME_SEC_PER_MIN, TIME_MIN_PER_HOUR,"
diff --git a/docs/1.0-dev/_modules/evennia/server/sessionhandler.html b/docs/1.0-dev/_modules/evennia/server/sessionhandler.html
index d80c299abf..ceb026d560 100644
--- a/docs/1.0-dev/_modules/evennia/server/sessionhandler.html
+++ b/docs/1.0-dev/_modules/evennia/server/sessionhandler.html
@@ -70,10 +70,9 @@
fromevennia.server.portalimportampfromevennia.server.signalsimportSIGNAL_ACCOUNT_POST_LOGIN,SIGNAL_ACCOUNT_POST_LOGOUTfromevennia.server.signalsimportSIGNAL_ACCOUNT_POST_FIRST_LOGIN,SIGNAL_ACCOUNT_POST_LAST_LOGOUT
-fromevennia.utils.inlinefuncsimportparse_inlinefuncfromcodecsimportdecodeascodecs_decode
-_INLINEFUNC_ENABLED=settings.INLINEFUNC_ENABLED
+_FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED=settings.FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED# delayed imports_AccountDB=None
@@ -101,6 +100,9 @@
_MAX_SERVER_COMMANDS_PER_SECOND=100.0_MAX_SESSION_COMMANDS_PER_SECOND=5.0_MODEL_MAP=None
+_FUNCPARSER=None
+
+
# input handlers
@@ -193,7 +195,8 @@
[docs]defclean_senddata(self,session,kwargs):"""
- Clean up data for sending across the AMP wire. Also apply INLINEFUNCS.
+ Clean up data for sending across the AMP wire. Also apply the
+ FuncParser using callables from `settings.FUNCPARSER_OUTGOING_MESSAGES_MODULES`. Args: session (Session): The relevant session instance.
@@ -209,10 +212,15 @@
Returns: kwargs (dict): A cleaned dictionary of cmdname:[[args],{kwargs}] pairs, where the keys, args and kwargs have all been converted to
- send-safe entities (strings or numbers), and inlinefuncs have been
+ send-safe entities (strings or numbers), and funcparser parsing has been applied. """
+ global_FUNCPARSER
+ ifnot_FUNCPARSER:
+ fromevennia.utils.funcparserimportFuncParser
+ _FUNCPARSER=FuncParser(settings.FUNCPARSER_OUTGOING_MESSAGES_MODULE,raise_errors=True)
+
options=kwargs.pop("options",None)or{}raw=options.get("raw",False)strip_inlinefunc=options.get("strip_inlinefunc",False)
@@ -244,9 +252,10 @@
elifisinstance(data,(str,bytes)):data=_utf8(data)
- if_INLINEFUNC_ENABLEDandnotrawandisinstance(self,ServerSessionHandler):
- # only parse inlinefuncs on the outgoing path (sessionhandler->)
- data=parse_inlinefunc(data,strip=strip_inlinefunc,session=session)
+ if_FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLEDandnotrawandisinstance(self,ServerSessionHandler):
+ # only apply funcparser on the outgoing path (sessionhandler->)
+ # data = parse_inlinefunc(data, strip=strip_inlinefunc, session=session)
+ data=_FUNCPARSER.parse(data,strip=strip_inlinefunc,session=session)returnstr(data)elif(
diff --git a/docs/1.0-dev/_modules/evennia/utils/funcparser.html b/docs/1.0-dev/_modules/evennia/utils/funcparser.html
new file mode 100644
index 0000000000..dd66d00821
--- /dev/null
+++ b/docs/1.0-dev/_modules/evennia/utils/funcparser.html
@@ -0,0 +1,1287 @@
+
+
+
+
+
+
+
+ evennia.utils.funcparser — Evennia 1.0-dev documentation
+
+
+
+
+
+
+
+
+
+
+
+
+"""
+Generic function parser for functions embedded in a string, on the form
+`$funcname(*args, **kwargs)`, for example:
+
+```
+"A string $foo() with $bar(a, b, c, $moo(), d=23) etc."
+```
+
+Each arg/kwarg can also be another nested function. These will be executed
+inside-out and their return will used as arguments for the enclosing function
+(so the same as for regular Python function execution).
+
+This is the base for all forms of embedded func-parsing, like inlinefuncs and
+protfuncs. Each function available to use must be registered as a 'safe'
+function for the parser to accept it. This is usually done in a module with
+regular Python functions on the form:
+
+```python
+# in a module whose path is passed to the parser
+
+def _helper(x):
+ # use underscore to NOT make the function available as a callable
+
+def funcname(*args, **kwargs):
+ # this can be accecssed as $funcname(*args, **kwargs)
+ # it must always accept *args and **kwargs.
+ ...
+ return something
+```
+
+Usage:
+
+```python
+from evennia.utils.funcparser
+
+parser = FuncParser("path.to.module_with_callables")
+result = parser.parse("String with $funcname() in it")
+
+```
+
+The `FuncParser` also accepts a direct dict mapping of `{'name': callable, ...}`.
+
+---
+
+"""
+importre
+importdataclasses
+importinspect
+importrandom
+fromfunctoolsimportpartial
+fromdjango.confimportsettings
+fromevennia.utilsimportlogger
+fromevennia.utils.utilsimport(
+ make_iter,callables_from_module,variable_from_module,pad,crop,justify,
+ safe_convert_to_types)
+fromevennia.utilsimportsearch
+fromevennia.utils.verb_conjugation.conjugateimportverb_actor_stance_components
+
+# setup
+
+_CLIENT_DEFAULT_WIDTH=settings.CLIENT_DEFAULT_WIDTH
+_MAX_NESTING=settings.FUNCPARSER_MAX_NESTING
+_START_CHAR=settings.FUNCPARSER_START_CHAR
+_ESCAPE_CHAR=settings.FUNCPARSER_ESCAPE_CHAR
+
+
+@dataclasses.dataclass
+class_ParsedFunc:
+ """
+ Represents a function parsed from the string
+
+ """
+ prefix:str=_START_CHAR
+ funcname:str=""
+ args:list=dataclasses.field(default_factory=list)
+ kwargs:dict=dataclasses.field(default_factory=dict)
+
+ # state storage
+ fullstr:str=""
+ infuncstr:str=""
+ single_quoted:bool=False
+ double_quoted:bool=False
+ current_kwarg:str=""
+ open_lparens:int=0
+ open_lsquate:int=0
+ open_lcurly:int=0
+ exec_return=""
+
+ defget(self):
+ returnself.funcname,self.args,self.kwargs
+
+ def__str__(self):
+ returnself.fullstr+self.infuncstr
+
+
+
[docs]classParsingError(RuntimeError):
+ """
+ Failed to parse for some reason.
+ """
+ pass
+
+
+
[docs]classFuncParser:
+ """
+ Sets up a parser for strings containing `$funcname(*args, **kwargs)`
+ substrings.
+
+ """
+
+
[docs]def__init__(self,
+ callables,
+ start_char=_START_CHAR,
+ escape_char=_ESCAPE_CHAR,
+ max_nesting=_MAX_NESTING,
+ **default_kwargs):
+ """
+ Initialize the parser.
+
+ Args:
+ callables (str, module, list or dict): Where to find
+ 'safe' functions to make available in the parser. If a `dict`,
+ it should be a direct mapping `{"funcname": callable, ...}`. If
+ one or mode modules or module-paths, the module(s) are first checked
+ for a dict `FUNCPARSER_CALLABLES = {"funcname", callable, ...}`. If
+ no such variable exists, all callables in the module (whose name does
+ not start with an underscore) will be made available to the parser.
+ start_char (str, optional): A character used to identify the beginning
+ of a parseable function. Default is `$`.
+ escape_char (str, optional): Prepend characters with this to have
+ them not count as a function. Default is the backtick, `\\\\`.
+ max_nesting (int, optional): How many levels of nested function calls
+ are allowed, to avoid exploitation. Default is 20.
+ **default_kwargs: These kwargs will be passed into all callables. These
+ kwargs can be overridden both by kwargs passed direcetly to `.parse` *and*
+ by kwargs given directly in the string `$funcname` call. They are
+ suitable for global defaults that is intended to be changed by the
+ user. To guarantee a call always gets a particular kwarg, pass it
+ into `.parse` as `**reserved_kwargs` instead.
+
+ """
+ ifisinstance(callables,dict):
+ loaded_callables={**callables}
+ else:
+ # load all modules/paths in sequence. Later-added will override
+ # earlier same-named callables (allows for overriding evennia defaults)
+ loaded_callables={}
+ formodule_or_pathinmake_iter(callables):
+ callables_mapping=variable_from_module(
+ module_or_path,variable="FUNCPARSER_CALLABLES")
+ ifcallables_mapping:
+ try:
+ # mapping supplied in variable
+ loaded_callables.update(callables_mapping)
+ exceptValueError:
+ raiseParsingError(
+ f"Failure to parse - {module_or_path}.FUNCPARSER_CALLABLES "
+ "(must be a dict {'funcname': callable, ...})")
+ else:
+ # use all top-level variables
+ # (handles both paths and module instances
+ loaded_callables.update(callables_from_module(module_or_path))
+ self.validate_callables(loaded_callables)
+ self.callables=loaded_callables
+ self.escape_char=escape_char
+ self.start_char=start_char
+ self.default_kwargs=default_kwargs
+
+
[docs]defvalidate_callables(self,callables):
+ """
+ Validate the loaded callables. Each callable must support at least
+ `funcname(*args, **kwargs)`.
+ property.
+
+ Args:
+ callables (dict): A mapping `{"funcname": callable, ...}` to validate
+
+ Raise:
+ AssertionError: If invalid callable was found.
+
+ Notes:
+ This is also a good method to override for individual parsers
+ needing to run any particular pre-checks.
+
+ """
+ forfuncname,clbleincallables.items():
+ try:
+ mapping=inspect.getfullargspec(clble)
+ exceptTypeError:
+ logger.log_trace(f"Could not run getfullargspec on {funcname}: {clble}")
+ else:
+ assertmapping.varargs,f"Parse-func callable '{funcname}' does not support *args."
+ assertmapping.varkw,f"Parse-func callable '{funcname}' does not support **kwargs."
+
+
[docs]defexecute(self,parsedfunc,raise_errors=False,**reserved_kwargs):
+ """
+ Execute a parsed function
+
+ Args:
+ parsedfunc (_ParsedFunc): This dataclass holds the parsed details
+ of the function.
+ raise_errors (bool, optional): Raise errors. Otherwise return the
+ string with the function unparsed.
+ **reserved_kwargs: These kwargs are _guaranteed_ to always be passed into
+ the callable on every call. It will override any default kwargs
+ _and_ also a same-named kwarg given manually in the $funcname
+ call. This is often used by Evennia to pass required data into
+ the callable, for example the current Session for inlinefuncs.
+ Returns:
+ any: The result of the execution. If this is a nested function, it
+ can be anything, otherwise it will be converted to a string later.
+ Always a string on un-raised error (the unparsed function string).
+
+ Raises:
+ ParsingError, any: A `ParsingError` if the function could not be
+ found, otherwise error from function definition. Only raised if
+ `raise_errors` is `True`
+
+ Notes:
+ The kwargs passed into the callable will be a mixture of the
+ `default_kwargs` passed into `FuncParser.__init__`, kwargs given
+ directly in the `$funcdef` string, and the `reserved_kwargs` this
+ function gets from `.parse()`. For colliding keys, funcdef-defined
+ kwargs will override default kwargs while reserved kwargs will always
+ override the other two.
+
+ """
+ funcname,args,kwargs=parsedfunc.get()
+ func=self.callables.get(funcname)
+
+ ifnotfunc:
+ ifraise_errors:
+ available=", ".join(f"'{key}'"forkeyinself.callables)
+ raiseParsingError(f"Unknown parsed function '{str(parsedfunc)}' "
+ f"(available: {available})")
+ returnstr(parsedfunc)
+
+ nargs=len(args)
+
+ # build kwargs in the proper priority order
+ kwargs={**self.default_kwargs,**kwargs,**reserved_kwargs,
+ **{'funcparser':self,"raise_errors":raise_errors}}
+
+ try:
+ ret=func(*args,**kwargs)
+ returnret
+ exceptParsingError:
+ ifraise_errors:
+ raise
+ returnstr(parsedfunc)
+ exceptException:
+ logger.log_trace()
+ ifraise_errors:
+ raise
+ returnstr(parsedfunc)
+
+
[docs]defparse(self,string,raise_errors=False,escape=False,
+ strip=False,return_str=True,**reserved_kwargs):
+ """
+ Use parser to parse a string that may or may not have
+ `$funcname(*args, **kwargs)` - style tokens in it. Only the callables
+ used to initiate the parser will be eligible for parsing.
+
+ Args:
+ string (str): The string to parse.
+ raise_errors (bool, optional): By default, a failing parse just
+ means not parsing the string but leaving it as-is. If this is
+ `True`, errors (like not closing brackets) will lead to an
+ ParsingError.
+ escape (bool, optional): If set, escape all found functions so they
+ are not executed by later parsing.
+ strip (bool, optional): If set, strip any inline funcs from string
+ as if they were not there.
+ return_str (bool, optional): If set (default), always convert the
+ parse result to a string, otherwise return the result of the
+ latest called inlinefunc (if called separately).
+ **reserved_kwargs: If given, these are guaranteed to _always_ pass
+ as part of each parsed callable's **kwargs. These override
+ same-named default options given in `__init__` as well as any
+ same-named kwarg given in the string function. This is because
+ it is often used by Evennia to pass necessary kwargs into each
+ callable (like the current Session object for inlinefuncs).
+
+ Returns:
+ str or any: The parsed string, or the same string on error (if
+ `raise_errors` is `False`). This is always a string
+
+ Raises:
+ ParsingError: If a problem is encountered and `raise_errors` is True.
+
+ """
+ start_char=self.start_char
+ escape_char=self.escape_char
+
+ # replace e.g. $$ with \$ so we only need to handle one escape method
+ string=string.replace(start_char+start_char,escape_char+start_char)
+
+ # parsing state
+ callstack=[]
+
+ single_quoted=False
+ double_quoted=False
+ open_lparens=0# open (
+ open_lsquare=0# open [
+ open_lcurly=0# open {
+ escaped=False
+ current_kwarg=""
+ exec_return=""
+
+ curr_func=None
+ fullstr=''# final string
+ infuncstr=''# string parts inside the current level of $funcdef (including $)
+
+ forcharinstring:
+
+ ifescaped:
+ # always store escaped characters verbatim
+ ifcurr_func:
+ infuncstr+=char
+ else:
+ fullstr+=char
+ escaped=False
+ continue
+
+ ifchar==escape_char:
+ # don't store the escape-char itself
+ escaped=True
+ continue
+
+ ifchar==start_char:
+ # start a new function definition (not escaped as $$)
+
+ ifcurr_func:
+ # we are starting a nested funcdef
+ return_str=True
+ iflen(callstack)>_MAX_NESTING:
+ # stack full - ignore this function
+ ifraise_errors:
+ raiseParsingError("Only allows for parsing nesting function defs "
+ f"to a max depth of {_MAX_NESTING}.")
+ infuncstr+=char
+ continue
+ else:
+ # store state for the current func and stack it
+ curr_func.current_kwarg=current_kwarg
+ curr_func.infuncstr=infuncstr
+ curr_func.single_quoted=single_quoted
+ curr_func.double_quoted=double_quoted
+ curr_func.open_lparens=open_lparens
+ curr_func.open_lsquare=open_lsquare
+ curr_func.open_lcurly=open_lcurly
+ current_kwarg=""
+ infuncstr=""
+ single_quoted=False
+ double_quoted=False
+ open_lparens=0
+ open_lsquare=0
+ open_lcurly=0
+ exec_return=""
+ callstack.append(curr_func)
+
+ # start a new func
+ curr_func=_ParsedFunc(prefix=char,fullstr=char)
+ continue
+
+ ifnotcurr_func:
+ # a normal piece of string
+ fullstr+=char
+ # this must always be a string
+ return_str=True
+ continue
+
+ # in a function def (can be nested)
+
+ ifexec_return!=''andcharnotin(",=)"):
+ # if exec_return is followed by any other character
+ # than one demarking an arg,kwarg or function-end
+ # it must immediately merge as a string
+ infuncstr+=str(exec_return)
+ exec_return=''
+
+ ifchar=="'":# note that this is the same as "\'"
+ # a single quote - flip status
+ single_quoted=notsingle_quoted
+ infuncstr+=char
+ continue
+
+ ifchar=='"':# note that this is the same as '\"'
+ # a double quote = flip status
+ double_quoted=notdouble_quoted
+ infuncstr+=char
+ continue
+
+ ifdouble_quotedorsingle_quoted:
+ # inside a string definition - this escapes everything else
+ infuncstr+=char
+ continue
+
+ # special characters detected inside function def
+ ifchar=='(':
+ ifnotcurr_func.funcname:
+ # end of a funcdef name
+ curr_func.funcname=infuncstr
+ curr_func.fullstr+=infuncstr+char
+ infuncstr=''
+ else:
+ # just a random left-parenthesis
+ infuncstr+=char
+ # track the open left-parenthesis
+ open_lparens+=1
+ continue
+
+ ifcharin'[]':
+ # a square bracket - start/end of a list?
+ infuncstr+=char
+ open_lsquare+=-1ifchar==']'else1
+ continue
+
+ ifcharin'{}':
+ # a curly bracket - start/end of dict/set?
+ infuncstr+=char
+ open_lcurly+=-1ifchar=='}'else1
+ continue
+
+ ifchar=='=':
+ # beginning of a keyword argument
+ ifexec_return!='':
+ infuncstr=exec_return
+ current_kwarg=infuncstr.strip()
+ curr_func.kwargs[current_kwarg]=""
+ curr_func.fullstr+=infuncstr+char
+ infuncstr=''
+ continue
+
+ ifcharin(',)'):
+ # commas and right-parens may indicate arguments ending
+
+ ifopen_lparens>1:
+ # one open left-parens is ok (beginning of arglist), more
+ # indicate we are inside an unclosed, nested (, so
+ # we need to not count this as a new arg or end of funcdef.
+ infuncstr+=char
+ open_lparens-=1ifchar==')'else0
+ continue
+
+ ifopen_lcurly>0oropen_lsquare>0:
+ # also escape inside an open [... or {... structure
+ infuncstr+=char
+ continue
+
+ ifexec_return!='':
+ # store the execution return as-received
+ ifcurrent_kwarg:
+ curr_func.kwargs[current_kwarg]=exec_return
+ else:
+ curr_func.args.append(exec_return)
+ else:
+ # store a string instead
+ ifcurrent_kwarg:
+ curr_func.kwargs[current_kwarg]=infuncstr.strip()
+ elifinfuncstr.strip():
+ # don't store the empty string
+ curr_func.args.append(infuncstr.strip())
+
+ # note that at this point either exec_return or infuncstr will
+ # be empty. We need to store the full string so we can print
+ # it 'raw' in case this funcdef turns out to e.g. lack an
+ # ending paranthesis
+ curr_func.fullstr+=str(exec_return)+infuncstr+char
+
+ current_kwarg=""
+ exec_return=''
+ infuncstr=''
+
+ ifchar==')':
+ # closing the function list - this means we have a
+ # ready function-def to run.
+ open_lparens=0
+
+ ifstrip:
+ # remove function as if it returned empty
+ exec_return=''
+ elifescape:
+ # get function and set it as escaped
+ exec_return=escape_char+curr_func.fullstr
+ else:
+ # execute the function - the result may be a string or
+ # something else
+ exec_return=self.execute(
+ curr_func,raise_errors=raise_errors,**reserved_kwargs)
+
+ ifcallstack:
+ # unnest the higher-level funcdef from stack
+ # and continue where we were
+ curr_func=callstack.pop()
+ current_kwarg=curr_func.current_kwarg
+ ifcurr_func.infuncstr:
+ # if we have an ongoing string, we must merge the
+ # exec into this as a part of that string
+ infuncstr=curr_func.infuncstr+str(exec_return)
+ exec_return=''
+ curr_func.infuncstr=''
+ single_quoted=curr_func.single_quoted
+ double_quoted=curr_func.double_quoted
+ open_lparens=curr_func.open_lparens
+ open_lsquare=curr_func.open_lsquare
+ open_lcurly=curr_func.open_lcurly
+ else:
+ # back to the top-level string - this means the
+ # exec_return should always be converted to a string.
+ curr_func=None
+ fullstr+=str(exec_return)
+ ifreturn_str:
+ exec_return=''
+ infuncstr=''
+ continue
+
+ infuncstr+=char
+
+ ifcurr_func:
+ # if there is a still open funcdef or defs remaining in callstack,
+ # these are malformed (no closing bracket) and we should get their
+ # strings as-is.
+ callstack.append(curr_func)
+ for_inrange(len(callstack)):
+ infuncstr=str(callstack.pop())+infuncstr
+
+ ifnotreturn_strandexec_return!='':
+ # return explicit return
+ returnexec_return
+
+ # add the last bit to the finished string
+ fullstr+=infuncstr
+
+ returnfullstr
+
+
[docs]defparse_to_any(self,string,raise_errors=False,**reserved_kwargs):
+ """
+ This parses a string and if the string only contains a "$func(...)",
+ the return will be the return value of that function, even if it's not
+ a string. If mixed in with other strings, the result will still always
+ be a string.
+
+ Args:
+ string (str): The string to parse.
+ raise_errors (bool, optional): If unset, leave a failing (or
+ unrecognized) inline function as unparsed in the string. If set,
+ raise an ParsingError.
+ **reserved_kwargs: If given, these are guaranteed to _always_ pass
+ as part of each parsed callable's **kwargs. These override
+ same-named default options given in `__init__` as well as any
+ same-named kwarg given in the string function. This is because
+ it is often used by Evennia to pass necessary kwargs into each
+ callable (like the current Session object for inlinefuncs).
+
+ Returns:
+ any: The return from the callable. Or string if the callable is not
+ given alone in the string.
+
+ Raises:
+ ParsingError: If a problem is encountered and `raise_errors` is True.
+
+ Notes:
+ This is a convenience wrapper for `self.parse(..., return_str=False)` which
+ accomplishes the same thing.
+
+ Examples:
+ ::
+
+ from ast import literal_eval
+ from evennia.utils.funcparser import FuncParser
+
+
+ def ret1(*args, **kwargs):
+ return 1
+
+ parser = FuncParser({"lit": lit})
+
+ assert parser.parse_to_any("$ret1()" == 1
+ assert parser.parse_to_any("$ret1() and text" == '1 and text'
+
+ """
+ returnself.parse(string,raise_errors=False,escape=False,strip=False,
+ return_str=False,**reserved_kwargs)
+
+
+#
+# Default funcparser callables. These are made available from this module's
+# FUNCPARSER_CALLABLES.
+#
+
+
[docs]deffuncparser_callable_eval(*args,**kwargs):
+ """
+ Funcparser callable. This will combine safe evaluations to try to parse the
+ incoming string into a python object. If it fails, the return will be same
+ as the input.
+
+ Args:
+ string (str): The string to parse. Only simple literals or operators are allowed.
+
+ Returns:
+ any: The string parsed into its Python form, or the same as input.
+
+ Examples:
+ - `$py(1) -> 1`
+ - `$py([1,2,3,4] -> [1, 2, 3]`
+ - `$py(3 + 4) -> 7`
+
+ """
+ args,kwargs=safe_convert_to_types(("py",{}),*args,**kwargs)
+ returnargs[0]ifargselse''
+
+
+def_apply_operation_two_elements(*args,operator="+",**kwargs):
+ """
+ Helper operating on two arguments
+
+ Args:
+ val1 (any): First value to operate on.
+ val2 (any): Second value to operate on.
+
+ Return:
+ any: The result of val1 + val2. Values must be
+ valid simple Python structures possible to add,
+ such as numbers, lists etc. The $eval is usually
+ better for non-list arithmetic.
+
+ """
+ args,kwargs=safe_convert_to_types((('py','py'),{}),*args,**kwargs)
+ ifnotlen(args)>1:
+ return''
+ val1,val2=args[0],args[1]
+ try:
+ ifoperator=="+":
+ returnval1+val2
+ elifoperator=="-":
+ returnval1-val2
+ elifoperator=="*":
+ returnval1*val2
+ elifoperator=="/":
+ returnval1/val2
+ exceptException:
+ ifkwargs.get('raise_errors'):
+ raise
+ return''
+
+
+
[docs]deffuncparser_callable_round(*args,**kwargs):
+ """
+ Funcparser callable. Rounds an incoming float to a
+ certain number of significant digits.
+
+ Args:
+ inp (str or number): If a string, it will attempt
+ to be converted to a number first.
+ significant (int): The number of significant digits. Default is None -
+ this will turn the result into an int.
+
+ Returns:
+ any: The rounded value or inp if inp was not a number.
+
+ Examples:
+ - `$round(3.5434343, 3) -> 3.543`
+ - `$round($random(), 2)` - rounds random result, e.g `0.22`
+
+ """
+ ifnotargs:
+ return''
+ args,_=safe_convert_to_types(((float,int),{})*args,**kwargs)
+
+ num,*significant=args
+ significant=significant[0]ifsignificantelse0
+ try:
+ round(num,significant)
+ exceptException:
+ ifkwargs.get('raise_errors'):
+ raise
+ return''
+
+
[docs]deffuncparser_callable_random(*args,**kwargs):
+ """
+ Funcparser callable. Returns a random number between 0 and 1, from 0 to a
+ maximum value, or within a given range (inclusive).
+
+ Args:
+ minval (str, optional): Minimum value. If not given, assumed 0.
+ maxval (str, optional): Maximum value.
+
+ Notes:
+ If either of the min/maxvalue has a '.' in it, a floating-point random
+ value will be returned. Otherwise it will be an
+ integer value in the given range.
+
+ Examples:
+ - `$random()` - random value [0 .. 1) (float).
+ - `$random(5)` - random value [0..5] (int)
+ - `$random(5.0)` - random value [0..5] (float)
+ - `$random(5, 10)` - random value [5..10] (int)
+ - `$random(5, 10.0)` - random value [5..10] (float)
+
+ """
+ args,_=safe_convert_to_types((('py','py'),{}),*args,**kwargs)
+
+ nargs=len(args)
+ ifnargs==1:
+ # only maxval given
+ minval,maxval=0,args[0]
+ elifnargs>1:
+ minval,maxval=args[:2]
+ else:
+ minval,maxval=0,1
+
+ try:
+ ifisinstance(minval,float)orisinstance(maxval,float):
+ returnminval+((maxval-minval)*random.random())
+ else:
+ returnrandom.randint(minval,maxval)
+ exceptException:
+ ifkwargs.get('raise_errors'):
+ raise
+ return''
[docs]deffuncparser_callable_choice(*args,**kwargs):
+ """
+ FuncParser callable. Picks a random choice from a list.
+
+ Args:
+ listing (list): A list of items to randomly choose between.
+ This will be converted from a string to a real list.
+
+ Returns:
+ any: The randomly chosen element.
+
+ Example:
+ - `$choice([key, flower, house])`
+ - `$choice([1, 2, 3, 4])`
+
+ """
+ ifnotargs:
+ return''
+ args,_=safe_convert_to_types(('py',{}),*args,**kwargs)
+ try:
+ returnrandom.choice(args[0])
+ exceptException:
+ ifkwargs.get('raise_errors'):
+ raise
+ return''
+
+
+
[docs]deffuncparser_callable_pad(*args,**kwargs):
+ """
+ FuncParser callable. Pads text to given width, optionally with fill-characters
+
+ Args:
+ text (str): Text to pad.
+ width (int): Width of padding.
+ align (str, optional): Alignment of padding; one of 'c', 'l' or 'r'.
+ fillchar (str, optional): Character used for padding. Defaults to a space.
+
+ Example:
+ - `$pad(text, 12, r, ' ') -> " text"`
+ - `$pad(text, width=12, align=c, fillchar=-) -> "----text----"`
+
+ """
+ ifnotargs:
+ return''
+ args,kwargs=safe_convert_to_types(
+ ((str,int,str,str),{'width':int,'align':str,'fillchar':str}),*args,**kwargs)
+
+ text,*rest=args
+ nrest=len(rest)
+ try:
+ width=int(kwargs.get("width",rest[0]ifnrest>0else_CLIENT_DEFAULT_WIDTH))
+ exceptTypeError:
+ width=_CLIENT_DEFAULT_WIDTH
+
+ align=kwargs.get("align",rest[1]ifnrest>1else'c')
+ fillchar=kwargs.get("fillchar",rest[2]ifnrest>2else' ')
+ ifalignnotin('c','l','r'):
+ align='c'
+ returnpad(str(text),width=width,align=align,fillchar=fillchar)
+
+
+
[docs]deffuncparser_callable_crop(*args,**kwargs):
+ """
+ FuncParser callable. Crops ingoing text to given widths.
+
+ Args:
+ text (str, optional): Text to crop.
+ width (str, optional): Will be converted to an integer. Width of
+ crop in characters.
+ suffix (str, optional): End string to mark the fact that a part
+ of the string was cropped. Defaults to `[...]`.
+
+ Example:
+ - `$crop(A long text, 10, [...]) -> "A lon[...]"`
+ - `$crop(text, width=11, suffix='[...]) -> "A long[...]"`
+
+ """
+ ifnotargs:
+ return''
+ text,*rest=args
+ nrest=len(rest)
+ try:
+ width=int(kwargs.get("width",rest[0]ifnrest>0else_CLIENT_DEFAULT_WIDTH))
+ exceptTypeError:
+ width=_CLIENT_DEFAULT_WIDTH
+ suffix=kwargs.get('suffix',rest[1]ifnrest>1else"[...]")
+ returncrop(str(text),width=width,suffix=str(suffix))
[docs]deffuncparser_callable_clr(*args,**kwargs):
+ """
+ FuncParser callable. Colorizes nested text.
+
+ Args:
+ startclr (str, optional): An ANSI color abbreviation without the
+ prefix `|`, such as `r` (red foreground) or `[r` (red background).
+ text (str, optional): Text
+ endclr (str, optional): The color to use at the end of the string. Defaults
+ to `|n` (reset-color).
+ Kwargs:
+ color (str, optional): If given,
+
+ Example:
+ - `$clr(r, text, n) -> "|rtext|n"`
+ - `$clr(r, text) -> "|rtext|n`
+ - `$clr(text, start=r, end=n) -> "|rtext|n"`
+
+ """
+ ifnotargs:
+ return''
+
+ startclr,text,endclr='','',''
+ iflen(args)>1:
+ # $clr(pre, text, post))
+ startclr,*rest=args
+ ifrest:
+ text,*endclr=rest
+ ifendclr:
+ endclr=endclr[0]
+ else:
+ # $clr(text, start=pre, end=post)
+ text=args[0]
+ startclr=kwargs.get("start",'')
+ endclr=kwargs.get("end",'')
+
+ startclr="|"+startclrifstartclrelse""
+ endclr="|"+endclrifendclrelse("|n"ifstartclrelse'')
+ returnf"{startclr}{text}{endclr}"
+
+
+
[docs]deffuncparser_callable_search(*args,caller=None,access="control",**kwargs):
+ """
+ FuncParser callable. Finds an object based on name or #dbref. Note that
+ this requries the parser be called with the caller's Session for proper
+ security. If called without session, the call is aborted.
+
+ Args:
+ query (str): The key or dbref to search for.
+
+ Keyword Args:
+ return_list (bool): If set, return a list of objects with
+ 0, 1 or more matches to `query`. Defaults to False.
+ type (str): One of 'obj', 'account', 'script'
+ caller (Entity): Supplied to Parser. This is required and will
+ be passed into the access check for the entity being searched for.
+ The 'control' permission is required.
+ access (str): Which locktype access to check. Unset to disable the
+ security check.
+
+ Returns:
+ any: An entity match or None if no match or a list if `return_list` is set.
+
+ Raise:
+ ParsingError: If zero/multimatch and `return_list` is False, or caller was not
+ passed into parser.
+
+ Examples:
+ - "$search(#233)"
+ - "$search(Tom, type=account)"
+ - "$search(meadow, return_list=True)"
+
+ """
+ return_list=kwargs.get("return_list","false").lower()=="true"
+
+ ifnotargs:
+ return[]ifreturn_listelseNone
+ ifnotcaller:
+ raiseParsingError("$search requires a `caller` passed to the parser.")
+
+ query=str(args[0])
+
+ typ=kwargs.get("type","obj")
+ targets=[]
+ iftyp=="obj":
+ targets=search.search_object(query)
+ eliftyp=="account":
+ targets=search.search_account(query)
+ eliftyp=="script":
+ targets=search.search_script(query)
+
+ ifnottargets:
+ ifreturn_list:
+ return[]
+ raiseParsingError(f"$search: Query '{query}' gave no matches.")
+
+ iflen(targets)>1andnotreturn_list:
+ raiseParsingError("$search: Query '{query}' found {num} matches. "
+ "Set return_list=True to accept a list".format(
+ query=query,num=len(targets)))
+
+ fortargetintargets:
+ ifnottarget.access(caller,target,access):
+ raiseParsingError('$search Cannot add found entity - access failure.')
+
+ returnlist(targets)ifreturn_listelsetargets[0]
+
+
+
[docs]deffuncparser_callable_search_list(*args,caller=None,access="control",**kwargs):
+ """
+ Usage: $objlist(#123)
+
+ Legacy alias for search with a return_list=True kwarg preset.
+
+ """
+ returnfuncparser_callable_search(*args,caller=caller,access=access,
+ return_list=True,**kwargs)
+
+
+
[docs]deffuncparser_callable_you(*args,caller=None,receiver=None,mapping=None,capitalize=False,**kwargs):
+ """
+ Usage: $you() or $you(key)
+
+ Replaces with you for the caller of the string, with the display_name
+ of the caller for others.
+
+ Keyword Args:
+ caller (Object): The 'you' in the string. This is used unless another
+ you-key is passed to the callable in combination with `mapping`.
+ receiver (Object): The recipient of the string.
+ mapping (dict, optional): This is a mapping `{key:Object, ...}` and is
+ used to find which object `$you(key)` refers to. If not given, the
+ `caller` kwarg is used.
+ capitalize (bool): Passed by the You helper, to capitalize you.
+
+ Returns:
+ str: The parsed string.
+
+ Raises:
+ ParsingError: If `caller` and `receiver` were not supplied.
+
+ Notes:
+ The kwargs should be passed the to parser directly.
+
+ Examples:
+ This can be used by the say or emote hooks to pass actor stance
+ strings. This should usually be combined with the $inflect() callable.
+
+ - `With a grin, $you() $conj(jump) at $you(tommy).`
+
+ The caller-object will see "With a grin, you jump at Tommy."
+ Tommy will see "With a grin, CharName jumps at you."
+ Others will see "With a grin, CharName jumps at Tommy."
+
+ """
+ ifargsandmapping:
+ # this would mean a $you(key) form
+ try:
+ caller=mapping.get(args[0])
+ exceptKeyError:
+ pass
+
+ ifnot(callerandreceiver):
+ raiseParsingError("No caller or receiver supplied to $you callable.")
+
+ capitalize=bool(capitalize)
+ ifcaller==receiver:
+ return"You"ifcapitalizeelse"you"
+ returncaller.get_display_name(looker=receiver)ifhasattr(caller,"get_display_name")elsestr(caller)
[docs]deffuncparser_callable_conjugate(*args,caller=None,receiver=None,**kwargs):
+ """
+ Conjugate a verb according to if it should be 2nd or third person.
+
+ Keyword Args:
+ caller (Object): The object who represents 'you' in the string.
+ receiver (Object): The recipient of the string.
+
+ Returns:
+ str: The parsed string.
+
+ Raises:
+ ParsingError: If `you` and `recipient` were not both supplied.
+
+ Notes:
+ Note that the verb will not be capitalized. It also
+ assumes that the active party (You) is the one performing the verb.
+ This automatic conjugation will fail if the active part is another person
+ than 'you'. The caller/receiver must be passed to the parser directly.
+
+ Examples:
+ This is often used in combination with the $you/You( callables.
+
+ - `With a grin, $you() $conj(jump)`
+
+ You will see "With a grin, you jump."
+ Others will see "With a grin, CharName jumps."
+
+ """
+ ifnotargs:
+ return''
+ ifnot(callerandreceiver):
+ raiseParsingError("No caller/receiver supplied to $conj callable")
+
+ second_person_str,third_person_str=verb_actor_stance_components(args[0])
+ returnsecond_person_strifcaller==receiverelsethird_person_str
+
+
+# these are made available as callables by adding 'evennia.utils.funcparser' as
+# a callable-path when initializing the FuncParser.
+
+FUNCPARSER_CALLABLES={
+ # 'standard' callables
+
+ # eval and arithmetic
+ "eval":funcparser_callable_eval,
+ "add":funcparser_callable_add,
+ "sub":funcparser_callable_sub,
+ "mult":funcparser_callable_mult,
+ "div":funcparser_callable_div,
+ "round":funcparser_callable_round,
+ "toint":funcparser_callable_toint,
+
+ # randomizers
+ "random":funcparser_callable_random,
+ "randint":funcparser_callable_randint,
+ "choice":funcparser_callable_choice,
+
+ # string manip
+ "pad":funcparser_callable_pad,
+ "crop":funcparser_callable_crop,
+ "just":funcparser_callable_justify,
+ "ljust":funcparser_callable_left_justify,
+ "rjust":funcparser_callable_right_justify,
+ "cjust":funcparser_callable_center_justify,
+ "justify":funcparser_callable_justify,# aliases for backwards compat
+ "justify_left":funcparser_callable_left_justify,
+ "justify_right":funcparser_callable_right_justify,
+ "justify_center":funcparser_callable_center_justify,
+ "space":funcparser_callable_space,
+ "clr":funcparser_callable_clr,
+}
+
+SEARCHING_CALLABLES={
+ # requires `caller` and optionally `access` to be passed into parser
+ "search":funcparser_callable_search,
+ "obj":funcparser_callable_search,# aliases for backwards compat
+ "objlist":funcparser_callable_search_list,
+ "dbref":funcparser_callable_search,
+}
+
+ACTOR_STANCE_CALLABLES={
+ # requires `you`, `receiver` and `mapping` to be passed into parser
+ "you":funcparser_callable_you,
+ "You":funcparser_callable_You,
+ "conj":funcparser_callable_conjugate,
+}
+
-"""
-Inline functions (nested form).
-
-This parser accepts nested inlinefunctions on the form
-
-```python
-$funcname(arg, arg, ...)
-```
-
-embedded in any text where any arg can be another ``$funcname()`` call.
-This functionality is turned off by default - to activate,
-`settings.INLINEFUNC_ENABLED` must be set to `True`.
-
-Each token starts with `$funcname(` where there must be no space
-between the `$funcname` and `"("`. The inlinefunc ends with a matched ending parentesis.
-`")"`.
-
-Inside the inlinefunc definition, one can use `\` to escape. This is
-mainly needed for escaping commas in flowing text (which would
-otherwise be interpreted as an argument separator), or to escape `)`
-when not intended to close the function block. Enclosing text in
-matched `\"\"\"` (triple quotes) or `'''` (triple single-quotes) will
-also escape *everything* within without needing to escape individual
-characters.
-
-The available inlinefuncs are defined as global-level functions in
-modules defined by `settings.INLINEFUNC_MODULES`. They are identified
-by their function name (and ignored if this name starts with `_`). They
-should be on the following form:
-
-```python
-def funcname (*args, **kwargs):
- # ...
-```
-
-Here, the arguments given to `$funcname(arg1,arg2)` will appear as the
-`*args` tuple. This will be populated by the arguments given to the
-inlinefunc in-game - the only part that will be available from
-in-game. `**kwargs` are not supported from in-game but are only used
-internally by Evennia to make details about the caller available to
-the function. The kwarg passed to all functions is `session`, the
-Sessionobject for the object seeing the string. This may be `None` if
-the string is sent to a non-puppetable object. The inlinefunc should
-never raise an exception.
-
-There are two reserved function names:
-
-- "nomatch": This is called if the user uses a functionname that is
- not registered. The nomatch function will get the name of the
- not-found function as its first argument followed by the normal
- arguments to the given function. If not defined the default effect is
- to print `<UNKNOWN>` to replace the unknown function.
-- "stackfull": This is called when the maximum nested function stack is reached.
- When this happens, the original parsed string is returned and the result of
- the `stackfull` inlinefunc is appended to the end. By default this is an
- error message.
-
-Syntax errors, notably failing to completely closing all inlinefunc
-blocks, will lead to the entire string remaining unparsed. Inlineparsing should
-never traceback.
-
-----
-
-"""
-
-importre
-importfnmatch
-importrandomasbase_random
-fromdjango.confimportsettings
-
-fromevennia.utilsimportutils,logger
-
-# The stack size is a security measure. Set to <=0 to disable.
-_STACK_MAXSIZE=settings.INLINEFUNC_STACK_MAXSIZE
-
-
-# example/testing inline functions
-
-
-
[docs]defrandom(*args,**kwargs):
- """
- Inlinefunc. Returns a random number between
- 0 and 1, from 0 to a maximum value, or within a given range (inclusive).
-
- Args:
- minval (str, optional): Minimum value. If not given, assumed 0.
- maxval (str, optional): Maximum value.
-
- Keyword argumuents:
- session (Session): Session getting the string.
-
- Notes:
- If either of the min/maxvalue has a '.' in it, a floating-point random
- value will be returned. Otherwise it will be an integer value in the
- given range.
-
- Example:
- `$random()`
- `$random(5)`
- `$random(5, 10)`
-
- """
- nargs=len(args)
- ifnargs==1:
- # only maxval given
- minval,maxval="0",args[0]
- elifnargs>1:
- minval,maxval=args[:2]
- else:
- minval,maxval=("0","1")
-
- if"."inminvalor"."inmaxval:
- # float mode
- try:
- minval,maxval=float(minval),float(maxval)
- exceptValueError:
- minval,maxval=0,1
- return"{:.2f}".format(minval+maxval*base_random.random())
- else:
- # int mode
- try:
- minval,maxval=int(minval),int(maxval)
- exceptValueError:
- minval,maxval=0,1
- returnstr(base_random.randint(minval,maxval))
-
-
-
[docs]defpad(*args,**kwargs):
- """
- Inlinefunc. Pads text to given width.
-
- Args:
- text (str, optional): Text to pad.
- width (str, optional): Will be converted to integer. Width
- of padding.
- align (str, optional): Alignment of padding; one of 'c', 'l' or 'r'.
- fillchar (str, optional): Character used for padding. Defaults to a
- space.
-
- Keyword Args:
- session (Session): Session performing the pad.
-
- Example:
- `$pad(text, width, align, fillchar)`
-
- """
- text,width,align,fillchar="",78,"c"," "
- nargs=len(args)
- ifnargs>0:
- text=args[0]
- ifnargs>1:
- width=int(args[1])ifargs[1].strip().isdigit()else78
- ifnargs>2:
- align=args[2]ifargs[2]in("c","l","r")else"c"
- ifnargs>3:
- fillchar=args[3]
- returnutils.pad(text,width=width,align=align,fillchar=fillchar)
-
-
-
[docs]defcrop(*args,**kwargs):
- """
- Inlinefunc. Crops ingoing text to given widths.
-
- Args:
- text (str, optional): Text to crop.
- width (str, optional): Will be converted to an integer. Width of
- crop in characters.
- suffix (str, optional): End string to mark the fact that a part
- of the string was cropped. Defaults to `[...]`.
- Keyword Args:
- session (Session): Session performing the crop.
-
- Example:
- `$crop(text, width=78, suffix='[...]')`
-
- """
- text,width,suffix="",78,"[...]"
- nargs=len(args)
- ifnargs>0:
- text=args[0]
- ifnargs>1:
- width=int(args[1])ifargs[1].strip().isdigit()else78
- ifnargs>2:
- suffix=args[2]
- returnutils.crop(text,width=width,suffix=suffix)
-
-
-
[docs]defspace(*args,**kwargs):
- """
- Inlinefunc. Inserts an arbitrary number of spaces. Defaults to 4 spaces.
-
- Args:
- spaces (int, optional): The number of spaces to insert.
-
- Keyword Args:
- session (Session): Session performing the crop.
-
- Example:
- `$space(20)`
-
- """
- width=4
- ifargs:
- width=abs(int(args[0]))ifargs[0].strip().isdigit()else4
- return" "*width
-
-
-
[docs]defclr(*args,**kwargs):
- """
- Inlinefunc. Colorizes nested text.
-
- Args:
- startclr (str, optional): An ANSI color abbreviation without the
- prefix `|`, such as `r` (red foreground) or `[r` (red background).
- text (str, optional): Text
- endclr (str, optional): The color to use at the end of the string. Defaults
- to `|n` (reset-color).
- Keyword Args:
- session (Session): Session object triggering inlinefunc.
-
- Example:
- `$clr(startclr, text, endclr)`
-
- """
- text=""
- nargs=len(args)
- ifnargs>0:
- color=args[0].strip()
- ifnargs>1:
- text=args[1]
- text="|"+color+text
- ifnargs>2:
- text+="|"+args[2].strip()
- else:
- text+="|n"
- returntext
[docs]defnomatch(name,*args,**kwargs):
- """
- Default implementation of nomatch returns the function as-is as a string.
-
- """
- kwargs.pop("inlinefunc_stack_depth",None)
- kwargs.pop("session")
-
- return"${name}({args}{kwargs})".format(
- name=name,
- args=",".join(args),
- kwargs=",".join("{}={}".format(key,val)forkey,valinkwargs.items()),
- )
-
-
-_INLINE_FUNCS={}
-
-# we specify a default nomatch function to use if no matching func was
-# found. This will be overloaded by any nomatch function defined in
-# the imported modules.
-_DEFAULT_FUNCS={
- "nomatch":lambda*args,**kwargs:"<UNKNOWN>",
- "stackfull":lambda*args,**kwargs:"\n (not parsed: ",
-}
-
-_INLINE_FUNCS.update(_DEFAULT_FUNCS)
-
-# load custom inline func modules.
-formoduleinutils.make_iter(settings.INLINEFUNC_MODULES):
- try:
- _INLINE_FUNCS.update(utils.callables_from_module(module))
- exceptImportErroraserr:
- ifmodule=="server.conf.inlinefuncs":
- # a temporary warning since the default module changed name
- raiseImportError(
- "Error: %s\nPossible reason: mygame/server/conf/inlinefunc.py should "
- "be renamed to mygame/server/conf/inlinefuncs.py (note "
- "the S at the end)."%err
- )
- else:
- raise
-
-
-# regex definitions
-
-_RE_STARTTOKEN=re.compile(r"(?<!\\)\$(\w+)\(")# unescaped $funcname( (start of function call)
-
-# note: this regex can be experimented with at https://regex101.com/r/kGR3vE/2
-_RE_TOKEN=re.compile(
- r"""
- (?<!\\)\'\'\'(?P<singlequote>.*?)(?<!\\)\'\'\'| # single-triplets escape all inside
- (?<!\\)\"\"\"(?P<doublequote>.*?)(?<!\\)\"\"\"| # double-triplets escape all inside
- (?P<comma>(?<!\\)\,)| # , (argument sep)
- (?P<end>(?<!\\)\))| # ) (possible end of func call)
- (?P<leftparens>(?<!\\)\()| # ( (lone left-parens)
- (?P<start>(?<!\\)\$\w+\()| # $funcname (start of func call)
- (?P<escaped> # escaped tokens to re-insert sans backslash
- \\\'|\\\"|\\\)|\\\$\w+\(|\\\()|
- (?P<rest> # everything else to re-insert verbatim
- \$(?!\w+\()|\'|\"|\\|[^),$\'\"\\\(]+)""",
- re.UNICODE|re.IGNORECASE|re.VERBOSE|re.DOTALL,
-)
-
-# Cache for function lookups.
-_PARSING_CACHE=utils.LimitedSizeOrderedDict(size_limit=1000)
-
-
-
[docs]classParseStack(list):
- """
- Custom stack that always concatenates strings together when the
- strings are added next to one another. Tuples are stored
- separately and None is used to mark that a string should be broken
- up into a new chunk. Below is the resulting stack after separately
- appending 3 strings, None, 2 strings, a tuple and finally 2
- strings:
-
- [string + string + string,
- None
- string + string,
- tuple,
- string + string]
-
- """
-
-
[docs]def__init__(self,*args,**kwargs):
- super().__init__(*args,**kwargs)
- # always start stack with the empty string
- list.append(self,"")
- # indicates if the top of the stack is a string or not
- self._string_last=True
[docs]defappend(self,item):
- """
- The stack will merge strings, add other things as normal
- """
- ifisinstance(item,str):
- ifself._string_last:
- self[-1]+=item
- else:
- list.append(self,item)
- self._string_last=True
- else:
- # everything else is added as normal
- list.append(self,item)
- self._string_last=False
[docs]defparse_inlinefunc(string,strip=False,available_funcs=None,stacktrace=False,**kwargs):
- """
- Parse the incoming string.
-
- Args:
- string (str): The incoming string to parse.
- strip (bool, optional): Whether to strip function calls rather than
- execute them.
- available_funcs (dict, optional): Define an alternative source of functions to parse for.
- If unset, use the functions found through `settings.INLINEFUNC_MODULES`.
- stacktrace (bool, optional): If set, print the stacktrace to log.
- Keyword Args:
- session (Session): This is sent to this function by Evennia when triggering
- it. It is passed to the inlinefunc.
- kwargs (any): All other kwargs are also passed on to the inlinefunc.
-
-
- """
- global_PARSING_CACHE
- usecache=False
- ifnotavailable_funcs:
- available_funcs=_INLINE_FUNCS
- usecache=True
- else:
- # make sure the default keys are available, but also allow overriding
- tmp=_DEFAULT_FUNCS.copy()
- tmp.update(available_funcs)
- available_funcs=tmp
-
- ifusecacheandstringin_PARSING_CACHE:
- # stack is already cached
- stack=_PARSING_CACHE[string]
- elifnot_RE_STARTTOKEN.search(string):
- # if there are no unescaped start tokens at all, return immediately.
- returnstring
- else:
- # no cached stack; build a new stack and continue
- stack=ParseStack()
-
- # process string on stack
- ncallable=0
- nlparens=0
- nvalid=0
-
- ifstacktrace:
- out="STRING: {} =>".format(string)
- print(out)
- logger.log_info(out)
-
- formatchin_RE_TOKEN.finditer(string):
- gdict=match.groupdict()
-
- ifstacktrace:
- out=" MATCH: {}".format({key:valforkey,valingdict.items()ifval})
- print(out)
- logger.log_info(out)
-
- ifgdict["singlequote"]:
- stack.append(gdict["singlequote"])
- elifgdict["doublequote"]:
- stack.append(gdict["doublequote"])
- elifgdict["leftparens"]:
- # we have a left-parens inside a callable
- ifncallable:
- nlparens+=1
- stack.append("(")
- elifgdict["end"]:
- ifnlparens>0:
- nlparens-=1
- stack.append(")")
- continue
- ifncallable<=0:
- stack.append(")")
- continue
- args=[]
- whilestack:
- operation=stack.pop()
- ifcallable(operation):
- ifnotstrip:
- stack.append((operation,[argforarginreversed(args)]))
- ncallable-=1
- break
- else:
- args.append(operation)
- elifgdict["start"]:
- funcname=_RE_STARTTOKEN.match(gdict["start"]).group(1)
- try:
- # try to fetch the matching inlinefunc from storage
- stack.append(available_funcs[funcname])
- nvalid+=1
- exceptKeyError:
- stack.append(available_funcs["nomatch"])
- stack.append(funcname)
- stack.append(None)
- ncallable+=1
- elifgdict["escaped"]:
- # escaped tokens
- token=gdict["escaped"].lstrip("\\")
- stack.append(token)
- elifgdict["comma"]:
- ifncallable>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:
- # no callable active - just a string
- stack.append(",")
- else:
- # the rest
- stack.append(gdict["rest"])
-
- ifncallable>0:
- # this means not all inlinefuncs were complete
- returnstring
-
- if_STACK_MAXSIZE>0and_STACK_MAXSIZE<nvalid:
- # if stack is larger than limit, throw away parsing
- returnstring+available_funcs["stackfull"](*args,**kwargs)
- elifusecache:
- # cache the stack - we do this also if we don't check the cache above
- _PARSING_CACHE[string]=stack
-
- # run the stack recursively
- def_run_stack(item,depth=0):
- retval=item
- ifisinstance(item,tuple):
- ifstrip:
- return""
- else:
- func,arglist=item
- args=[""]
- forarginarglist:
- ifargisNone:
- # an argument-separating comma - start a new arg
- args.append("")
- else:
- # all other args should merge into one string
- args[-1]+=_run_stack(arg,depth=depth+1)
- # execute the inlinefunc at this point or strip it.
- kwargs["inlinefunc_stack_depth"]=depth
- retval=""ifstripelsefunc(*args,**kwargs)
- returnutils.to_str(retval)
-
- retval="".join(_run_stack(item)foriteminstack)
- ifstacktrace:
- out="STACK: \n{} => {}\n".format(stack,retval)
- print(out)
- logger.log_info(out)
-
- # execute the stack
- returnretval
-
-
-
[docs]defraw(string):
- """
- Escape all inlinefuncs in a string so they won't get parsed.
-
- Args:
- string (str): String with inlinefuncs to escape.
- """
-
- def_escape(match):
- return"\\"+match.group(0)
-
- return_RE_STARTTOKEN.sub(_escape,string)
-
-
-#
-# Nick templating
-#
-
-
-"""
-This supports the use of replacement templates in nicks:
-
-This happens in two steps:
-
-1) The user supplies a template that is converted to a regex according
- to the unix-like templating language.
-2) This regex is tested against nicks depending on which nick replacement
- strategy is considered (most commonly inputline).
-3) If there is a template match and there are templating markers,
- these are replaced with the arguments actually given.
-
-@desc $1 $2 $3
-
-This will be converted to the following regex:
-
-\@desc (?P<1>\w+) (?P<2>\w+) $(?P<3>\w+)
-
-Supported template markers (through fnmatch)
- * matches anything (non-greedy) -> .*?
- ? matches any single character ->
- [seq] matches any entry in sequence
- [!seq] matches entries not in sequence
-Custom arg markers
- $N argument position (1-99)
-
-"""
-_RE_NICK_ARG=re.compile(r"\\(\$)([1-9][0-9]?)")
-_RE_NICK_TEMPLATE_ARG=re.compile(r"(\$)([1-9][0-9]?)")
-_RE_NICK_SPACE=re.compile(r"\\ ")
-
-
-
[docs]definitialize_nick_templates(in_template,out_template):
- """
- Initialize the nick templates for matching and remapping a string.
-
- Args:
- in_template (str): The template to be used for nick recognition.
- out_template (str): The template to be used to replace the string
- matched by the in_template.
-
- Returns:
- regex (regex): Regex to match against strings
- template (str): Template with markers {arg1}, {arg2}, etc for
- replacement using the standard .format method.
-
- Raises:
- evennia.utils.inlinefuncs.NickTemplateInvalid: If the in/out template
- does not have a matching number of $args.
-
- """
- # create the regex for in_template
- regex_string=fnmatch.translate(in_template)
- n_inargs=len(_RE_NICK_ARG.findall(regex_string))
- regex_string=_RE_NICK_SPACE.sub("\s+",regex_string)
- regex_string=_RE_NICK_ARG.sub(lambdam:"(?P<arg%s>.+?)"%m.group(2),regex_string)
-
- # create the out_template
- template_string=_RE_NICK_TEMPLATE_ARG.sub(lambdam:"{arg%s}"%m.group(2),out_template)
-
- # validate the tempaltes - they should at least have the same number of args
- n_outargs=len(_RE_NICK_TEMPLATE_ARG.findall(out_template))
- ifn_inargs!=n_outargs:
- raiseNickTemplateInvalid
-
- returnre.compile(regex_string),template_string
-
-
-
[docs]defparse_nick_template(string,template_regex,outtemplate):
- """
- Parse a text using a template and map it to another template
-
- Args:
- string (str): The input string to processj
- template_regex (regex): A template regex created with
- initialize_nick_template.
- outtemplate (str): The template to which to map the matches
- produced by the template_regex. This should have $1, $2,
- etc to match the regex.
-
- """
- match=template_regex.match(string)
- ifmatch:
- returnouttemplate.format(**match.groupdict())
- returnstring
-
-
-
\ No newline at end of file
diff --git a/docs/1.0-dev/_modules/evennia/utils/utils.html b/docs/1.0-dev/_modules/evennia/utils/utils.html
index 81e1984c59..774ab09e4e 100644
--- a/docs/1.0-dev/_modules/evennia/utils/utils.html
+++ b/docs/1.0-dev/_modules/evennia/utils/utils.html
@@ -62,6 +62,8 @@
importimportlibimportimportlib.utilimportimportlib.machinery
+fromastimportliteral_eval
+fromsimpleevalimportsimple_evalfromunicodedataimporteast_asian_widthfromtwisted.internet.taskimportdeferLaterfromtwisted.internet.deferimportreturnValue# noqa - used as import target
@@ -1120,7 +1122,8 @@
store_key (tuple, optional): This is only used in combination with `stop` and should be the return given from the original `repeat` call. If this is given, all other args except `stop` are ignored.
- *args, **kwargs: Used as arguments to `callback`.
+ *args: Used as arguments to `callback`.
+ **kwargs: Keyword-arguments to pass to `callback`. Returns: tuple or None: The tuple is the `store_key` - the identifier for the
@@ -1154,8 +1157,8 @@
Args: store_key (tuple): This is the return from `repeat`, used to uniquely
- identify the ticker to stop. Without the store_key, the ticker
- must be stopped by passing its parameters to `TICKER_HANDLER.remove`
+ identify the ticker to stop. Without the store_key, the ticker
+ must be stopped by passing its parameters to `TICKER_HANDLER.remove` directly. Returns:
@@ -2432,6 +2435,107 @@
returnretreturndecorator
+
+
+
[docs]defsafe_convert_to_types(converters,*args,raise_errors=True,**kwargs):
+ """
+ Helper function to safely convert inputs to expected data types.
+
+ Args:
+ converters (tuple): A tuple `((converter, converter,...), {kwarg: converter, ...})` to
+ match a converter to each element in `*args` and `**kwargs`.
+ Each converter will will be called with the arg/kwarg-value as the only argument.
+ If there are too few converters given, the others will simply not be converter. If the
+ converter is given as the string 'py', it attempts to run
+ `safe_eval`/`literal_eval` on the input arg or kwarg value. It's possible to
+ skip the arg/kwarg part of the tuple, an empty tuple/dict will then be assumed.
+ *args: The arguments to convert with `argtypes`.
+ raise_errors (bool, optional): If set, raise any errors. This will
+ abort the conversion at that arg/kwarg. Otherwise, just skip the
+ conversion of the failing arg/kwarg. This will be set by the FuncParser if
+ this is used as a part of a FuncParser callable.
+ **kwargs: The kwargs to convert with `kwargtypes`
+
+ Returns:
+ tuple: `(args, kwargs)` in converted form.
+
+ Raises:
+ utils.funcparser.ParsingError: If parsing failed in the `'py'`
+ converter. This also makes this compatible with the FuncParser
+ interface.
+ any: Any other exception raised from other converters, if raise_errors is True.
+
+ Notes:
+ This function is often used to validate/convert input from untrusted sources. For
+ security, the "py"-converter is deliberately limited and uses `safe_eval`/`literal_eval`
+ which only supports simple expressions or simple containers with literals. NEVER
+ use the python `eval` or `exec` methods as a converter for any untrusted input! Allowing
+ untrusted sources to execute arbitrary python on your server is a severe security risk,
+
+ Example:
+ ::
+
+ $funcname(1, 2, 3.0, c=[1,2,3])
+
+ def _funcname(*args, **kwargs):
+ args, kwargs = safe_convert_input(((int, int, float), {'c': 'py'}), *args, **kwargs)
+ # ...
+
+ """
+ def_safe_eval(inp):
+ ifnotinp:
+ return''
+ ifnotisinstance(inp,str):
+ # already converted
+ returninp
+
+ try:
+ returnliteral_eval(inp)
+ exceptExceptionaserr:
+ literal_err=f"{err.__class__.__name__}: {err}"
+ try:
+ returnsimple_eval(inp)
+ exceptExceptionaserr:
+ simple_err=f"{str(err.__class__.__name__)}: {err}"
+ pass
+
+ ifraise_errors:
+ fromevennia.utils.funcparserimportParsingError
+ err=(f"Errors converting '{inp}' to python:\n"
+ f"literal_eval raised {literal_err}\n"
+ f"simple_eval raised {simple_err}")
+ raiseParsingError(err)
+
+ # handle an incomplete/mixed set of input converters
+ ifnotconverters:
+ returnargs,kwargs
+ arg_converters,*kwarg_converters=converters
+ arg_converters=make_iter(arg_converters)
+ kwarg_converters=kwarg_converters[0]ifkwarg_converterselse{}
+
+ # apply the converters
+ ifargsandarg_converters:
+ args=list(args)
+ arg_converters=make_iter(arg_converters)
+ foriarg,arginenumerate(args[:len(arg_converters)]):
+ converter=arg_converters[iarg]
+ converter=_safe_evalifconverterin('py','python')elseconverter
+ try:
+ args[iarg]=converter(arg)
+ exceptException:
+ ifraise_errors:
+ raise
+ args=tuple(args)
+ ifkwarg_convertersandisinstance(kwarg_converters,dict):
+ forkey,converterinkwarg_converters.items():
+ converter=_safe_evalifconverterin('py','python')elseconverter
+ ifkeyin{**kwargs}:
+ try:
+ kwargs[key]=converter(kwargs[key])
+ exceptException:
+ ifraise_errors:
+ raise
+ returnargs,kwargs
Source code for evennia.utils.verb_conjugation.conjugate
+"""
+English verb conjugation
+
+Original Author: Tom De Smedt <tomdesmedt@organisms.be> of Nodebox
+Refactored by Griatch 2021, for Evennia.
+
+This is distributed under the GPL2 license. See ./LICENSE.txt for details.
+
+The verb.txt morphology was adopted from the XTAG morph_englis.flat:
+http://www.cis.upenn.edu/~xtag/
+
+
+"""
+
+importos
+
+_VERBS_FILE="verbs.txt"
+
+# Each verb and its tenses is a list in verbs.txt,
+# indexed according to the following keys:
+# the negated forms (for supported verbs) are ind+11.
+
+verb_tenses_keys={
+ "infinitive":0,
+ "1st singular present":1,
+ "2nd singular present":2,
+ "3rd singular present":3,
+ "present plural":4,
+ "present participle":5,
+ "1st singular past":6,
+ "2nd singular past":7,
+ "3rd singular past":8,
+ "past plural":9,
+ "past":10,
+ "past participle":11,
+}
+
+# allow to specify tenses with a shorter notation
+verb_tenses_aliases={
+ "inf":"infinitive",
+ "1sgpres":"1st singular present",
+ "2sgpres":"2nd singular present",
+ "3sgpres":"3rd singular present",
+ "pl":"present plural",
+ "prog":"present participle",
+ "1sgpast":"1st singular past",
+ "2sgpast":"2nd singular past",
+ "3sgpast":"3rd singular past",
+ "pastpl":"past plural",
+ "ppart":"past participle",
+}
+
+# Each verb has morphs for infinitve,
+# 3rd singular present, present participle,
+# past and past participle.
+# Verbs like "be" have other morphs as well
+# (i.e. I am, you are, she is, they aren't)
+# Additionally, the following verbs can be negated:
+# be, can, do, will, must, have, may, need, dare, ought.
+
+# load the conjugation forms from ./verbs.txt
+verb_tenses={}
+
+path=os.path.join(os.path.dirname(__file__),_VERBS_FILE)
+withopen(path)asfil:
+ forlineinfil.readlines():
+ wordlist=[part.strip()forpartinline.split(",")]
+ verb_tenses[wordlist[0]]=wordlist
+
+# Each verb can be lemmatised:
+# inflected morphs of the verb point
+# to its infinitive in this dictionary.
+verb_lemmas={}
+forinfinitiveinverb_tenses:
+ fortenseinverb_tenses[infinitive]:
+ iftense:
+ verb_lemmas[tense]=infinitive
+
+
+
[docs]defverb_infinitive(verb):
+ """
+ Returns the uninflected form of the verb, like 'are' -> 'be'
+
+ Args:
+ verb (str): The verb to get the uninflected form of.
+
+ Returns:
+ str: The uninflected verb form of `verb`.
+
+ """
+
+ returnverb_lemmas.get(verb,'')
+
+
+
[docs]defverb_conjugate(verb,tense="infinitive",negate=False):
+ """
+ Inflects the verb to the given tense.
+
+ Args:
+ verb (str): The single verb to conjugate.
+ tense (str): The tense to convert to. This can be given either as a long or short form
+ - "infinitive" ("inf") - be
+ - "1st/2nd/3rd singular present" ("1/2/3sgpres") - am/are/is
+ - "present plural" ("pl") - are
+ - "present participle" ("prog") - being
+ - "1st/2nd/3rd singular past" ("1/2/3sgpast") - was/were/was
+ - "past plural" ("pastpl") - were
+ - "past" - were
+ - "past participle" ("ppart") - been
+ negate (bool): Negates the verb. This only supported
+ for a limited number of verbs: be, can, do, will, must, have, may,
+ need, dare, ought.
+
+ Returns:
+ str: The conjugated verb. If conjugation fails, the original verb is returned.
+
+ Examples:
+ The verb 'be':
+ - present: I am, you are, she is,
+ - present participle: being,
+ - past: I was, you were, he was,
+ - past participle: been,
+ - negated present: I am not, you aren't, it isn't.
+
+ """
+ tense=verb_tenses_aliases.get(tense,tense)
+ verb=verb_infinitive(verb)
+ ind=verb_tenses_keys[tense]
+ ifnegate:
+ ind+=len(verb_tenses_keys)
+ try:
+ returnverb_tenses[verb][ind]
+ exceptIndexError:
+ # TODO implement simple algorithm here with +s for certain tenses?
+ returnverb
+
+
+
[docs]defverb_present(verb,person="",negate=False):
+ """
+ Inflects the verb in the present tense.
+
+ Args:
+ person (str or int): This can be 1, 2, 3, "1st", "2nd", "3rd", "plural" or "*".
+ negate (bool): Some verbs like be, have, must, can be negated.
+
+ Returns:
+ str: The present tense verb.
+
+ Example:
+ had -> have
+
+ """
+
+ person=str(person).replace("pl","*").strip("stndrgural")
+ mapping={
+ "1":"1st singular present",
+ "2":"2nd singular present",
+ "3":"3rd singular present",
+ "*":"present plural",
+ }
+ ifpersoninmappingandverb_conjugate(verb,mapping[person],negate)!="":
+ returnverb_conjugate(verb,mapping[person],negate)
+
+ returnverb_conjugate(verb,"infinitive",negate)
+
+
+
[docs]defverb_present_participle(verb):
+ """
+ Inflects the verb in the present participle.
+
+ Args:
+ verb (str): The verb to inflect.
+
+ Returns:
+ str: The inflected verb.
+
+ Examples:
+ give -> giving, be -> being, swim -> swimming
+
+ """
+ returnverb_conjugate(verb,"present participle")
+
+
+
[docs]defverb_past(verb,person="",negate=False):
+ """
+
+ Inflects the verb in the past tense.
+
+ Args:
+ verb (str): The verb to inflect.
+ person (str, optional): The person can be specified with 1, 2, 3,
+ "1st", "2nd", "3rd", "plural", "*".
+ negate (bool, optional): Some verbs like be, have, must, can be negated.
+
+ Returns:
+ str: The inflected verb.
+
+ Examples:
+ give -> gave, be -> was, swim -> swam
+
+ """
+
+ person=str(person).replace("pl","*").strip("stndrgural")
+ mapping={
+ "1":"1st singular past",
+ "2":"2nd singular past",
+ "3":"3rd singular past",
+ "*":"past plural",
+ }
+ ifpersoninmappingandverb_conjugate(verb,mapping[person],negate)!="":
+ returnverb_conjugate(verb,mapping[person],negate)
+
+ returnverb_conjugate(verb,"past",negate=negate)
+
+
+
[docs]defverb_past_participle(verb):
+ """
+ Inflects the verb in the present participle.
+
+ Args:
+ verb (str): The verb to inflect.
+
+ Returns:
+ str: The inflected verb.
+
+ Examples:
+ give -> given, be -> been, swim -> swum
+
+ """
+ returnverb_conjugate(verb,"past participle")
+
+
+
[docs]defverb_all_tenses():
+ """
+ Get all all possible verb tenses.
+
+ Returns:
+ list: A list if string names.
+
+ """
+
+ returnlist(verb_tenses_keys.keys())
+
+
+
[docs]defverb_tense(verb):
+ """
+ Returns a string from verb_tenses_keys representing the verb's tense.
+
+ Args:
+ verb (str): The verb to check the tense of.
+
+ Returns:
+ str: The tense.
+
+ Example:
+ given -> "past participle"
+
+ """
+ infinitive=verb_infinitive(verb)
+ data=verb_tenses[infinitive]
+ fortenseinverb_tenses_keys:
+ ifdata[verb_tenses_keys[tense]]==verb:
+ returntense
+ ifdata[verb_tenses_keys[tense]+len(verb_tenses_keys)]==verb:
+ returntense
+
+
+
[docs]defverb_is_tense(verb,tense):
+ """
+ Checks whether the verb is in the given tense.
+
+ Args:
+ verb (str): The verb to check.
+ tense (str): The tense to check.
+
+ Return:
+ bool: If verb matches given tense.
+
+ """
+ tense=verb_tenses_aliases.get(tense,tense)
+ returnverb_tense(verb)==tense
+
+
+
[docs]defverb_is_present(verb,person="",negated=False):
+ """
+ Checks whether the verb is in the present tense.
+
+ Args:
+ verb (str): The verb to check.
+ person (str): Check which person.
+ negated (bool): Check if verb was negated.
+
+ Returns:
+ bool: If verb was in present tense.
+
+ """
+
+ person=str(person).replace("*","plural")
+ tense=verb_tense(verb)
+ iftenseisnotNone:
+ if"present"intenseandpersonintense:
+ ifnotnegated:
+ returnTrue
+ elif"n't"inverbor" not"inverb:
+ returnTrue
+ returnFalse
+
+
+
[docs]defverb_is_present_participle(verb):
+ """
+ Checks whether the verb is in present participle.
+
+ Args:
+ verb (str): The verb to check.
+
+ Returns:
+ bool: Result of check.
+
+ """
+
+ tense=verb_tense(verb)
+ returntense=="present participle"
+
+
+
[docs]defverb_is_past(verb,person="",negated=False):
+ """
+ Checks whether the verb is in the past tense.
+
+ Args:
+ verb (str): The verb to check.
+ person (str): The person to check.
+ negated (bool): Check if verb is negated.
+
+ Returns:
+ bool: Result of check.
+
+ """
+
+ person=str(person).replace("*","plural")
+ tense=verb_tense(verb)
+ iftenseisnotNone:
+ if"past"intenseandpersonintense:
+ ifnotnegated:
+ returnTrue
+ elif"n't"inverbor" not"inverb:
+ returnTrue
+
+ returnFalse
+
+
+
[docs]defverb_is_past_participle(verb):
+ """
+ Checks whether the verb is in past participle.
+
+ Args:
+ verb (str): The verb to check.
+
+ Returns:
+ bool: The result of the check.
+
+ """
+ tense=verb_tense(verb)
+ returntense=="past participle"
+
+
+
[docs]defverb_actor_stance_components(verb):
+ """
+ Figure out actor stance components of a verb.
+
+ Args:
+ verb (str): The verb to analyze
+
+ Returns:
+ tuple: The 2nd person (you) and 3rd person forms of the verb,
+ in the same tense as the ingoing verb.
+
+ """
+ tense=verb_tense(verb)
+ if"participle"intenseor"plural"intense:
+ return(verb,verb)
+ iftense=="infinitive"or"present"intense:
+ you_str=verb_present(verb,person="2")orverb
+ them_str=verb_present(verb,person="3")orverb+"s"
+ else:
+ you_str=verb_past(verb,person="2")orverb
+ them_str=verb_past(verb,person="3")orverb+"s"
+ return(you_str,them_str)
+
+
+
\ No newline at end of file
diff --git a/docs/1.0-dev/_modules/index.html b/docs/1.0-dev/_modules/index.html
index 580eee8efd..6c90db1098 100644
--- a/docs/1.0-dev/_modules/index.html
+++ b/docs/1.0-dev/_modules/index.html
@@ -217,11 +217,11 @@
diff --git a/docs/1.0-dev/_sources/Components/FuncParser.md.txt b/docs/1.0-dev/_sources/Components/FuncParser.md.txt
new file mode 100644
index 0000000000..696b20c3be
--- /dev/null
+++ b/docs/1.0-dev/_sources/Components/FuncParser.md.txt
@@ -0,0 +1,382 @@
+# The Inline Function Parser
+
+The [FuncParser](api:evennia.utils.funcparser#evennia.utils.funcparser.FuncParser) extracts and executes
+'inline functions'
+embedded in a string on the form `$funcname(args, kwargs)`. Under the hood, this will
+lead to a call to a Python function you control. The inline function call will be replaced by
+the return from the function.
+
+```python
+from evennia.utils.funcparser import FuncParser
+
+def _power_callable(*args, **kwargs):
+ """This will be callable as $square(number, power=) in string"""
+ pow = int(kwargs.get('power', 2))
+ return float(args[0]) ** pow
+
+parser = FuncParser({"pow": _power_callable})
+
+```
+Next, just pass a string into the parser, optionally containing `$func(...)` markers:
+
+```python
+parser.parse("We have that 4 x 4 x 4 is $pow(4, power=3).")
+"We have that 4 x 4 x 4 is 64."
+```
+
+Normally the return is always converted to a string but you can also get the actual data type from the call:
+
+```python
+parser.parse_to_any("$pow(4)")
+16
+```
+
+To show a `$func()` verbatim in your code without parsing it, escape it as either `$$func()` or `\$func()`:
+
+
+```python
+parser.parse("This is an escaped $$pow(4) and so is this \$pow(3)")
+"This is an escaped $pow(4) and so is this $pow(3)"
+```
+
+## Uses in default Evennia
+
+The FuncParser can be applied to any string. Out of the box it's applied in a few situations:
+
+- _Outgoing messages_. All messages sent from the server is processed through FuncParser and every
+ callable is provided the [Session](./Sessions) of the object receiving the message. This potentially
+ allows a message to be modified on the fly to look different for different recipients.
+- _Prototype values_. A [Prototype](./Prototypes) dict's values are run through the parser such that every
+ callable gets a reference to the rest of the prototype. In the Prototype ORM, this would allow builders
+ to safely call functions to set non-string values to prototype values, get random values, reference
+ other fields of the prototype, and more.
+- _Actor-stance in messages to others_. In the
+ [Object.msg_contents](api:evennia.objects.objects#DefaultObject.msg_contents) method,
+ the outgoing string is parsed for special `$You()` and `$conj()` callables to decide if a given recipient
+ should see "You" or the character's name.
+
+```important::
+ The inline-function parser is not intended as a 'softcode' programming language. It does not
+ have things like loops and conditionals, for example. While you could in principle extend it to
+ do very advanced things and allow builders a lot of power, all-out coding is something
+ Evennia expects you to do in a proper text editor, outside of the game, not from inside it.
+```
+
+## Using the FuncParser
+
+You can apply inline function parsing to any string. The
+[FuncParser](api:evennia.utils.funcparser.FuncParser) is found in `evennia.utils.funcparser.py`.
+
+```python
+from evennia.utils import funcparser
+
+parser = FuncParser(callables, **default_kwargs)
+parsed_string = parser.parser(input_string, raise_errors=False,
+ escape=False, strip=False,
+ return_str=True, **reserved_kwargs)
+
+# callables can also be passed as paths to modules
+parser = FuncParser(["game.myfuncparser_callables", "game.more_funcparser_callables"])
+```
+
+Here, `callables` points to a collection of normal Python functions (see next section) for you to make
+available to the parser as you parse strings with it. It can either be
+- A `dict` of `{"functionname": callable, ...}`. This allows you do pick and choose exactly which callables
+ to include and how they should be named. Do you want a callable to be available under more than one name?
+ Just add it multiple times to the dict, with a different key.
+- A `module` or (more commonly) a `python-path` to a module. This module can define a dict
+ `FUNCPARSER_CALLABLES = {"funcname": callable, ...}` - this will be imported and used like ther `dict` above.
+ If no such variable is defined, _every_ top-level function in the module (whose name doesn't start with
+ an underscore `_`) will be considered a suitable callable. The name of the function will be the `$funcname`
+ by which it can be called.
+- A `list` of modules/paths. This allows you to pull in modules from many sources for your parsing.
+
+The other arguments to the parser:
+
+- `raise_errors` - By default, any errors from a callable will be quietly ignored and the result
+ will be that the failing function call will show verbatim. If `raise_errors` is set,
+ then parsing will stop and whatever exception happened will be raised. It'd be up to you to handle
+ this properly.
+- `escape` - Returns a string where every `$func(...)` has been escaped as `\$func()`.
+- `strip` - Remove all `$func(...)` calls from string (as if each returned `''`).
+- `return_str` - When `True` (default), `parser` always returns a string. If `False`, it may return
+ the return value of a single function call in the string. This is the same as using the `.parse_to_any`
+ method.
+- The `**default/reserved_keywords` are optional and allow you to pass custom data into _every_ function
+ call. This is great for including things like the current session or config options. Defaults can be
+ replaced if the user gives the same-named kwarg in the string's function call. Reserved kwargs are always passed,
+ ignoring defaults or what the user passed. In addition, the `funcparser` and `raise_errors`
+ reserved kwargs are always passed - the first is a back-reference to the `FuncParser` instance and the second
+ is the `raise_errors` boolean passed into `FuncParser.parse`.
+
+Here's an example of using the default/reserved keywords:
+
+```python
+def _test(*args, **kwargs):
+ # do stuff
+ return something
+
+parser = funcparser.FuncParser({"test": _test}, mydefault=2)
+result = parser.parse("$test(foo, bar=4)", myreserved=[1, 2, 3])
+```
+Here the callable will be called as
+
+```python
+_test('foo', bar='4', mydefault=2, myreserved=[1, 2, 3],
+ funcparser=, raise_errrors=False)
+```
+
+The `mydefault=2` kwarg could be overwritten if we made the call as `$test(mydefault=...)`
+but `myreserved=[1, 2, 3]` will _always_ be sent as-is and will override a call `$test(myreserved=...)`.
+The `funcparser`/`raise_errors` kwargs are also always included as reserved kwargs.
+
+## Defining custom callables
+
+All callables made available to the parser must have the following signature:
+
+```python
+def funcname(*args, **kwargs):
+ # ...
+ return something
+```
+
+> The `*args` and `**kwargs` must always be included. If you are unsure how `*args` and `**kwargs` work in Python,
+> [read about them here](https://www.digitalocean.com/community/tutorials/how-to-use-args-and-kwargs-in-python-3).
+
+The input from the innermost `$funcname(...)` call in your callable will always be a `str`. Here's
+an example of an `$toint` function; it converts numbers to integers.
+
+ "There's a $toint(22.0)% chance of survival."
+
+What will enter the `$toint` callable (as `args[0]`) is the _string_ `"22.0"`. The function is responsible
+for converting this to a number so that we can convert it to an integer. We must also properly handle invalid
+inputs (like non-numbers).
+
+If you want to mark an error, raise `evennia.utils.funcparser.ParsingError`. This stops the entire parsing
+of the string and may or may not raise the exception depending on what you set `raise_errors` to when you
+created the parser.
+
+However, if you _nest_ functions, the return of the innermost function may be something other than
+a string. Let's introduce the `$eval` function, which evaluates simple expressions using
+Python's `literal_eval` and/or `simple_eval`.
+
+ "There's a $toint($eval(10 * 2.2))% chance of survival."
+
+Since the `$eval` is the innermost call, it will get a string as input - the string `"10 * 2.2"`.
+It evaluates this and returns the `float` `22.0`. This time the outermost `$toint` will be called with
+this `float` instead of with a string.
+
+> It's important to safely validate your inputs since users may end up nesting your callables in any order.
+> See the next section for useful tools to help with this.
+
+In these examples, the result will be embedded in the larger string, so the result of the entire parsing
+will be a string:
+
+```python
+ parser.parse(above_string)
+ "There's a 22% chance of survival."
+```
+
+However, if you use the `parse_to_any` (or `parse(..., return_str=True)`) and _don't add any extra string around the outermost function call_,
+you'll get the return type of the outermost callable back:
+
+```python
+parser.parse_to_any("$toint($eval(10 * 2.2)%")
+"22%"
+parser.parse_to_any("$toint($eval(10 * 2.2)")
+22
+```
+
+### Safe convertion of inputs
+
+Since you don't know in which order users may use your callables, they should always check the types
+of its inputs and convert to the type the callable needs. Note also that when converting from strings,
+there are limits what inputs you can support. This is because FunctionParser strings are often used by
+non-developer players/builders and some things (such as complex classes/callables etc) are just not
+safe/possible to convert from string representation.
+
+In `evennia.utils.utils` is a helper called
+[safe_convert_to_types](api:evennia.utils.utils#evennia.utils.utils.safe_convert_to_types). This function
+automates the conversion of simple data types in a safe way:
+
+```python
+from evennia.utils.utils import safe_convert_to_types
+
+def _process_callable(*args, **kwargs):
+ """
+ A callable with a lot of custom options
+
+ $process(expression, local, extra=34, extra2=foo)
+
+ """
+ args, kwargs = safe_convert_to_type(
+ (('py', 'py'), {'extra1': int, 'extra2': str}),
+ *args, **kwargs)
+
+ # args/kwargs should be correct types now
+
+```
+
+In other words,
+
+```python
+args, kwargs = safe_convert_to_type(
+ (tuple_of_arg_converters, dict_of_kwarg_converters), *args, **kwargs)
+```
+
+Each converter should be a callable taking one argument - this will be the arg/kwarg-value to convert. The
+special converter `"py"` will try to convert a string argument to a Python structure with the help of the
+following tools (which you may also find useful to experiment with on your own):
+
+- [ast.literal_eval](https://docs.python.org/3.8/library/ast.html#ast.literal_eval) is an in-built Python
+ function. It
+ _only_ supports strings, bytes, numbers, tuples, lists, dicts, sets, booleans and `None`. That's
+ it - no arithmetic or modifications of data is allowed. This is good for converting individual values and
+ lists/dicts from the input line to real Python objects.
+- [simpleeval](https://pypi.org/project/simpleeval/) is a third-party tool included with Evennia. This
+ allows for evaluation of simple (and thus safe) expressions. One can operate on numbers and strings
+ with +-/* as well as do simple comparisons like `4 > 3` and more. It does _not_ accept more complex
+ containers like lists/dicts etc, so this and `literal_eval` are complementary to each other.
+
+```warning::
+ It may be tempting to run use Python's in-built ``eval()`` or ``exec()`` functions as converters since
+ these are able to convert any valid Python source code to Python. NEVER DO THIS unless you really, really
+ know that ONLY developers will ever modify the string going into the callable. The parser is intended
+ for untrusted users (if you were trusted you'd have access to Python already). Letting untrusted users
+ pass strings to ``eval``/``exec`` is a MAJOR security risk. It allows the caller to run arbitrary
+ Python code on your server. This is the path to maliciously deleted hard drives. Just don't do it and
+ sleep better at night.
+```
+
+## Default callables
+
+These are some example callables you can import and add your parser. They are divided into
+global-level dicts in `evennia.utils.funcparser`. Just import the dict(s) and merge/add one or
+more to them when you create your `FuncParser` instance to have those callables be available.
+
+### `evennia.utils.funcparser.FUNCPARSER_CALLABLES`
+
+These are the 'base' callables.
+
+- `$eval(expression)` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_eval)) -
+ this uses `literal_eval` and `simple_eval` (see previous section) attemt to convert a string expression
+ to a python object. This handles e.g. lists of literals `[1, 2, 3]` and simple expressions like `"1 + 2"`.
+- `$toint(number)` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_toint)) -
+ always converts an output to an integer, if possible.
+- `$add/sub/mult/div(obj1, obj2)` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_add)) -
+ this adds/subtracts/multiplies and divides to elements together. While simple addition could be done with
+ `$eval`, this could for example be used also to add two lists together, which is not possible with `eval`;
+ for example `$add($eval([1,2,3]), $eval([4,5,6])) -> [1, 2, 3, 4, 5, 6]`.
+- `$round(float, significant)` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_round)) -
+ rounds an input float into the number of provided significant digits. For example `$round(3.54343, 3) -> 3.543`.
+- `$random([start, [end]])` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_random)) -
+ this works like the Python `random()` function, but will randomize to an integer value if both start/end are
+ integers. Without argument, will return a float between 0 and 1.
+- `$randint([start, [end]])` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_randint)) -
+ works like the `randint()` python function and always returns an integer.
+- `$choice(list)` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_choice)) -
+ the input will automatically be parsed the same way as `$eval` and is expected to be an iterable. A random
+ element of this list will be returned.
+- `$pad(text[, width, align, fillchar])` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_pad)) -
+ this will pad content. `$pad("Hello", 30, c, -)` will lead to a text centered in a 30-wide block surrounded by `-`
+ characters.
+- `$crop(text, width=78, suffix='[...]')` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_crop)) -
+ this will crop a text longer than the width, by default ending it with a `[...]`-suffix that also fits within
+ the width. If no width is given, the client width or `settings.DEFAULT_CLIENT_WIDTH` will be used.
+- `$space(num)` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_space)) -
+ this will insert `num` spaces.
+- `$just(string, width=40, align=c, indent=2)` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_justify)) -
+ justifies the text to a given width, aligning it left/right/center or 'f' for full (spread text across width).
+- `$ljust` - shortcut to justify-left. Takes all other kwarg of `$just`.
+- `$rjust` - shortcut to right justify.
+- `$cjust` - shortcut to center justify.
+- `$clr(startcolor, text[, endcolor])` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_clr)) -
+ color text. The color is given with one or two characters without the preceeding `|`. If no endcolor is
+ given, the string will go back to neutral, so `$clr(r, Hello)` is equivalent to `|rHello|n`.
+
+### `evennia.utils.funcparser.SEARCHING_CALLABLES`
+
+These are callables that requires access-checks in order to search for objects. So they require some
+extra reserved kwargs to be passed when running the parser:
+
+```python
+parser.parse_to_any(string, caller=
This parser accepts nested inlinefunctions on the form
-
$funcname(arg, arg, ...)
-
-
-
embedded in any text where any arg can be another **$funcname()** call.
-This functionality is turned off by default - to activate,
-settings.INLINEFUNC_ENABLED must be set to True.
-
Each token starts with $funcname( where there must be no space
-between the $funcname and “(“. The inlinefunc ends with a matched ending parentesis.
-“)”.
-
Inside the inlinefunc definition, one can use ** to escape. This is
-mainly needed for escaping commas in flowing text (which would
-otherwise be interpreted as an argument separator), or to escape **)
-when not intended to close the function block. Enclosing text in
-matched “”” (triple quotes) or ‘’’ (triple single-quotes) will
-also escape everything within without needing to escape individual
-characters.
-
The available inlinefuncs are defined as global-level functions in
-modules defined by settings.INLINEFUNC_MODULES. They are identified
-by their function name (and ignored if this name starts with _). They
-should be on the following form:
-
deffuncname(*args,**kwargs):
- # ...
-
-
-
Here, the arguments given to $funcname(arg1,arg2) will appear as the
-*args tuple. This will be populated by the arguments given to the
-inlinefunc in-game - the only part that will be available from
-in-game. **kwargs are not supported from in-game but are only used
-internally by Evennia to make details about the caller available to
-the function. The kwarg passed to all functions is session, the
-Sessionobject for the object seeing the string. This may be None if
-the string is sent to a non-puppetable object. The inlinefunc should
-never raise an exception.
-
There are two reserved function names:
-
-
“nomatch”: This is called if the user uses a functionname that is
-not registered. The nomatch function will get the name of the
-not-found function as its first argument followed by the normal
-arguments to the given function. If not defined the default effect is
-to print <UNKNOWN> to replace the unknown function.
-
“stackfull”: This is called when the maximum nested function stack is reached.
-When this happens, the original parsed string is returned and the result of
-the stackfull inlinefunc is appended to the end. By default this is an
-error message.
-
-
Syntax errors, notably failing to completely closing all inlinefunc
-blocks, will lead to the entire string remaining unparsed. Inlineparsing should
-never traceback.
Inlinefunc. Returns a random number between
-0 and 1, from 0 to a maximum value, or within a given range (inclusive).
-
-
Parameters
-
-
minval (str, optional) – Minimum value. If not given, assumed 0.
-
maxval (str, optional) – Maximum value.
-
-
-
-
-
Keyword argumuents:
session (Session): Session getting the string.
-
-
-
Notes
-
If either of the min/maxvalue has a ‘.’ in it, a floating-point random
-value will be returned. Otherwise it will be an integer value in the
-given range.
Custom stack that always concatenates strings together when the
-strings are added next to one another. Tuples are stored
-separately and None is used to mark that a string should be broken
-up into a new chunk. Below is the resulting stack after separately
-appending 3 strings, None, 2 strings, a tuple and finally 2
-strings:
strip (bool, optional) – Whether to strip function calls rather than
-execute them.
-
available_funcs (dict, optional) – Define an alternative source of functions to parse for.
-If unset, use the functions found through settings.INLINEFUNC_MODULES.
-
stacktrace (bool, optional) – If set, print the stacktrace to log.
-
-
-
Keyword Arguments
-
-
session (Session) – This is sent to this function by Evennia when triggering
-it. It is passed to the inlinefunc.
-
kwargs (any) – All other kwargs are also passed on to the inlinefunc.
Initialize the nick templates for matching and remapping a string.
-
-
Parameters
-
-
in_template (str) – The template to be used for nick recognition.
-
out_template (str) – The template to be used to replace the string
-matched by the in_template.
-
-
-
Returns
-
regex (regex) – Regex to match against strings
-template (str): Template with markers {arg1}, {arg2}, etc for
-replacement using the standard .format method.
-
-
-
\ No newline at end of file
diff --git a/docs/1.0-dev/api/evennia.utils.utils.html b/docs/1.0-dev/api/evennia.utils.utils.html
index 94966e30e2..567c6962e9 100644
--- a/docs/1.0-dev/api/evennia.utils.utils.html
+++ b/docs/1.0-dev/api/evennia.utils.utils.html
@@ -733,8 +733,7 @@ ticker instead of creating a new one.
should be the return given from the original repeat call. If this
is given, all other args except stop are ignored.
*args – Used as arguments to callback.
-
**kwargs –
Used as arguments to callback.
-
+
**kwargs – Keyword-arguments to pass to callback.
Returns
@@ -1546,6 +1545,56 @@ function has no arg or kwarg named ‘caller’.
This turns the decorated function or method into a generator.
Helper function to safely convert inputs to expected data types.
+
+
Parameters
+
+
converters (tuple) – A tuple ((converter, converter,…), {kwarg: converter, …}) to
+match a converter to each element in *args and **kwargs.
+Each converter will will be called with the arg/kwarg-value as the only argument.
+If there are too few converters given, the others will simply not be converter. If the
+converter is given as the string ‘py’, it attempts to run
+safe_eval/literal_eval on the input arg or kwarg value. It’s possible to
+skip the arg/kwarg part of the tuple, an empty tuple/dict will then be assumed.
+
*args – The arguments to convert with argtypes.
+
raise_errors (bool, optional) – If set, raise any errors. This will
+abort the conversion at that arg/kwarg. Otherwise, just skip the
+conversion of the failing arg/kwarg. This will be set by the FuncParser if
+this is used as a part of a FuncParser callable.
+
**kwargs – The kwargs to convert with kwargtypes
+
+
+
Returns
+
tuple – (args, kwargs) in converted form.
+
+
Raises
+
+
utils.funcparser.ParsingError – If parsing failed in the ‘py’
+converter. This also makes this compatible with the FuncParser
+interface.
+
any – Any other exception raised from other converters, if raise_errors is True.
+
+
+
+
Notes
+
This function is often used to validate/convert input from untrusted sources. For
+security, the “py”-converter is deliberately limited and uses safe_eval/literal_eval
+which only supports simple expressions or simple containers with literals. NEVER
+use the python eval or exec methods as a converter for any untrusted input! Allowing
+untrusted sources to execute arbitrary python on your server is a severe security risk,
tense (str) – The tense to convert to. This can be given either as a long or short form
+- “infinitive” (“inf”) - be
+- “1st/2nd/3rd singular present” (“1/2/3sgpres”) - am/are/is
+- “present plural” (“pl”) - are
+- “present participle” (“prog”) - being
+- “1st/2nd/3rd singular past” (“1/2/3sgpast”) - was/were/was
+- “past plural” (“pastpl”) - were
+- “past” - were
+- “past participle” (“ppart”) - been
+
negate (bool) – Negates the verb. This only supported
+for a limited number of verbs: be, can, do, will, must, have, may,
+need, dare, ought.
+
+
+
Returns
+
str – The conjugated verb. If conjugation fails, the original verb is returned.
+
+
+
Examples
+
The verb ‘be’:
+- present: I am, you are, she is,
+- present participle: being,
+- past: I was, you were, he was,
+- past participle: been,
+- negated present: I am not, you aren’t, it isn’t.
+
+
+
\ No newline at end of file
diff --git a/docs/1.0-dev/genindex.html b/docs/1.0-dev/genindex.html
index 5afd8e1f19..8952495da4 100644
--- a/docs/1.0-dev/genindex.html
+++ b/docs/1.0-dev/genindex.html
@@ -273,7 +273,7 @@